Node.jsのチャットデータをNSFに保存してみる

それではいよいよ、リアルタイムの会話をNSFに保存してみます。プログラムコードのベースは、前回のDominoを離れてNode.jsだけでチャットの実装をしてみる - Chiburu Systemsのブログの「domtest2」になります。

チャットデータと、DominoのREST APIを取り持つ、新しいカスタムモジュールを準備します。/domtest2直下に「NodeChat.js」ファイルを作成します。

// RESTクライアントモジュールを使用します。
var Client = require('node-rest-client').Client;

// NodeChatクラスを新設します。
function NodeChat(user)
{
  this.user = user;

  // 基本認証を組み込んで、RESTクライアントを初期化します。
  this.client = new Client({
    user: user.username
    , password: user.password
  });
}

さらに、Chatオブジェクトにchatメソッドを追加します。

// チャットデータをNSFにPOSTします。
NodeChat.prototype.chat = function(data)
{
  var userObj = this.user;
  var cliObj = this.client;
  return new Promise(function(resolve, reject)
  {
    var args = {
      'parameters': {
        form: 'MainTopic'
        , computewithform: true
      }
      , 'headers': {
        'Content-Type': 'application/json'
      }
      , 'data': {
        'Subject': data.message
      }
    };
    cliObj.post(
      'http://localhost/NodeChat.nsf/api/data/documents'
      , args
      , function(retData, response)
      {
        if (retData.code && retData.code !== 200)
          reject(retData);
        else
          resolve(userObj);
      }
    ).on('error', reject);
  });
}

Dominoデータサービスによる文書の追加は、以下のリンクに情報があります。

IBM Notes and Domino Application Development wiki : IBM Domino Access Services 9.0.1

URLの指定先は、http(s)://(ホスト)/(NSF)/api/data/documentsとなります。 これに、クエリとしてform=MainTopic&computewithform=trueを付けることで、「フォームはMainTopic、保存時にフォーム全体を計算」を解釈してくれます。 フォームに保存するデータはJSON形式にし、「data」プロパティとして追加します。その時に「Content-Type: application/json」というヘッダー情報を付けるようにします。

締めくくりに、NodeChatクラスをエキスポートしておきます。

module.exports = NodeChat;

以上で、NodeChat.jsの実装は終わりです。次に、NodeChatを使用する側となる、「mySocket.js」にコードを追加していきましょう。

var Socket = require('socket.io');
var cookie = require('cookie');
var cookieParser = require('cookie-parser');
var NodeChat = require('./NodeChat');
var io;

モジュールをロードする場所に、「./NodeChat」を追加しておきます。次に、エキスポートする関数に一つ、Userという引数を追加しておきます。

module.exports = function(server, sessionStore, User)
{
  io = new Socket(server);

  var getUser = function(socket)
  {
    return new Promise(function(resolve, reject)
    {
      User.findById(socket.session.passport.user.id, function(err, user)
      {
        if (err)
          reject(err);
        else
          resolve(user);
      });
    });
  };

前回は必要がなかったユーザーのパスワード情報が必要になるため、「getUser」というPromise対応関数を作ります。JavaScriptの「Promise」については、以下のようなリンクを参照して下さい。乱暴に簡潔に述べるなら、非同期メソッドチェーンを簡単にする技です。

azu.github.io

続いて、ミドルウェアを追加するコードのあとに、チャットデータを受け取るとDominoにそのデータを送信するコードを追加します。

  /**
   * ソケットサーバーにミドルウェアを追加します。
   */
  io.use(function(socket, next)
  {
    // ソケットにセッション情報を追加します。
    var signedCookies = cookie.parse(socket.handshake.headers.cookie);
    var cookies = cookieParser.signedCookies(signedCookies, 'domtest2');
    return sessionStore.get(cookies['connect.sid'], function(error, session)
    {
      if (error != null)
        return next(error);
      if (session.passport == null || !session.passport.user == null)
        return next('Guest');
      socket.session = session;
      return next();
    });
  });

  /**
   * ソケットサーバーに接続要求が来たら処理します。
   */
  io.on('connect', function(socket)
  {
    console.log(socket.id);

    // ソケットクライアントに'chat'リスナーを追加します。
    socket.on('chat', function(data)
    {
      getUser(socket)
      .then(function(user)
      {
        var nodeChat = new NodeChat(user);
        return nodeChat.chat(data);
      })
      .then(function(user)
      {
        data.username = user.username;
        io.sockets.emit('chat', data);
      })
      .catch(function(err)
      {
        console.log(err);
      });
    });
  });
};

nodeChat.chatメソッドでDominoへのREST API送出に成功すると、チャットクライアントにメッセージを送信するようになります。Node.jsを介して、Socket.ioによるリアルタイム通信とDomino REST APIを使った情報のやり取りは、以上のようなコードで実現できます。

それでは、以下に前回のコードから変更になったソースを、順を追ってみていきます。

// auth.js

var passport = require('passport');
var LocalStrategy = require('passport-local').Strategy;

module.exports = function(User)
{

  // セッションサポート用の直列化、非直列化。

  passport.serializeUser(function(user, done)
  {
    done(null, { id: user._id, username: user.username });
  });
  passport.deserializeUser(function(user, done)
  {
    User.findById(user.id, function(err, user)
    {
      done(err, user);
    });
  });

  // ユーザ検証戦略の設定。
  passport.use(new LocalStrategy(
    function (username, password, done)
    {
      User.findOne({ username: username }, function(err, user)
      {
        if (err) { return  done(err); }
        if (!user) {
          return done(null, false, {message: 'ユーザID/パスワードが違います。'});
        }
        if (!user.validPassword(password))
        {
          return done(null, false, {message: 'ユーザID/パスワードが違います。'});
        }
        return done(null, user);
      })
    }
  ));

  return {
    passport: passport
  };
};

前回と比べ、Mongoose、MongoDBに関するパートを別のモジュールにし、認証に使う「User」のみ受け取れるようにしてあります。

そのMongoDBに関するモジュールは、dbinfo.jsとして新しくまとめました。

// dbinfo.js

module.exports = function(mongoose)
{
  // MongoDBに接続。
  var db = mongoose.createConnection(
    "mongodb://localhost/domtest"
    , function(error, res){}
  );

  // ユーザ情報のスキーマ。
  var UserSchema = new mongoose.Schema({
    username: {type: String, require: true}
    , password: {type: String, require: true}
  });

  // ユーザ情報のモデル。
  var User = db.model("User", UserSchema);

  // パスワード検証用メソッド。
  User.prototype.validPassword = function(pass)
  {
    return this.password === pass;
  };

  return {
    db: db
    , User: User
  };
};

Mongooseモジュールは、bin/wwwでロードしておき、mongooseを引数にしてdbinfoをロードするようにします。 その他のbin/wwwの変更点は以下の通りです。

// bin/www

...

/**
 * データベース環境を初期化します。
 */
 var mongoose = require('mongoose');
 var dbInfo = require('../dbInfo')(mongoose);

/**
 * 認証系を初期化します。
 */
var auth = require('../auth')(dbInfo.User);

/**
 * セッションの初期設定をします。
 * セッションの保存先にもMongoDBを使用します。
 */
var session = require('express-session');
var MongoStore = require('connect-mongo')(session);
var sessionStore = new MongoStore({
  mongooseConnection: dbInfo.db
});

...

// (中略)

...

/**
 * ソケットの初期設定をします。
 */
require('../mySocket')(server, sessionStore, dbInfo.User);

...

それと、クライアント用JSでも一点だけ変更があります。前回までのコードですと、送信済みのメッセージが残ったままになってしまうので、送信後にメッセージを消去するようにします。

// public/javascript/chat.js

(function()
{
  // クライアント側のソケットを初期化します。
  var socket = io.connect('http://localhost:3000');

  // 送信ボタンをクリックしたときの操作を定義します。
  $('button#send').click(function()
  {
    var msg = $('#message').val();
    console.log('Message', msg);
    socket.emit('chat', {
      message: msg
    });
    $('#message').val('');
  });

  // Enterキーにも対応させる。
  $('input#message').keypress(function(event)
  {
    if (event.which == 13)
      $('button#send').click();
  });

  // チャットメッセージを受信します。
  socket.on('chat', function(data)
  {
    $('#output').append(
      '<p>'
        + '<strong>' + data.username + '</strong>: '
      + data.message + '</p>'
    );
  });

})()

以上がコードの追加、変更部分でした。

では実際に、チャットデータがNSFに保存される様子を見てみましょう。コマンドプロンプトでdomtest2フォルダに移動し、npm startとすればサーバが起動します。Dominoサーバも忘れずに起動しておきます。ブラウザでhttp://localhost:3000にアクセスして、ログインします。

www.dropbox.com

チャットデータを保存することができたので、再ログインしたあとにチャットデータの履歴を表示することができます。次回はDominoからREST APIでデータを受け取り、チャットの履歴を表示してみましょう。