凹みTips

C++、JavaScript、Unity、ガジェット等の Tips について雑多に書いています。

Twitter Streaming API + node.js でブラウザ用簡易 Twitter クライアントを作ってみた

はじめに

前回、node.js で Twitter Streaming API につないでみた - 凹みTips という記事を書きました。今回は取得した情報をクライアント側(ブラウザ側)へ取得した情報を Socket.IO(Socket.IO: the cross-browser WebSocket for realtime apps.) を用いて送信する、簡単な Twitter クライアントを作ってみました。

動作

こんな感じで動きます。

ううむ、vim で検索しているのだけど日本人居ない。

クライアント側で表示するページを作成

...
// クライアント側で表示するページを用意
var fs = require('fs');
var app = require('http').createServer(
	function(req, res) {
		fs.readFile(__dirname + '/index.html', function(err, data) {
			if (err) {
				res.writeHead(500);
				return res.end('Error loading index.html');
			}
			res.writeHead(200);
			res.end(data);
		});
	}
).listen(8080);
...

こんな感じで書いてあげれば、同フォルダ内の index.html を読み取って表示してくれるみたいです。例えば hogehoge.com で稼働させれば hogehoge.com:8080 にアクセスすれば index.html が表示されます。別途 HTTP サーバが動いていればそちらからアクセスしても構いません(動画では Apache で動かしてます)。

Socket.IOを使ってクライアント側とやり取り

...
// クライアント側へSocket.IO をオープン
var io     = require('socket.io').listen(3000);
var clientCnt = 0;
io.sockets.on('connection', function(socket) {
	console.log('Connected');
	console.log('Sockets: ' + ++clientCnt);
}).on('disconnect', function() {
	console.log('Disconnected');
	console.log('Sockets: ' + --clientCnt);
});
...

これでクライアント側と Socket.IO を用いて通信ができます。通信に使用するポート listen で指定できます。今回の例で言えば 3000 番で通信を行なっているので、hogehoge.com:3000 に対してソケットを作ればリアルタイムな通信が可能となるわけです。

Twitter Streaming API から取ってきた情報を送信

...
// Twitter から Streaming API で取得したデータを
// クライアント側へ Socket.IO 経由で送信
// Streaming API
const host = {
	host : 'stream.twitter.com',
	port : 443,
	path : '/1/statuses/filter.json?track=vim',
	// path : '/1/statuses/sample.json',
	auth : 'USER:PASSWORD'
};
var https  = require('https');
var request = https.get(host, function(response) {
	console.log('Response: ' + response.statusCode);
}).on('response', function(response) {
	response.on('data', function(chunk) {
		var tweet = JSON.parse(chunk);
		if ('user' in tweet && 'name' in tweet.user) {
			io.sockets.emit('tweet', {
				name: tweet.user.name,
				text: tweet.text,
				time: tweet.created_at,
				img : tweet.user.profile_image_url,
			});
		}
	});
}).on('error', function(e){
	console.log(e);
});
...

Twitter から取ってくるところは前回と同じです。前はサーバ側のコンソールに表示するだけでしたが、今回は io.sockets.emit を実行することで接続しているすべてのクライアントに対して tweet メッセージを送信しています。例として filter.json を用いて vim に関連したキーワードを取ってきています。USER:PASSWORD は適当なものに書き換えて下さい。

まとめ

ここまでの一連のコードをまとめると次のようになります。

// Streaming API
const host = {
	host : 'stream.twitter.com',
	port : 443,
	path : '/1/statuses/filter.json?track=vim',
	// path : '/1/statuses/sample.json',
	auth : 'USER:PASSWORD'
};

var https  = require('https');
var fs     = require('fs');
var io     = require('socket.io').listen(3000);
var clientCnt = 0;

// クライアント側で表示するページを用意
var app = require('http').createServer(
	function(req, res) {
		fs.readFile(__dirname + '/index.html', function(err, data) {
			if (err) {
				res.writeHead(500);
				return res.end('Error loading index.html');
			}
			res.writeHead(200);
			res.end(data);
		});
	}
).listen(8080);

// クライアント側へSocket.IO をオープン
io.sockets.on('connection', function(socket) {
	console.log('Connected');
	console.log('Sockets: ' + ++clientCnt);
}).on('disconnect', function() {
	console.log('Disconnected');
	console.log('Sockets: ' + --clientCnt);
});

// Twitter から Streaming API で取得したデータを
// クライアント側へ Socket.IO 経由で送信
var request = https.get(host, function(response) {
	console.log('Response: ' + response.statusCode);
}).on('response', function(response) {
	response.on('data', function(chunk) {
		var tweet = JSON.parse(chunk);
		if ('user' in tweet && 'name' in tweet.user) {
			io.sockets.emit('tweet', {
				name: tweet.user.name,
				text: tweet.text,
				time: tweet.created_at,
				img : tweet.user.profile_image_url,
			});
		}
	});
}).on('error', function(e){
	console.log(e);
});

// 例外キャッチ
process.on('uncaughtException', function (err) {
	console.log('uncaughtException => ' + err);
});

これを server.js として作ったのであれば次のようにして実行します。

$ node server.js

クライアント側で接続

こんな感じでやります。
まず、socket.io.js をロードします。

<script src="http://hecom.in:3000/socket.io/socket.io.js" type="text/javascript"></script>

で、接続します。

...
// 接続
var socket = io.connect('http://hecom.in:3000');
...

最後に、やってきた tweet メッセージを補足して書き出します。

...
// tweet を書き出し
socket.on('tweet', function(tweet) {
	// tweet.name とか tweet.txt とかサーバから送信したデータを利用して書き出し
});
...

で、飛躍しますが、これらをまとめて装飾したりして書いたものが以下になります。Android 携帯用に文字とか画像とかデカ目にしてます。エラーメッセージの表示のためだけに twitter bootstrap 使ってます。

<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="cache-control" content="no-cache">
<meta http-equiv="expires" content="0">
<title>Tw Streaming API Test</title>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js" type="text/javascript"></script>
<script src="http://hogehoge.com:3000/socket.io/socket.io.js" type="text/javascript"></script>
<link rel="stylesheet" href="http://hogehoge.com/twitter/bootstrap/css/bootstrap.css" type="text/css" />
<script>
$(function(){
	// 切断時の再接続までの時間(sec)
	const RECONNECT_DELAY = 5;
	var msgTimer = null;

	// エラーメッセージ
	function error(str) {
		var msg = $('#msg');
		if (msgTimer) {
			clearTimeout(msgTimer);
		}
		msg.show()
		   .html(str)
		   .removeClass('alert-success')
		   .addClass('alert-error');
	}

	// 成功メッセージ
	function success(str) {
		var msg = $('#msg');
		msg.show()
		   .html(str)
		   .removeClass('alert-error')
		   .addClass('alert-success');
		msgTimer = setTimeout( function() {
			msg.hide(1000, function() {
				msg.hide();
			});
			mstTimer = null;
		}, 2000);
	}

	// node が動いていない場合
	if (!('io' in window)) {
		error('<p>エラーが発生しました。socket.io.js が読み込めません。</p>');
		return;
	}

	// 接続
	var socket = io.connect('http://hogehoge.com:3000', {
		'reconnect': true,
		'reconnection delay': RECONNECT_DELAY*1000,
		'reconnection limit': RECONNECT_DELAY*1000,
		'max reconnection attempts': Infinity,
	});
	var connection = false;

	// つながった時の処理
	socket.on('connect', function() {
		connection = true;
		success('<p>接続しました</p>');
	});

	// 切断された時の処理
	socket.on('disconnect', function() {
		connection = false;
		error('<p>切断しました。' + RECONNECT_DELAY + ' 秒後に再度接続します。</p>');

		var timer = null;
		var reconnect = function(time) {
			time = (time - 1 < 0) ? RECONNECT_DELAY - 1 : time - 1;
			setTimeout(function() {
				if (connection) return;
				error('<p>切断しました。' + time + ' 秒後に再度接続します。</p>');
				reconnect(time);
			}, 1000);
		};
		reconnect(RECONNECT_DELAY);
	});

	// ツイートが来た時
	var tweetNum = 0;
	socket.on('tweet', function(tweet) {
		$('<div id="tweet' + tweetNum + '" class="tweet ' + (((tweetNum++)%2) ? 'odd' : 'even') + '">' +
			'<div class="tw_img"><img src="' + tweet.img  + '" /></div>' +
			'<div class="tw_content">' +
				'<div class="tw_id">'  + tweet.name + '<span class="tw_time">' + tweet.time + '</span></div>' +
				'<div class="tw_txt">' + tweet.text + '</div>' +
			'</div>' +
		'</div>').hide().prependTo('#tweets').show('fast');

		if (tweetNum >= 100) {
			$('#tweet' + (tweetNum - 100)).remove();
		}
	});

});
</script>
<style type="text/css">
* {
	margin: 0;
	padding: 0;
}

body {
	font-family: "ヒラギノ角ゴ Pro W3", "メイリオ", "MS Pゴシック", sans-serif;
	font-size: 2em;
}

h1 {
	padding: 0.5em;
	font-size: 1.5em;
	color: #fff;
	text-shadow: 1px 1px 6px #08a;
	-webkit-box-shadow: 0px 1px 6px #555;
	-moz-box-shadow: 0px 1px 6px #555;
	background: #00A0D1;
	background: -moz-linear-gradient(top, #0ad, #08b);
	background: -webkit-gradient(linear, left top, left bottom, from(#0ad), to(#08b));
}

#msg p {
	font-size: 1em;
}

#tweets .tweet {
	padding: 14px;
	min-height: 96px;
	clear:both;
	border-bottom: 1px #f5f5f5 solid;
}

#tweets .even {
	background: -moz-linear-gradient(top, #fff, #f0f0f0);
	background: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#f0f0f0));
}

#tweets .odd {
	background: -moz-linear-gradient(top, #e8edff, #e4e7fa);
	background: -webkit-gradient(linear, left top, left bottom, from(#e8edff), to(#e4e7fa));
}

#tweets .tw_img img {
	position: relative;
	width: 96px;
	height: 96px;
}

#tweets .tw_content {
	position: relative;
	top : -96px;
	left: 106px;
	margin-bottom: -96px;
	margin-right: 106px;
}

#tweets .tw_content .tw_id {
	color: #888;
	font-weight: bold;
}

#tweets .tw_content .tw_time {
	margin-left: 1em;
	font-size: 0.8em;
	font-weight: normal;
	color: #bbb;
}

#tweets .tw_content .tw_txt {
	margin-top: 0.5em;
	line-height: 1em;
	color: #555;
}
</style>
</head>
<body>
	<h1>Tw Client w/ node.js</h1>
	<div id="msg" class="alert alert-block"></div>
	<div id="tweets"></div>
</body>
</html>

hogehoge.com のところは自分のサーバに合わせて書き換えます。
たったこれだけのコードでサーバとのやりとりもできてしまう node.js すごい。

問題点

Android で見ていたら、雰囲気は良いのですがキャッシュがたまってたまって…。以下のサイトを参考に meta タグを追加してみたのですが、改善されませんでした。

んーむ、どなたかご存知でしたら教えて下さい。