Fork me on GitHub

2009/10/13

はてな
twitter stream APIとは、twitterのステータス更新に対してキーワードでtrackしたり、あるグループ内に属するステータス更新をフィルタしたり出来るAPIなのだけど、実際にはchunkedなストリームが流れて来ているのであって、これを使ったWebアプリを作る際にはlong pollを使うのが良い。ただしクライアントサイドでjavascriptを処理する際に
  • サーバサイドでステータス更新をFIFOに溜め込む
  • クライアントからリクエストをブロック(long poll)しFIFOからステータスを送出する
  • クライアントサイドでlong pollを行い画面を更新する
  • 再度サーバへリクエストを投げる
を繰り返すのであれば、せっかくストリームなのにアプリサーバとの接続を切ってしまう事になる。出来ればクライアントからWebアプリもストリーミングとしたい。しかしながらサーバからのステータス更新を受け取るのであれば、JSON/JSONPなアクションが必要になる。
ここで使える!と思ったのがmultipart/mixedです。

multipart/mixedはboundaryで区切られたボディ部を繰り返し送出する際に用いられる仕組みで、実はXHR(XMLHttpRequest)はこのmultipart/mixedなレスポンスを受け取る事が出来る。
受け取るとは言っても、実際にはreadyStateが変化する...でしかないのだけど、これを扱うのに良いライブラリがある。
diggで使われているDUI.jsとStream.jsです。
digg's stream at master - GitHub

Alpha repository for DUI.Stream. When it stabilizes, it will be merged back into DUI

http://github.com/digg/stream
このstreamライブラリは内部でreadyStateが3になるのを待ち、タイマを張ってboundaryを監視するものです。つまり1 body部を扱う事になるので、使う際にはmime typeを指定する事になります。
var s = new DUI.Stream();
s.listen('text/html', function(payload) {
  ...
});
s.load('/push');
この仕組みを使って、twitterからのストリームをクライアントにmultipart/mixedで送出し、かつクライアントも接続を保持したままレスポンスを受け取るのです。接続を切らないので、ほぼタイムロス無しのリアルタイムと言って良いでしょう。
今日はこれをsinatraを使って実装してみました。出来れば一度動かしてみて下さい。これまでリクエストを繰り返し送っていたアプリケーションと比べてスムーズにデータを受け取っているのが分かって頂けるかと思います。
上記diggのライブラリを作業フォルダのstatic/jsフォルダ配下に置き、以下のファイル(fast-twitter-stream.rb)を作業フォルダ直下に置きます。
require 'rubygems'
require 'sinatra'
require 'json'  
require 'tweetstream'  
require 'pit'

# doesn't work on thin and webrick
set :server, 'mongrel'
set :public, File.dirname(__FILE__) + '/static'

config = Pit.get("twitter.com", :require => {
    "username" => "your username in twitter",
    "password" => "your password in twitter"
})

get '/' do
  <<HTML
  <html> <head>
    <title>Server Push</title>
    <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.3.1/jquery.min.js"></script>
    <script type="text/javascript" src="/js/DUI.js"></script>
    <script type="text/javascript" src="/js/Stream.js"></script>
    <script type="text/javascript">
    $(function() {
      var s = new DUI.Stream();
      s.listen('text/javascript', function(payload) {
        var status = eval(payload);
        $('#content').append('<p>' + status.text + '</p>');
      });
      s.load('/push');
    });
    </script>
  </head>
  <body>
    <h1>Server Push</h1>
    <div id="content"></div>
  </body>
</html>
HTML
end

get '/push' do
  boundary = '|||'
  response['Content-Type'] = 'multipart/mixed; boundary="' + boundary + '"'

  MultipartResponse.new(boundary, 'text/javascript')
end

class MultipartResponse
  def initialize(boundary, content_type)
    @boundary = boundary
    @content_type = content_type
  end

  def each
    TweetStream::Client.new(config[:username], config[:password]).sample do |status|  
      yield "--#{@boundary}\nContent-Type: #{@content_type}\n(#{status.to_json})"
    end  
  end
end
元となったコードはyoupyさんの物を参考にさせて頂きました。

ブラウザから"http://localhost:4567/"にアクセスすると、もの凄い勢いでステータスが追加されていくのが分かるかと思います。しかも切断していないのでかなり高速です。
お試しの効果には個人差があります。

このmultipart/mixed、結構使えるなーと思いました。得にリアルタイムな画像ストリーミングとして送出したり、リアルタイム電光掲示板の様な物にも使えるかと思います。

今回のサンプルもgithubに置いてあります。
mattn's fast-twitter-stream at master - GitHub

fast twitter stream client using multipart/mixed.

http://github.com/mattn/fast-twitter-stream
よろしければご参考までに。

JavaScript & Ajax プロが教える“本当の使い方” JavaScript & Ajax プロが教える“本当の使い方”
MdN編集部
MdN / ¥ 2,940 (2009-07-31)
 
発送可能時間:在庫あり。


2009/09/07

はてな
個人的には一番使っていて無いとちょっと不便に感じる自作のグリモンといえば「Google Reader Full Feed」なのですが、最近メインのブラウザをGoogle Chromeに変えた事もあり、使えずにちょっぴり不便になってました。
しかしながらGoogle Chromeに移植するとなれば簡単には行かないだろう事が分かっていたので移植するのを躊躇していました。先日、はてなブックマーク数を表示するGoogle Chrome Extensionも作った事だし、少しは知識もついたので、ようやく重い腰をあげて作ってみました。
最初は移植を考えてましたが、結構元にしているLDR FullFeedのコードがまばらになっていてメンテナンス性も悪かったので、今回は元のコードを捨てて1から作り直しました。とはいっても中で使っている部品などはConstellationさんの物や、os0xさんの物を使わせて頂いています。感謝

画面キャプチャは以下みたいな感じです。
chrome-grff
操作感はFirefox版とほとんど同じになってます。
今のところ、残課題はSITEINFOをGearsを使ってキャッシュする事くらいだと思ってますが、もしよければ使ってみた感想など頂けると助かります。
ソースは例のごとく、githubに置いてあります。
mattn's chrome-grff at master - GitHub

google reader full feed for chrome

http://github.com/mattn/chrome-grff/tree/master
インストールは以下のリンクから...
chrome-grff.crx
追記
キーは g じゃなく、z ですので、お気をつけて。

2009/09/02

はてな
こんな拡張が欲しい人なんて半ば病気ですよ。

夜になるとエアコン無しに過ごせる涼しい季節になって来ました。皆さん如何お過ごしでしょうか。

最近Google Chromeを使っているのですが、AutoPagerizeのGoogle Chrome拡張を入れてみて感動し、被はてなブックマーク数を画面下に表示する拡張が欲しくなったので、つい勢いで作ってしまいました。
はいはい。病気病気

スクリーンショットはこんな感じ
chrome-hbcount

os0xさんの記事をふんだんに参考にしながらなんとか作り上げました(os0x++)。
以下その作成手順。

まず、manifest.jsonを用意しました。
manifest.json
{
  "name": "hatena bookmark counter",
  "description": "hatena bookmark counter for google chrome. hatena bookmark is social bookmark web service. see 'http://b.hatena.ne.jp/'. this extension show image of numbers which how many users bookmarked the URL.",
  "version": "0.0.1",
  "icons": { "normal": "hbcount.gif" },
  "permissions": [ "tabs" ],
  "toolstrips": [ "hbcount.html" ],
  "content_scripts": [
    {
      "js": [ "hbcount.js" ],
      "matches": [ "http://*/*" ]
    }
  ]
}
今回の拡張は、ページがロードされた瞬間に発動するjavascriptと、Google Chrome起動中に画面下で常駐するためのtoolstripsと呼ばれるHTMLコンテンツを使いました。
まずユーザスクリプト側。
hbcount.js
if (window == top{
  var port = chrome.extension.connect();
  port.onMessage.addListener(function(data) {
    location.href = data.url;
  });
  port.postMessage({url: location.href});
}
ユーザスクリプト側からはtoolstripのコントロールに対してアクセス出来ませんのでpostMessageでメッセージを送信し被はてなブックマーク数アイコンを更新します。なおtoolstripにあるアイコンをクリックすると、はてなブックマークエントリページを開く様になっているのですが、toolstrip側から現在開いているタブのコンテンツにはアクセスする事も出来たのですが、少し危険を感じたのでpostMessageを使いtoolstrip側からユーザスクリプト側にURLを返信してlocation.hrefでの画面遷移をさせています。
真ん中のイベント待ちはその為の物です。なお、topかどうかを確認しているのは現在のURL以外でもこの拡張が走ってしまうのを防止している小細工です。例えばiframeなんかでアフィが表示されているとそれに対してもこの拡張が実行されてしまいます。

次に本体であるtoolstripのソースは以下の通り。
hbcount.html
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
</head>
<script type="text/javascript">
window.onload = function() {
  var hbcount = document.getElementById("hbcount");
  chrome.extension.onConnect.addListener(function(port) {
    function update(url) {
      hbcount.src = 'hbcount.gif';
      hbcount.onclick = null;
      if (url.match(/^chrome:\/\//)) return;
      url = url.replace(/#/g, '%23');
      hbcount.src = 'http://b.hatena.ne.jp/entry/image/' + url;
      hbcount.onclick = function() { port.postMessage({'url': 'http://b.hatena.ne.jp/entry/' + url}); }
    }
    port.onMessage.addListener(function(data) { update(data.url); });
    chrome.tabs.onSelectionChanged.addListener(function(id, props) {
      chrome.tabs.get(id, function(tab) { update(tab.url); });
    });
  });
};
</script>
<img src="hbcount.gif" id="hbcount" style="cursor: pointer;" title="はてなブックマーク"/>
ユーザスクリプトからメッセージを受け取りupdate関数にて更新します。アイコンクリック時のメッセージ返信を行う処理もここにあります。
その下にある処理ではタブが切り替わったタイミングで現在の被ブックマーク数を更新する処理になります。この場合、いくらmanifest.jsonでhttp://のみと宣言していてもタブに対するイベント登録を行うのでchrome://といったURLも対象になってしまいます。update関数でURLを避けているのはその為です。

さて、ここから話がそれ始めます。
Google Chromeの拡張はchromeブラウザ自身でパッキングする事が出来ます。ただchromeへのパスをいちいち書いてられないのでMakefileを書きました。
Makefile
ifndef CHROME
ifneq ($(windir),)

# Windows
DEST = "$(shell pwd)\chrome-hbcount"
CHROME = "$(USERPROFILE)/Local Settings/Application Data/Google/Chrome/Application/chrome.exe"

else

# Other Platform: Linux? Mac?
DEST = $(shell pwd)/chrome-hbcount

CHROME = $(shell which crxmake)
ifeq ($(CHROME),)
CHROME = $(shell which google-chrome)
endif
ifeq ($(CHROME),)
CHROME = $(shell which chromium-browser)
endif
ifeq ($(CHROME),)
CHROME = chrome
endif
endif
endif

SRCS = hbcount.gif hbcount.html hbcount.js manifest.json

all : chrome-hbcount.crx

first : $(SRCS)
    @-rm -r $(DEST)
    @mkdir $(DEST)
    @cp $(SRCS) $(DEST)/.
    $(CHROME) --pack-extension=$(DEST)

chrome-hbcount.crx : $(SRCS)
    @-rm -r $(DEST)
    @mkdir $(DEST)
    @cp $(SRCS) $(DEST)/.
    $(CHROME) --pack-extension=$(DEST) --pack-extension-key=chrome-hbcount.pem

clean:
    @-rm *.crx
    @-rm -r $(DEST)
初回だけmake first、以降はmakeでビルドです。
WindowsでもLinuxでもMacでも使える様に、かつgoogle-chromeが入っていない場合にはchromium-browserを使う様になっています。ちなみにcrxmakeというのはConstellationさんが書いたchrome拡張のパッケージングツールです(Constellation++)。chromeを起動する事なくパッケージング出来ます。なおchrome-hbcountという部分を書き換えれば、他の拡張でも使えるかもしれません。

さらに拡張を書いていると拡張のバージョンがちょくちょくあがります。
Google Chrome(chromium-browser)の拡張にはupdate_urlという設定項目があり、決まった形のXMLファイルを用意しておけば自動アップデート出来る様になる予定があるそうなのですが、拡張を書き換える度にいちいちupdate.xmlを書き換えてアップロードするも面倒くさそうだったので、手順を自動化出来る様にしてみました。簡単には
  • manifest.jsonからバージョンを抜き出す
  • 抜き出したバージョンでupdate.xmlを更新する
  • githubのダウンロードページにあるファイルを一旦削除する
  • 拡張モジュール(crx)とupdate.xmlをアップロードする
という一連の流れをPerlスクリプトにしてみました。
dist-upload.pl
#!/usr/bin/perl

use strict;
use warnings;

use JSON;
use Perl6::Slurp;
use WWW::Mechanize;
use Net::GitHub::Upload;
use Config::Pit;

my $config = pit_get('github-upload', require => {
    'login' => 'your login id on github.com',
    'password' => 'your password on github.com',
});

my $manifest = from_json(slurp 'manifest.json');
my $id = $manifest->{id};
my $version = $manifest->{version};

open my $fh, '>update.xml';
print $fh <<EOF;
<gupdate xmlns="http://www.google.com/update2/response" protocol="2.0">
    <app appid="$id">
        <updatecheck codebase="http://cloud.github.com/downloads/mattn/chrome-hbcount/chrome-hbcount.crx" version="$version" />
    </app>
</gupdate>
EOF
close $fh;

my $mech = WWW::Mechanize->new;
$mech->get('https://github.com/login');
$mech->submit_form(
    form_number => 2,
    fields      => {
        login => $config->{login},
        password => $config->{password},
});

$mech->get('http://github.com/mattn/chrome-hbcount/downloads');
for my $form (@{$mech->forms}) {
    if ($form->action =~ /^http:\/\/github.com\/mattn\/chrome-hbcount\/downloads\//) {
        print "deleting ".$form->action."\n";
        $mech->request($form->click);
    }
}

chomp(my $user  = `git config github.user`);
chomp(my $token = `git config github.token`);
my $gh = Net::GitHub::Upload->new(
    login => $user,
    token => $token,
);
print "uploading chrome-hbcount.crx\n";
$gh->upload( repos => 'mattn/chrome-hbcount', file  => 'chrome-hbcount.crx' );
print "uploading update.xml\n";
$gh->upload( repos => 'mattn/chrome-hbcount', file  => 'update.xml' );
こちらもmattnとかchrome-hbcountという部分を書き換えれば、他の拡張で使えるかもしれません。ちなみにNet::GitHub::Uploadはtypesterさんのモジュールです(typester++)。
これで、あとは
  • 拡張を書き換える
  • manifest.jsonのバージョンを上げる
  • make
  • dist-upload.pl
という単純な手順で皆様に最新の拡張を自動ダウンロードして頂ける...予定です。まだupdate.xmlを使った自動アップデート機能は動いてないらしいです。


さて、はてなブックマークカウンタ拡張に話を戻して...
コードおよび拡張ファイルはgithubに置いてあります。Downloadのページからでもダウンロード出来ますが、以下のリンクでインストール出来る様になっています。
hatena bookmark counter for google chrome
リポジトリは以下のリンク先にあります。
mattn's chrome-hbcount at master - GitHu

hatena bookmark counter for google chrome

http://github.com/mattn/chrome-hbcount/tree/master
ライセンスはBSDにしましたので、どうぞお好きに使って下さい。流用してdelicious拡張なんてのもいいですね。

2009/08/25

はてな
ネタ的にはZIGOROuさんかhasegawaさんのネタっぽいが...
@if(0)==(0) ECHO OFF
CScript.exe //NoLogo //E:JScript "%~f0" %*
GOTO :EOF
@end

function wsock_ConnectionRequest(reqId) {    
    if (socket.State != 0/* closed */) socket.Close();
    socket.Accept(reqId);
}

function wsock_DataArrival(bytesTotal) {    
    var data = script.Run('GetData', socket, bytesTotal);
    socket.SendData([
        "HTTP/1.1 200 OK",
        "Connection: closed",
        "Content-Type: text/html;",
        "",
        "Hello World! " + new Date(),
        ""
    ].join("\n"));
    // 相手が閉じてくれないので閉じたいけど待たないとレスポンスが無くなる
    WScript.Sleep(1000);
    socket.Close();
    socket.Listen();
}

var socket = WScript.CreateObject('MSWinsock.Winsock', 'wsock_');
var script = WScript.CreateObject('ScriptControl');
script.language = 'VBScript';
script.AddObject('WScript', WScript);
script.AddCode([
    'Function GetData(socket, bytesTotal):',
    '  Dim data:',
    '  socket.GetData data, vbString, bytesTotal:',
    '  GetData = data:',
    'End Function'
].join(''));

socket.Bind(8080);
socket.Listen();
while (socket.State != 9/* error */{ 
    WScript.Sleep(100);
} 

// vim:set ft=javascript:
GetDataがByRefなので、ScriptControlを使ってます。