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 Channel = require('./channel.js');
const ChannelsEntity = require('./data/channels.js');

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

const AccessManager = require('twilio-common').AccessManager;

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|String} accessManager - The Client's AccessManager, or a Capability
 *   token JWT string.
 * @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 {String} identity - The unique identifier for the User of this Client.
 * @fires Client#channelAdded
 * @fires Client#channelInvited
 * @fires Client#channelJoined
 * @fires Client#channelLeft
 * @fires Client#channelRemoved
 * @fires Client#channelUpdated
 * @fires Client#tokenExpired
 */
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 (typeof accessManager === 'string') {
    accessManager = new AccessManager(accessManager);
  }

  if (!accessManager || !accessManager.token) {
    throw new Error('A valid Twilio AccessManager or Capability Token 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, token, config);
  let sessionPromise = session.initialize();

  let channels = new Map();
  let channelsEntity = new ChannelsEntity(session, 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'));

    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 },
    _token: { value: token, writable: true },
    _twilsock: { value: twilsock },
    accessManager: {
      enumerable: true,
      value: accessManager
    },
    channels: {
      enumerable: true,
      value: channels
    },
    identity: {
      enumerable: true,
      get: () => accessManager.identity
    }
  });

  this._initialize(options.typingTimeout || 5000);
  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);
/**
 * @param typingTimeout
 * @returns {*|Promise.<T>|Request}
 * @private
 */
Client.prototype._initialize = function _initialize(typingTimeout) {
  return this._sessionPromise.then(() => {
    this._notification.subscribe('twilio.ipmsg.typing_indicator', 'twilsock');
    this._notification.subscribe('twilio.channel.new_message', 'gcm');
    this._notification.subscribe('twilio.channel.added_to_channel', 'gcm');

    this._notification.on('message', (type, message) => {
      if (type === 'twilio.ipmsg.typing_indicator') {
        log.trace('Got new typing indicator push!');
        log.trace(message);

        this._channelsPromise.then((channels) => {
          return channels.channels.get(message.channel_sid);
        }).then((channel) => {
          for (var member of channel.members.values()) {
            if (member.identity === message.identity) {
              member._startTyping(typingTimeout);
              break;
            }
          }
        }).catch((err) => {
          log.error('IMPSG E: ', err);
          throw err;
        });
      }
    });
  });
};

/**
 * 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));
};

/**
 * 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(() => {
    for (var channel of this.channels.values()) {
      if (channel.uniqueName === uniqueName) {
        return channel;
      }
    }
  });
};

/**
 * 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 || { };

  let channel = new Channel(this._session, options, null);
  return this._channelsPromise.then((channelsEntity) => channelsEntity.addChannel(channel));
};

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 the supplied token expires.
 * @event Client#tokenExpired
 */

module.exports = Client;