Backend chat service

So far, we've only scratched the surface of our backend application. We are going to add a service layer to our server. This abstraction layer will implement all the business logic, such as instant messaging. The service layer will handle interaction with other application modules and layers.

As for the WebSockets part of the application, we are going to use socketIO, which is a real-time communication engine. They have a really neat chat application example. If you haven't heard of it, you can take a look at the following link:

http://socket.io/get-started/chat/

Chat service implementation

Now that we are familiar with socketIO, we can continue and implement our chat service. We are going to start by creating a new file called app/services/chat/index.js. This will be the main file for our chat service. Add the following code:

'use strict';

const socketIO = require('socket.io');
const InstantMessagingModule = require('./instant-messaging.module');

module.exports = build;

class ChatService {
}

function build(app, server) {
  return new ChatService(app, server);
}

Don't worry about the InstantMessagingModule. We just added it as a reference so that we'll not forget about it. We'll come back later to reveal the mystery. Our class should have a constructor. Let's add that now:

  constructor(app, server) {
    this.connectedClients = {};
    this.io = socketIO(server);
    this.sessionMiddleware = app.get('sessionMiddleware');
    this.initMiddlewares();
    this.bindHandlers();
  }

In the constructor, we initialize socketIO, get the session middleware, and finally bind all the handlers to our socketIO instance. More information about the session middleware can be found in our Express configuration file, config/express.js. Look for something similar:

  var sessionOpts = {
    secret: config.session.secret,
    key: 'skey.sid',
    resave: config.session.resave,
    saveUninitialized: config.session.saveUninitialized
  };

  if (config.session.type === 'mongodb') {
    sessionOpts.store = new MongoStore({
      url: config.mongodb.uri
    });
  }

  var sessionMiddleware = session(sessionOpts);
  app.set('sessionMiddleware', sessionMiddleware);

The nice thing is that we can share this session logic with socketIO and mount it with the .use() method. This will be done in the .initMiddlewares() method:

  initMiddlewares() {
    this.io.use((socket, next) => {
      this.sessionMiddleware(socket.request, socket.request.res, next);
    });

    this.io.use((socket, next) => {
      let user = socket.request.session.passport.user;

      //  authorize user
      if (!user) {
        let err = new Error('Unauthorized');
        err.type = 'unauthorized';
        return next(err);
      }

      // attach user to the socket, like req.user
      socket.user = {
        _id: socket.request.session.passport.user
      };
      next();
    });
  }

First, we mount the session middleware to our instance, which will do something similar to mounting it on our Express app. Second, we check whether the user is present on the socket's session, in other words, whether the user is authenticated or not.

Being able to add middleware is a pretty neat feature and enables us to do interesting things for each connected socket. We should also add the last method from the constructor:

  bindHandlers() {
    this.io.on('connection', socket => {
      // add client to the socket list to get the session later
      this.connectedClients[socket.request.session.passport.user] = socket;
      InstantMessagingModule.init(socket, this.connectedClients, this.io);
    });
  }

For each successfully connected client, we are going to initialize the instant messaging module and store the connected clients in a map, for later reference.

Instant messaging module

To be a little bit modular, we'll split functionalities that represent connected clients into separate modules. For now there will be only one module, but in the future, you can easily add new ones. The InstantMessagingModule will be found in the same folder with the main chat file, more precisely, app/services/chat/instant-messaging.module.js. You can safely add the following code to it:

'use strict';

const mongoose = require('mongoose');
const Message = mongoose.model('Message');
const Thread = mongoose.model('Thread');

module.exports.init = initInstantMessagingModule;

class InstantMessagingModule {
}

function initInstantMessagingModule(socket, clients) {
  return new InstantMessagingModule(socket, clients);
}

The service will use the Message and Thread models to validate and persist data. We are exporting an initialization function instead of the entire class. You could easily add extra initialization logic to the exported function.

The class constructor will be fairly simple, and it will look something similar to this:

  constructor(socket, clients) {
    this.socket = socket;
    this.clients = clients;
    this.threads = {};
    this.bindHandlers();
  }

We just assign the necessary dependencies to each property, and bind all the handlers to the connected socket. Let's continue with the .bindHandlers() method:

  bindHandlers() {
    this.socket.on('send:im', data => {
      data.sender = this.socket.user._id;

      if (!data.thread) {
        let err = new Error('You must be participating in a conversation.')
        err.type = 'no_active_thread';
        return this.handleError(err);
      }

      this.storeIM(data, (err, message, thread) => {
        if (err) {
          return this.handleError(err);
        }

        this.socket.emit('send:im:success', message);

        this.deliverIM(message, thread);
      });
    });
  }

When sending a new message through WebSockets, it will be stored using the .storeIM() method and delivered to each participant by the .deliverIM() method.

We slightly abstracted the logic to send instant messages, so let's define our first method, which stores the messages:

  storeIM(data, callback) {
    this.findThreadById(data.thread, (err, thread) => {
      if (err) {
        return callback(err);
      }

      let user = thread.participants.find((participant) => {
        return participant.toString() === data.sender.toString();
      });

      if (!user) {
        let err = new Error('Not a participant.')
        err.type = 'unauthorized_thread';
        return callback(err);
      }

      this.createMessage(data, (err, message) => {
        if (err) {
          return callback(err);
        }

        callback(err, message, thread);
      });
    });
  }

So basically, the .storeIM() method finds the conversation thread and creates a new message. We have also added a simple authorization when storing a message. The sender must be a participant in the given conversation. You could move that piece of logic into a more suitable module. I'll leave it to you as practice.

Let's add the next two methods that we used before:

  findThreadById(id, callback) {
    if (this.threads[id]) {
      return callback(null, this.threads[id]);
    }

    Thread.findById(id, (err, thread) => {
      if (err) {
        return callback(err);
      }

      this.threads[id] = thread;
      callback(null, thread);
    });
  }

  createMessage(data, callback) {
    Message.create(data, (err, newMessage) => {
      if (err) {
        return callback(err);
      }

      newMessage.populate('sender', callback);
    });
  }

Finally, we can deliver our message to the rest of the participants. The implementation can be found in the following class method:

  deliverIM(message, thread) {
    for (let i = 0; i < thread.participants.length; i++) {
      if (thread.participants[i].toString() === message.sender.toString()) {
        continue;
      }

      if (this.clients[thread.participants[i]]) {
        this.clients[thread.participants[i]].emit('receive:im', message);
      }
    }
  }

We have reached the end with our backend application. It should have all the necessary features implemented to start working on the client Angular application.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.143.3.208