Source: client.js

'use strict';

const EventEmitter = require('events').EventEmitter;
const inherits = require('util').inherits;
const log = require('loglevel');

const Configuration = require('./configuration');
const Session = require('./session.js');
const ChannelsEntity = require('./data/channels.js');

const UserInfos = require('./data/userinfos.js');
const TypingIndicator = require('./services/typingindicator');
const ConsumptionHorizon = require('./services/consumptionhorizon');

const DataSyncClient = require('../datasync/client');
const NotificationClient = require('../notification/client');
const TwilsockClient = require('../twilsock/client');
const Transport = require('../transport');

const SDK_VERSION = require('../../../package.json').version;

/**
 * @class
 * @classdesc A Client provides an interface for the local
 *   User to interact with Channels. The Client constructor will
 *   synchronously return an instance of Client, and will hold
 *   any outgoing methods until it has asynchronously finished
 *   syncing with the server.
 * @param {AccessManager} accessManager - The Client's AccessManager
 * @param {Client#ClientOptions} options - Options to customize the Client
 * @property {AccessManager} accessManager - The Client's AccessManager
 * @property {Map<sid, Channel>} channels - A Map containing all Channels known locally on
 *   the Client. To ensure the Channels have loaded before getting a response, use
 *   {@link Client#getChannels}.
 * @property {UserInfo} userInfo - User information for logged in user
 * @property {String} identity - Deprecated: User identity for logged in user
 * @fires Client#channelAdded
 * @fires Client#channelInvited
 * @fires Client#channelJoined
 * @fires Client#channelLeft
 * @fires Client#channelRemoved
 * @fires Client#channelUpdated
 * @fires Client#memberJoined
 * @fires Client#memberLeft
 * @fires Client#memberUpdated
 * @fires Client#messageAdded
 * @fires Client#messageRemoved
 * @fires Client#messageUpdated
 * @fires Client#tokenExpired
 * @fires Client#typingEnded
 * @fires Client#typingStarted
 * @fires Client#userInfoUpdated
 */
function Client(accessManager, options) {
  options = options || { };
  options.logLevel = options.logLevel || 'error';

  log.setDefaultLevel(options.logLevel);
  log.setLevel(options.logLevel);
  let config = new Configuration(options);

  if (!accessManager || !accessManager.token) {
    throw new Error('A valid Twilio AccessManager must be passed to IPMessaging Client');
  }

  let token = accessManager.token;

  let twilsock = options.twilsockClient || new TwilsockClient(token, options);
  let transport = options.transportClient || new Transport(twilsock, options);
  let notification = options.notificationClient || new NotificationClient(
        'ip_messaging', token, transport, twilsock, options);
  let datasync = options.dataSyncClient || new DataSyncClient(token, notification, transport, options);
  let session = new Session(datasync, transport, config);
  let sessionPromise = session.initialize(token);

  let userInfos = new UserInfos(session, datasync, accessManager.identity);
  userInfos.on('userInfoUpdated', this.emit.bind(this, 'userInfoUpdated'));

  let consumptionHorizon = new ConsumptionHorizon(config, session);
  let typingIndicator = new TypingIndicator(config
                                        , accessManager
                                        , transport
                                        , notification
                                        , this.getChannelBySid.bind(this));

  let channels = new Map();
  let channelsEntity = new ChannelsEntity({ session, userInfos, typingIndicator, consumptionHorizon }, channels );
  let channelsPromise = sessionPromise.then(() => {
    channelsEntity.on('channelAdded', this.emit.bind(this, 'channelAdded'));
    channelsEntity.on('channelRemoved', this.emit.bind(this, 'channelRemoved'));
    channelsEntity.on('channelInvited', this.emit.bind(this, 'channelInvited'));
    channelsEntity.on('channelJoined', this.emit.bind(this, 'channelJoined'));
    channelsEntity.on('channelLeft', this.emit.bind(this, 'channelLeft'));
    channelsEntity.on('channelUpdated', this.emit.bind(this, 'channelUpdated'));

    channelsEntity.on('memberJoined', this.emit.bind(this, 'memberJoined'));
    channelsEntity.on('memberLeft', this.emit.bind(this, 'memberLeft'));
    channelsEntity.on('memberUpdated', this.emit.bind(this, 'memberUpdated'));

    channelsEntity.on('messageAdded', this.emit.bind(this, 'messageAdded'));
    channelsEntity.on('messageUpdated', this.emit.bind(this, 'messageUpdated'));
    channelsEntity.on('messageRemoved', this.emit.bind(this, 'messageRemoved'));

    channelsEntity.on('typingStarted', this.emit.bind(this, 'typingStarted'));
    channelsEntity.on('typingEnded', this.emit.bind(this, 'typingEnded'));

    return channelsEntity.fetchChannels(session);
  });

  accessManager.on('tokenExpired', () => this.emit('tokenExpired', accessManager));
  accessManager.on('tokenUpdated', () => this._updateToken(accessManager.token));

  Object.defineProperties(this, {
    _channelsPromise: { value: channelsPromise },
    _datasync: { value: datasync },
    _notification: { value: notification },
    _session: { value: session },
    _sessionPromise: { value: sessionPromise },
    _initializePromise: { value: null, writable: true },
    _token: { value: token, writable: true },
    _twilsock: { value: twilsock },
    _typingIndicator: { value: typingIndicator },
    _userInfos: { value: userInfos },
    _userInfo: { writable: true },
    accessManager: {
      enumerable: true,
      value: accessManager
    },
    channels: {
      enumerable: true,
      value: channels
    },
    identity: {
      enumerable: true,
      get: () => accessManager.identity
    },
    userInfo: {
      enumerable: true,
      get: () => this._userInfos.myUserInfo
    }
  });

  this._initializePromise = this._initialize();
  EventEmitter.call(this);
}

/**
 * Current version of this IP Messaging Client.
 * @name Client#version
 * @type String
 * @readonly
 */
Object.defineProperties(Client, {
  version: {
    enumerable: true,
    value: SDK_VERSION
  }
});

inherits(Client, EventEmitter);

/**
 * @returns {Promise.<T>|Request}
 * @private
 */
Client.prototype._initialize = function _initialize() {
  return this._sessionPromise.then(() => {
      this._notification.subscribe('twilio.channel.new_message', 'gcm');
      this._notification.subscribe('twilio.channel.added_to_channel', 'gcm');
    })
    .then(this._typingIndicator.initialize());
};

/**
 * Initializes library
 * Library will be eventually initialized even without this method called,
 * but client can use returned promise to track library initialization state.
 * It's safe to call this method multiple times. It won't reinitialize library in ready state.
 *
 * @public
 * @returns {Promise<Client>}
 */
Client.prototype.initialize = function initialize() {
  return this._initializePromise.then(() => this);
};

/**
 * Update the token used by the Client and re-register with IP Messaging services.
 * @param {String} token - The JWT string of the new token.
 * @private
 * @returns {Promise<Client>}
 */
Client.prototype._updateToken = function _updateToken(token) {
  if (!token || token.split('.').length !== 3) {
    return log.error('Received a malformed token from AccessManager. \
      Token not updated in IP MessagingClient.');
  }

  if (token === this._token) {
    return Promise.resolve(this);
  }

  this._token = token;
  log.info('IPMSG I: authTokenUpdated');

  return Promise.all([
    this._twilsock.setAuthToken(token),
    this._notification.setAuthToken(token),
    this._datasync.setAuthToken(token),
    this._sessionPromise.then(() => this._session.updateToken(token))
  ]).then(()=> this);
};

/**
 * Get a Channel by its SID.
 * @param {String} channelSid - The sid of the Channel to get.
 * @returns {Promise<Channel>}
 */
Client.prototype.getChannelBySid = function getChannelBySid(channelSid) {
  if (!channelSid || typeof channelSid !== 'string') {
    throw new Error('Client.getChannelBySid requires a <String>channelSid parameter');
  }

  return this._channelsPromise.then(() => this.channels.get(channelSid) || null);
};

/**
 * Get a Channel by its unique identifier name.
 * @param {String} uniqueName - The unique identifier name of the Channel to get.
 * @returns {Promise<Channel>}
 */
Client.prototype.getChannelByUniqueName = function getChannelByUniqueName(uniqueName) {
  if (!uniqueName || typeof uniqueName !== 'string') {
    throw new Error('Client.getChannelByUniqueName requires a <String>uniqueName parameter');
  }

  return this._channelsPromise.then(() => {
    let foundChannel = null;
    this.channels.forEach(channel => {
      if (!foundChannel && (channel.uniqueName === uniqueName)) {
        foundChannel = channel;
      }
    });

    return foundChannel;
  });
};

/**
 * Get the current list of all Channels the Client knows about.
 * @returns {Promise<Array<Channel>>}
 */
Client.prototype.getChannels = function getChannels() {
  return this._channelsPromise.then(() => {
    let channels = [];
    this.channels.forEach((channel) => channels.push(channel));
    return channels;
  });
};

/**
 * Create a channel on the server.
 * @param {Client#CreateChannelOptions} [options] - Options for the Channel
 * @returns {Promise<Channel>}
 */
Client.prototype.createChannel = function createChannel(options) {
  options = options || { };
  return this._channelsPromise.then((channelsEntity) => channelsEntity.addChannel(options));
};

Object.freeze(Client);

/**
 * These options can be passed to Client.createChannel
 * @typedef {Object} Client#CreateChannelOptions
 * @property {Object} [attributes] - Any custom attributes to attach to the Channel.
 * @property {Boolean} [isPrivate] - Whether or not this Channel should be visible
 *  to uninvited Clients.
 * @property {String} [friendlyName] - The non-unique display name of the Channel.
 * @property {String} [uniqueName] - The unique identity name of the Channel.
 */

/**
 * These options can be passed to Client constructor
 * @typedef {Object} Client#ClientOptions
 * @property {String} [logLevel='error'] - The level of logging to enable. Valid options
 *   (from strictest to broadest): ['silent', 'error', 'warn', 'info', 'debug', 'trace']
 * @property {String} [wsServer] - A custom websocket server to connect to.
 */

/**
 * Fired when a Channel becomes visible to the Client.
 * @param {Channel} channel
 * @event Client#channelAdded
 */
/**
 * Fired when the Client is invited to a Channel.
 * @param {Channel} channel
 * @event Client#channelInvited
 */
/**
 * Fired when the Client joins a Channel.
 * @param {Channel} channel
 * @event Client#channelJoined
 */
/**
 * Fired when the Client leaves a Channel.
 * @param {Channel} channel
 * @event Client#channelLeft
 */
/**
 * Fired when a Channel is no longer visible to the Client.
 * @param {Channel} channel
 * @event Client#channelRemoved
 */
/**
 * Fired when a Channel's attributes or metadata have been updated.
 * @param {Channel} channel
 * @event Client#channelUpdated
 */
/**
 * Fired when a Member has joined the Channel.
 * @param {Member} member
 * @event Client#memberJoined
 */
/**
 * Fired when a Member has left the Channel.
 * @param {Member} member
 * @event Client#memberLeft
 */
/**
 * Fired when a Member's fields has been updated.
 * @param {Member} member
 * @event Client#memberUpdated
 */
/**
 * Fired when a new Message has been added to the Channel on the server.
 * @param {Message} message
 * @event Client#messageAdded
 */
/**
 * Fired when Message is removed from Channel's message list.
 * @param {Message} message
 * @event Client#messageRemoved
 */
/**
 * Fired when an existing Message's fields are updated with new values.
 * @param {Message} message
 * @event Client#messageUpdated
 */
/**
 * Fired when the supplied token expires.
 * @event Client#tokenExpired
 */
/**
 * Fired when a member has stopped typing.
 * @param {Member} member
 * @event Client#typingEnded
 */
/**
 * Fired when a member has begun typing.
 * @param {Member} member
 * @event Client#typingStarted
 */
/**
 * Fired when a userInfo has been updated.
 * @param {UserInfo} UserInfo
 * @event Client#userInfoUpdated
 */

module.exports = Client;