node.jsを使って簡単にwebサイトにインスタントメッセンジャーを組み込む方法(その1)

node.jsを使ってWebサイトにインスタントメッセンジャー
Google Talkでも、Yahoo!メッセンジャーでも、MSN Messengerでも、Facebook ChatでもXMPP対応のもの)
を組み込む方法の紹介(というかメモ)です。


デモ
(IEだとAjaxでエラーになっているっぽいです。ライブラリはIEでも動くっぽいので何かがおかしいのだと思いますが、何がおかしいか分かる人いたら教えてください。)

XMPPって何よ?

あんまり理解してなくてもWebに組み込む程度なら出来ますが、詳しく知りたい人は自分で調べてください。
Extensible Messaging and Presence Protocol - Wikipedia

Webサイトで対応する場合の仕組み

XMPPはHTTPプロトコルでは無いので、直接ブラウザから使用する事はできません。
なんらかの手段でHTTPとXMPPの変換する必要がありますが、この変換にはBOSHというプロトコルが使われている様です。
XMPP Technologies: BOSH


つまり、
Webサイト ─(HTTP)→ BOSHサーバ ─(XMPP)→ IMサーバ
という流れになります。

構成

今回はBOSHサーバーの実装として、node.js上で動くnode-xmpp-boshを使う事にします。
BOSHクライアントはStrophe.jsというJavaScriptライブラリを使います。


ブラウザ(Strophe.js) ─(HTTP)→ node-xmpp-bosh ─(XMPP)→ IMサーバ
の流れです。


今回はHerokuでNode.jsを動かしてみます。
Node.js 0.4.7
Strophe.js
node-xmpp-bosh


ここでは、開発マシンは、VMPlayer上のUbuntu11.04を使用します。(CentOS5.6でも大体同じ感じでいけました。)

node.jsとnpmのインストール

node.jsのインストールは簡単なので、公式ガイド等をみながらnpmのインストールまでやってください。
バージョンは最新だとherokuやnode-xmpp-boshに対応しないので、0.4.7にしてください。
https://github.com/joyent/node/wiki/Installation


なんかわからない事があったりうまくいかない事があったら自分で調べてください。
これからnode.jsを始める人のためのインストール系エントリまとめ

herokuの設定

公式のガイドがとてもわかりやすいので、公式ガイドをみながらサンプルのHello Worldを作ってみてください(無料範囲で作れます)。
今回はデータベースは使わないのでSQL Databaseのところは飛ばしても大丈夫です。
http://devcenter.heroku.com/articles/node-js

node-xmpp-boshインストール

これも簡単で、
「npm install node-xmpp-bosh」でいけます。
参考


とりあえずHerokuで動かすだけならこれで十分ですが、
↓ここ等を参考に、いろいろ設定してみても良いと思います。
https://code.google.com/p/node-xmpp-bosh/wiki/DebianHowTo

boshパッケージと、boshに必要なパッケージの設定

herokuの設定中に作成した、package.jsonのdependenciesに必要パッケージを追加します。
node-xmpp-boshをインストールしたディレクトリにあるpackage.jsonを参考に追加すれば良いと思います。
自分の場合は↓のように追加しました。

{
  "name": "node-example",
  "version": "0.0.1",
  "dependencies": {
    "dns-srv":"0.0.6",
    "eventpipe":"0.0.3",
    "jsdom":"0.2.0",
    "htmlparser":"1.7.3",
    "request":"1.9.5",
    "ltx":"0.0.5",
    "node-expat":"1.3.2",
    "node-uuid":"1.2.0",
    "tav":"0.1.0",
    "underscore":"1.1.6",
    "node-xmpp-bosh":"0.2.3"
  }
}

Heroku上でnode-xmpp-boshを動かす

boshをインストールしたディレクトリにある、

  • bosh.conf.example.js
  • run-server.js

をプロジェクトフォルダにコピーしてください。

bosh.conf.example.jsのファイル名をbosh.conf.jsに変更して、少々修正します。
exports.config = {
	port: process.env.PORT || 5280, //←Herokuに割り当てられたportを動的に設定する設定
	host: '0.0.0.0',                //これはこのままでOK
	path: /^\/http-bind(\/+)?$/,    //これ以降は必要に応じて変更してください。
	logging: 'INFO', 

	// The maximum number of bytes that the BOSH server will 
	// "hold" from the client
	max_data_held: 100000, 
次に、run-server.jsを修正します。
var fs   = require('fs');
var path = require('path');

var BOSH_DEFAULT_CONFIG_PATH = 'bosh.js.conf'; //コンフィグファイルのパスをさっき作成したものをみるように変更します。

...省略

	var nxb    = require("node-xmpp-bosh"); //node-xmpp-boshはパッケージから読み込むように修正します。
	//require("./src/main.js");
Procfileを修正します。

run-server.jsを実行するように変更します。

web: node run-server.js


ここまで終わったらHerokuにデプロイしてください。
「heroku logs」コマンドなどで確認すると、動いている事が確認できると思います。

Webのクライアント作成

strophe.jsを公式サイトからゲットします。
http://strophe.im/strophejs/


次のHTMLファイルをstrophe.jsと同じ場所に作成します。
(var BOSH_SERVICEの値は自分のサーバーに変更してください。)

<html>
<head>
<title>XMPP with JavaScript</title>
<script src='http://ajax.googleapis.com/ajax/libs/jquery/1.2.6/jquery.min.js'></script>
<script src='strophe.js'></script>

<script>
	var BOSH_SERVICE = 'http://node-xmpp-boshサーバーのアドレス/http-bind/';
	var connection = null;

	function log(msg) {
		$('#log').append('<div></div>').append(document.createTextNode(msg));
	}

	function onConnect(status) {
		if (status == Strophe.Status.CONNECTING) {
			log('Strophe is connecting.');
		} else if (status == Strophe.Status.CONNFAIL) {
			log('Strophe failed to connect.');
			$('#connect').get(0).value = 'connect';
		} else if (status == Strophe.Status.DISCONNECTING) {
			log('Strophe is disconnecting.');
		} else if (status == Strophe.Status.DISCONNECTED) {
			log('Strophe is disconnected.');
			$('#connect').get(0).value = 'connect';
		} else if (status == Strophe.Status.CONNECTED) {
			log('Strophe is connected.');
			log('Send a message to ' + connection.jid +
			' to talk to me.');

			connection.addHandler(onMessage, null, 'message', null, null,  null);
			connection.send($pres().tree());
		}
	}

	function onMessage(msg) {
		var to = msg.getAttribute('to');
		var from = msg.getAttribute('from');
		var type = msg.getAttribute('type');
		var elems = msg.getElementsByTagName('body');

		if (type == "chat" && elems.length > 0) {
			var body = elems[0];

			log('I got a message from ' + from + ': ' +
			Strophe.getText(body));
		}

		// we must return true to keep the handler alive.
		// returning false would remove it after it finishes.
		return true;
	}

	function sendMessage() {
		var message = $('#message').get(0).value;
		var to = $('#to').get(0).value;
		
		if(message && to){
			var reply = $msg({
				to: to,
				type: 'chat'
			})
			.cnode(Strophe.xmlElement('body', message));
			connection.send(reply.tree());

			log('I sent ' + to + ': ' + message);
		}
	}

	$(document).ready( function () {
		connection = new Strophe.Connection(BOSH_SERVICE);

		// Uncomment the following lines to spy on the wire traffic.
		//connection.rawInput = function (data) { log('RECV: ' + data); };
		//connection.rawOutput = function (data) { log('SEND: ' + data); };

		// Uncomment the following line to see all the debug output.
		//Strophe.log = function (level, msg) { log('LOG: ' + msg); };

		$('#connect').bind('click', function () {
			var button = $('#connect').get(0);
			if (button.value == 'connect') {
				button.value = 'disconnect';

		var jid = $('#jid').get(0).value;
				connection.connect(jid,
				$('#pass').get(0).value,
				onConnect);
			} else {
				button.value = 'connect';
				connection.disconnect();
			}
		});
		
		$('#send').bind('click', function () {
			sendMessage();
		});
	});
</script>
</head>
<body>
	<div id='login' style='text-align: center'>
		<form name='cred'>
			<label for='jid'>
				JID:
			</label>
			<input type='text' id='jid'>
			<label for='pass'>
				Password:
			</label>
			<input type='password' id='pass'>
			<input type='button' id='connect' value='connect'>
		</form>
	</div>
	<div id='login' style='text-align: center'>
		<label for='to'>
			to:
		</label>
		<input type='text' id='to'>
		<label for='message'>
			message:
		</label>
		<input type='text' id='message'>
		<input type='button' id='send' value='send'>
	</div>
	<hr>
	<div id='log'>
	</div>
</body>
</html>

という事で、

完成です。
作成したHTMLをブラウザで開くと使えます。
GoogleアカウントやFacebookアカウントを使ってログインできます(HTTPS対応してないのでログインは自己責任でお願いします)。
Facebookアカウントを使用する場合は、
ユーザーID@chat.facebook.comがJIDになります。


次回はFacebookOAuth認証を使ったログインを試してみます。