'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;