src/session/Session.js
import {version} from '../../version';
import Logs from '../Logs';
import {CallManager} from '../calls/CallManager';
import {ChatManager} from '../chat/ChatManager';
import {ConferenceManager} from '../conferences/ConferenceManager';
import {ContactRepository} from '../contacts-new/ContactRepository';
import {ContactManager} from '../contacts/ContactManager';
import {PresenceManager} from '../contacts/PresenceManager';
import {DataChannelDataPipe} from '../datapipe/DataChannelDataPipe';
import {DataPipeFactory} from '../datapipe/DataPipeFactory';
import {DataPipeType} from '../datapipe/DataPipeType';
import {WacDataPipe} from '../datapipe/WacDataPipe';
import {EventEmitter} from '../eventemitter';
import {FileManager} from '../file-sharing/FileManager';
import {GroupManager} from '../groups/GroupManager';
import {InstantMessage} from '../instant-messages/InstantMessage';
import {InstantMessageType} from '../instant-messages/InstantMessageType';
import {MeetingManager} from '../meetings/MeetingManager';
import Selfie from '../misc/Selfie';
import {PermissionManager} from '../permissions/PermissionManager';
import {Presence} from '../users-new/Presence';
import {UserRepository} from '../users-new/UserRepository';
import {UserManager} from '../users/UserManager';
import {PromiseQueue} from '../utils/PromiseQueue';
import {bindMethods} from '../utils/bindMethods';
import {VoiceMailManager} from '../voice-mail/VoiceMailManager';
import {WacProxy} from '../wac-proxy/WacProxy';
import {
AlreadyLoggedError,
LoginError,
StackRegisterError,
StackUnregisterError,
WacInternalError,
} from '../wac-proxy/errors';
import {Whiteboard} from '../whiteboard/Whiteboard';
import {RemoteSession} from './RemoteSession';
import {SessionError} from './SessionError';
import {SessionStatus} from './SessionStatus';
import {User} from './User';
const log = Logs.instance.getLogger('SippoJS/Session');
/**
* @typedef {Object} StatusEvent
* @property {SessionStatus} status The new status of the session
* @property {Session} target The session object
*/
/**
* Session objects manage communications for a given user identity.
* They are obtained by calling the {@link Sippo#createSession} method of the
* {@link Sippo} object.
*
* # Events
*
* - `status` {@link StatusEvent} - Triggered every time the status of the session changes.
* - `incomingInstantMessage` {@link InstatMessage} -
* Triggered when an incoming instant message is received during an active session
* - `incomingDataPipe` {@link Datapipe} -
* Triggered when an incoming data pipe is received during an active session
* - `incomingWhiteboard` {@link Whiteboard} -
* - `incomingSelfie` {@link Selfie} - Triggered when an Selfie request is received
* - `qs-connection-lost` - Triggered when the socket.io ping times out (deprecated)
* - `qs-transport-close` - Triggered when the socket is terminated (deprecated)
* - `qs-unknown-close` - Triggered when some other reason closed the socket (deprecated)
*/
export class Session extends EventEmitter {
/**
* Creates a new Session
* @param {string} issuer The authorization token issuer identifier
* @param {string} token The authorization token from the given issuer.
* @param {Object} config Session configuration
* @param {string} config.wacUri WAC URI
* @param {string} config.applicationToken A token for the application
* @param {string} config.deviceId string which identifies the device this session belongs to
* @protected
*/
static withOAuth2(issuer, token, config) {
const sessionEstablisher = (wacProxy, deviceId) => wacProxy.loginToken(issuer, token, deviceId);
return new Session(sessionEstablisher, config);
}
/**
* Creates a new Session
* @param {string} token The authorization token from the given issuer
* @param {Object} config Session configuration
* @param {string} config.wacUri WAC URI
* @param {string} config.applicationToken A token for the application
* @param {string} config.deviceId string which identifies the device this session belongs to
* @protected
*/
static withSessionToken(token, config) {
const sessionEstablisher = (wacProxy, deviceId) => wacProxy.recoverSession(token, deviceId);
return new Session(sessionEstablisher, config);
}
/**
* Creates a new Session for anonymous users without an authorization token.
* @param {string} issuer The authorization token issuer identifier
* @param {Object} config Session configuration
* @param {string} config.wacUri WAC URI
* @param {string} config.applicationToken A token for the application
* @param {string} config.deviceId string which identifies the device this session belongs to
* @protected
*/
static withoutToken(issuer, config) {
const sessionEstablisher = (wacProxy, deviceId) => wacProxy.loginToken(issuer, undefined, deviceId);
return new Session(sessionEstablisher, config);
}
/** @private */
constructor(sessionEstablisher, config) {
super();
/** @private */
this.status = SessionStatus.DISCONNECTED;
/** @private */
this.meetingManager = null;
/** @private */
this.session = null;
/** @private */
this.credential = null;
/** @private */
this.wacUri = config.wacUri;
/** @private */
this.applicationToken = config.applicationToken;
/** @private */
this.deviceId = config.deviceId;
/** @private */
this.sessionEstablisher = sessionEstablisher;
this._promiseQueue = new PromiseQueue();
this._boundEvents = false;
/** @private */
this.wacProxy = new WacProxy();
this.wacProxy.initialize(this.wacUri);
/**
* @private
* @deprecated
*/
this.userManager = new UserManager(this.wacProxy);
/** @private */
this.voiceMailManager = new VoiceMailManager(this.wacProxy);
/** @private */
this.setStatus(SessionStatus.UNCONNECTED);
bindMethods(this, [
'onSessionDisconnected',
'onSessionReconnecting',
'onSessionReconnected',
'onDataPipeInvite',
'onRinging',
'onInstantMessage',
'onTransportClose',
'onConnectionLost',
'onUnknownClose',
]);
}
/**
* Sets the new status on the session
*
* @private
* @param {SessionStatus} status New status
* @return {Session} The session object
*/
setStatus(status) {
if (this.status === status) {
return this;
}
log.info(`session status changed from ${SessionStatus[this.status]} to ${SessionStatus[status]}`);
this.status = status;
this.emit('status', {
status: this.status,
target: this,
});
return this;
}
/**
* Handles an incoming data pipe and emits the corresponding events
*
* @private
* @param {DataPipe} dataPipe A data pipe
* @return {undefined}
*/
handleDataPipe(dataPipe) {
switch (dataPipe.label) {
case Whiteboard.PROTOCOL:
this.emit('incomingWhiteboard', new Whiteboard(dataPipe));
break;
case Selfie.PROTOCOL: {
const selfie = new Selfie();
selfie._initDatapipe(dataPipe).then(() => {
this.emit('incomingSelfie', selfie);
}).catch(log.error);
break;
}
default:
dataPipe.connect().then(() =>
this.emit('incomingDataPipe', dataPipe),
).catch(log.error);
}
}
/** @private */
onDataPipeInvite(event) {
const dataPipe = new WacDataPipe(event.id, DataPipeType.INCOMING, this.wacProxy, event.participants, event.label);
this.handleDataPipe(dataPipe);
}
/** @private */
onRinging(event) {
if (event.media && event.media.data) {
const pipe = new DataChannelDataPipe(event.callid, DataPipeType.INCOMING, this.wacProxy, [this.getGatewayUsername(), event.userid]);
return this.handleDataPipe(pipe);
}
}
/** @private */
onInstantMessage(event) {
this.emit('incomingInstantMessage', new InstantMessage(
InstantMessageType.INCOMING,
this.wacProxy,
event.userid,
this.getGatewayUsername(),
event.text,
));
}
/** @private */
bindEventHandlers() {
if (this._boundEvents) {
return;
}
this.wacProxy.on('disconnected', this.onSessionDisconnected);
this.wacProxy.on('qs-reconnecting', this.onSessionReconnecting);
this.wacProxy.on('qs-reconnected', this.onSessionReconnected);
this.wacProxy.getDatapipeService().emitter.on('invite', this.onDataPipeInvite);
this.wacProxy.on('qs-ringing', this.onRinging);
this.wacProxy.on('qs-instant-message', this.onInstantMessage);
this.wacProxy.on('qs-transport-close', this.onTransportClose);
this.wacProxy.on('qs-connection-lost', this.onConnectionLost);
this.wacProxy.on('qs-unknown-close', this.onUnknownClose);
this._boundEvents = true;
}
/** @private */
unbindEventHandlers() {
if (!this._boundEvents) {
return;
}
this.wacProxy.off('disconnected', this.onSessionDisconnected);
this.wacProxy.off('qs-reconnecting', this.onSessionReconnecting);
this.wacProxy.off('qs-reconnected', this.onSessionReconnected);
this.wacProxy.getDatapipeService().emitter.off('invite', this.onDataPipeInvite);
this.wacProxy.off('qs-ringing', this.onRinging);
this.wacProxy.off('qs-instant-message', this.onInstantMessage);
this.wacProxy.off('qs-transport-close', this.onTransportClose);
this.wacProxy.off('qs-connection-lost', this.onConnectionLost);
this.wacProxy.off('qs-unknown-close', this.onUnknownClose);
this._boundEvents = false;
}
/** @private */
onSessionDisconnected() {
log.warn('[SippoJS] Session disconnected');
this.setStatus(SessionStatus.DISCONNECTED);
}
/** @private */
onSessionReconnecting() {
this.setStatus(SessionStatus.CONNECTING);
}
/** @private */
onSessionReconnected() {
this.setStatus(SessionStatus.CONNECTED);
}
/** @private */
onTransportClose() {
log.warn('WAPI transport closed');
this.emit('qs-transport-close');
}
/** @private */
onUnknownClose() {
log.warn('WAPI closed with unknown reason');
this.emit('qs-unknown-close');
}
/** @private */
onConnectionLost() {
log.warn('WAPI connection lost');
this.emit('qs-connection-lost');
}
/**
* Activates the communications session with a gateway server.
*
* @return {Promise<Session>} Resolved after the session is connected or rejected if an error
* occurs during connection
*/
async connect() {
this.setStatus(SessionStatus.CONNECTING);
try {
await this.wacProxy.start();
const res = await this.sessionEstablisher(this.wacProxy, this.deviceId);
this.userManager.init();
this.session = res.session;
if (!this.deviceId) {
this.deviceId = this.session.context.deviceId;
}
this.wacProxy.retainSession(this.session);
const credentials = res.credential ? [res.credential] : await this.wacProxy.getCredentials({type: 'ims', user: this.session.user});
if (!credentials.length || !credentials[0].data) {
log.warn('No credentials for this user. No stack will be used. Only wac');
} else {
this.wacProxy.retainCredential(credentials[0]);
const credential = credentials[0].data;
credential.wacUri = this.wacUri;
credential.token = this.session.token;
credential.applicationToken = this.applicationToken;
await this.wacProxy.register(credential, {deviceId: this.deviceId});
this.credential = credential;
}
this.bindEventHandlers();
/** @private */
this.user = User.newInstance(this.wacProxy);
await this.user.init();
this.setStatus(SessionStatus.CONNECTED);
return this;
} catch (error) {
try {
await this.disconnect();
} catch (e) {
log.error('Error disconnecting', e);
}
if (error instanceof LoginError) {
throw new SessionError('UNAUTHORIZED');
}
if (error instanceof WacInternalError) {
throw new SessionError('WAC_INTERNAL_ERROR');
}
if (error instanceof StackRegisterError) {
throw new SessionError('STACK_REGISTER_ERROR');
}
if (error instanceof AlreadyLoggedError) {
throw new SessionError('ALREADY_LOGGED');
}
throw error;
}
}
/**
* Returns the username in the gateway of the user owning this session
* @return {String} The users's gateway username
*/
getGatewayUsername() {
return this.credential.username;
}
/**
* Get user's alias
* @return {String} User's alias
*/
getAlias() {
return this.credential.alias;
}
/**
* Get session token
* @return {String}
*/
getSessionToken() {
return this.session.token;
}
/**
* Creates a new instance of a Whiteboard
* @param {String[]} participants gateway usernames of the desired participants
* @return {Promise{Whiteboard}}
*/
createWhiteboard(participants) {
return new Whiteboard(this.createDataPipe(participants, Whiteboard.PROTOCOL)).init();
}
/**
* Creates a new instance of a Selfie
* @param {String} to gateway username of the selfie recipient.
* @return {Promise}
*/
createSelfie(to) {
const selfie = new Selfie();
return selfie._initDatapipe(this.createDataPipe([to], Selfie.PROTOCOL)).then(() => selfie);
}
/**
* Creates a new data pipe instance for data exchange with the specified recipient.
* @throws {TypeError}
* @param {String[]} participants gateway usernames of the desired participants
* @param {String} label a shared pipe identifier.
* @return {DataPipe}
*/
createDataPipe(participants, label) {
if (!(participants instanceof Array)) {
throw new TypeError(`${participants} is not an Array`);
}
if (!participants.includes(this.getGatewayUsername())) {
participants.push(this.getGatewayUsername());
}
return DataPipeFactory.create(undefined, DataPipeType.OUTGOING, this.wacProxy, participants, label);
}
/**
* Create a standalone message to the given recipient
* @param {String} to User identifier of the recipient
* @param {String} text Message's body
* @return {InstantMessage}
*/
createInstantMessage(to, text) {
return new InstantMessage(this.wacProxy, InstantMessageType.OUTGOING, this.getGatewayUsername(), to, text);
}
/**
* Retrieves a call manager instance
* @return {CallManager}
*/
getCallManager() {
if (!this.callManager) {
/** @private */
this.callManager = CallManager.newInstance(this.wacProxy, this.userManager, this.credential.username);
}
return this.callManager;
}
/**
* Retrieves a conference manager instance
* @return {ConferenceManager}
*/
getConferenceManager() {
if (!this.conferenceManager) {
/** @private */
this.conferenceManager = ConferenceManager.newInstance(this.wacProxy, this.userManager, this.getPermissionManager());
}
return this.conferenceManager;
}
/**
* Retrieve a UserManager object
* @deprecated Use Session.getUserRepository() instead
* @return {UserManager}
*/
getUserManager() {
log.warn('Session.getUserManager() is deprecated. The new Session.getUserRepository() should be used instead.');
return this.userManager;
}
/**
* Returns the user repository.
* @return {UserRepository}
*/
getUserRepository() {
if (this.userRepository === undefined) {
/** @private */
this.userRepository = new UserRepository(this.wacProxy.getPresenceService(), this.wacProxy.getUserService());
}
return this.userRepository;
}
getPresence() {
if (this.presence === undefined) {
const ownId = `wac-user:${this.wacProxy.getCurrentSession().userObj.id}`;
/** @private */
this.presence = new Presence(this.wacProxy.getPresenceService(), ownId);
}
return this.presence;
}
/**
* Retrieves the chat manager
* @return {Promise<ChatManager>}
*/
async getChatManager() {
if (!this.chatManager) {
const chatService = this.wacProxy.getChatService();
const fileManager = await this.getFileManager();
const user = this.getUser();
/** @private */
this.chatManager = ChatManager.newInstance(chatService, fileManager, user, this.getSessionToken());
}
return this.chatManager;
}
/**
* Retrieves the file manager
* @return {Promise<FileManager>}
*/
async getFileManager() {
if (!this.fileManager) {
const fileSharingService = this.wacProxy.getFileSharingService();
/** @private */
this.fileManager = FileManager.newInstance(fileSharingService);
}
return this.fileManager;
}
/**
* Retrieves the contact manager.
* @deprecated Use Session.getContactRepository() instead
* @return {ContactManager}
*/
getContactManager() {
log.warn('Session.getContactManager() is deprecated. The new Session.getContactRepository() should be used instead.');
if (this.contactManager === undefined) {
/** @private */
this.contactManager = new ContactManager(this.wacProxy, this.userManager, this.getPresenceManager());
}
return this.contactManager;
}
/**
* Returns the contact repository.
* @return {ContactRepository}
*/
getContactRepository() {
if (this.contactRepository === undefined) {
/** @private */
this.contactRepository = new ContactRepository(this.wacProxy.getContactService(), this.getUserRepository());
}
return this.contactRepository;
}
/**
* Get a list of remote sessions where user is currently logged in
* @return {Promise<Array<RemoteSession>>}
*/
getRemoteSessions() {
return Promise.resolve().then(() => {
if (this.getCapabilities().includes('list-remote')) {
return this.wacProxy.getRemoteSessions();
}
return this.wacProxy.getSessions({to: -1}).then(sessions => sessions.filter(x => x.id !== this.session.id).map((x) => {
x.name = 'wac';
return x;
}));
}).then(sessions => sessions.map(session => new RemoteSession(session.id, session.name)));
}
/**
* Retrieve the User
* @return {User}
*/
getUser() {
return this.user;
}
/**
* Retrieves a {Logger} instance to log events in the WAC.
* @param {string} name The name of the logger
* @return {Logger}
*/
getLogger(name) {
return Logs.instance.getLogger(name);
}
/**
* Retrieves the meeting manager.
* @return {MeetingManager}
*/
getMeetingManager() {
if (this.meetingManager === null) {
this.meetingManager = new MeetingManager(this.wacProxy.getMeetingService());
this.meetingManager.init();
}
return this.meetingManager;
}
/**
* Retrieves the permissions manager.
* @returns {PermissionManager}
*/
getPermissionManager() {
if (!this.permissionsManager) {
/** @private */
this.permissionsManager = new PermissionManager(this.wacProxy.getPermissionService());
this.permissionsManager.init();
}
return this.permissionsManager;
}
/**
* Retrieves the group manager.
* @return {Promise<GroupManager>}
*/
getGroupManager() {
if (this.groupManager === undefined && this.getCapabilities().includes('user-group')) {
const groupService = this.wacProxy.getGroupService();
const phonebookService = this.wacProxy.getPhonebookService();
const userRepository = this.getUserRepository();
/** @private */
this.groupManager = GroupManager.newInstance(groupService, phonebookService, userRepository);
}
return this.groupManager;
}
/**
* Retrieves the presence manager.
* @deprecated
* @return {PresenceManager}
*/
getPresenceManager() {
log.warn('Session.getPresenceManager() is deprecated. Session.getPresence() or Session.getContactRepository() or Session.getUserRepository() should be used instead.');
if (!this.presenceManager) {
const presenceService = this.wacProxy.getOldPresenceService();
/** @private */
this.presenceManager = PresenceManager.of(presenceService);
}
return this.presenceManager;
}
/**
* Retrieves the voicemail manager
* @returns {VoiceMailMananger}
*/
getVoiceMailManager() {
return this.voiceMailManager;
}
/**
* Return stack-reported network connectivity tests results
* @param {string} [proto='udp'] - The transport protocol to test connectivity for, one of 'udp', 'tcp'.
* @return {Promise}
*/
checkConnection(proto = 'udp') {
if (!this.getCapabilities().includes('network-reporting')) {
throw Error('Network reporting is not supported');
}
return this.wacProxy.checkConnection(proto);
}
/**
* Retrieves and array with version information.
* @return {Object[]}
*/
getVersion() {
return [{name: 'SippoJS', version}].concat(this.wacProxy.getVersion());
}
/**
* Retrieves an array with the underlying capabilities
* @return {ImmutableSet<string>}
*/
getCapabilities() {
return this.wacProxy.getCapabilities();
}
/**
* Retrieves a promise which is resolved with the settings array.
* @return {Promise} This promise is resolved with a settings array
*/
settings() {
return this.wacProxy.getSettings();
}
/**
* @param {String} key Key to modified
* @param {String} value New value
* @return {Session}
*/
setSetting(key, value) {
this.wacProxy.setSetting(key, value);
return this;
}
/** @private */
async initManagers() {
const tasks = [];
if (this.conferenceManager) {
tasks.push(this.conferenceManager.getCurrentInvitations());
}
if (this.meetingManager) {
tasks.push(this.meetingManager.init());
}
if (this.groupManager) {
const groupManager = await this.groupManager;
tasks.push(groupManager.init());
}
if (this.chatManager) {
const chatManager = await this.chatManager;
tasks.push(chatManager.init());
}
if (this.permissionsManager) {
tasks.push(this.permissionsManager.init());
}
await Promise.all(tasks);
}
/** @private */
async uninitManagers(suspend = false) {
const tasks = [];
if (this.meetingManager) {
tasks.push(this.meetingManager.uninit());
}
if (this.groupManager) {
const groupManager = await this.groupManager;
tasks.push(groupManager.uninit());
}
if (this.chatManager) {
const chatManager = await this.chatManager;
tasks.push(suspend ? chatManager.suspend() : chatManager.uninit());
}
await Promise.all(tasks);
}
/**
* Ends a connected session.
* @return {Promise} A promise that is fulfilled when the session is disconnected
*/
async disconnect() {
if (this.status === SessionStatus.DISCONNECTED) {
return Promise.reject(Error('Session is already disconnected'));
}
this.unbindEventHandlers();
try {
await this.uninitManagers();
await this.wacProxy.stop();
} catch (error) {
if (error instanceof StackUnregisterError) {
throw new SessionError('STACK_UNREGISTER_ERROR');
}
throw error;
} finally {
this.setStatus(SessionStatus.DISCONNECTED);
}
}
async _suspend() {
if (this.status !== SessionStatus.CONNECTED) {
return false;
}
await this.uninitManagers(true);
if (this.permissionsManager.canUseVoiceMail()) {
await this.voiceMailService.unsubscribe();
}
await this.wacProxy.suspendSession();
this.setStatus(SessionStatus.SUSPENDED);
return true;
}
async _resume() {
if (this.status !== SessionStatus.SUSPENDED) {
return false;
}
await this.wacProxy.resumeSession();
await this.initManagers();
if (this.permissionsManager.canUseVoiceMail()) {
await this.voiceMailService.subscribe();
}
this.setStatus(SessionStatus.CONNECTED);
return true;
}
/**
* Suspend current session: WAC will not send or receive any message
* @return {Promise} Resolved when the session is suspended
*/
suspend() {
return this._promiseQueue.queue(this._suspend.bind(this), 'suspend');
}
/**
* Resume current session, restoring WAC communication
* @return {Promise} Resolved when the session is resumed
*/
resume() {
return this._promiseQueue.queue(this._resume.bind(this), 'resume');
}
/**
* Retrieves the current status of this session.
* @return {SessionStatus}
*/
getStatus() {
return this.status;
}
/**
* Notifies a push token to the WAC
* @param {string} tokenType type of the token. Supported values are "apn" and "firebase"
* @param {string} token token intended to be used to send notifications
* @return {Promise}
*/
notifyPushToken(tokenType, token, ...rest) {
if (rest.length === 1) {
log.warn('Only two arguments expected: Current method signature is "session.notifyPushToken(tokenType, token)"');
tokenType = token;
token = rest[0];
}
return this.wacProxy.getPushNotificationsService().notifyToken(this.deviceId, tokenType, token);
}
/**
* Destroy every existent token for the deviceId of this session
* @return {Promise}
*/
destroyPushToken() {
return this.wacProxy.getPushNotificationsService().destroyToken(this.deviceId);
}
}