Home Reference Source

src/session/Session.js

import {version} from '../../version';
import {PromiseQueue} from '../utils/PromiseQueue';
import {SessionStatus} from './SessionStatus';
import {SessionError} from './SessionError';
import {bindMethods} from '../utils';
import {
	LoginError,
	StackUnregisterError,
	StackRegisterError,
	WacInternalError,
	AlreadyLoggedError,
} from '../wac-proxy/errors';
import {ChatManager} from '../chat/ChatManager';
import {ContactManager} from '../contacts/ContactManager';
import {FileManager} from '../file-sharing/FileManager';
import {EventEmitter} from '../eventemitter';
import {CallManager} from '../calls/CallManager';
import {ConferenceManager} from '../conferences/ConferenceManager';
import {GroupManager} from '../groups/GroupManager';
import {InstantMessage} from '../instant-messages/InstantMessage';
import {InstantMessageType} from '../instant-messages/InstantMessageType';
import Logs from '../Logs';
import {MeetingManager} from '../meetings/MeetingManager';
import {DataPipeFactory} from '../datapipe/DataPipeFactory';
import {DataPipeType} from '../datapipe/DataPipeType';
import {DataChannelDataPipe} from '../datapipe/DataChannelDataPipe';
import {WacDataPipe} from '../datapipe/WacDataPipe';
import {WacProxy} from '../wac-proxy/WacProxy';
import {PresenceManager} from '../contacts/PresenceManager';
import {RemoteSession} from './RemoteSession';
import Selfie from '../misc/Selfie';
import {User} from './User';
import {UserManager} from '../users/UserManager';
import {Whiteboard} from '../whiteboard/Whiteboard';

let 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);
	}

	/** @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 */
		this.userManager = new UserManager(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:
				new Whiteboard(dataPipe).init()
					.then(whiteboard => this.emit('incomingWhiteboard', whiteboard))
					.catch(log.error);
				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) {
		let 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) {
			let 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]);
				let 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) {
		let 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);
		}
		return this.conferenceManager;
	}

	/**
	 * Retrieve a UserManager object
	 * @return {UserManager}
	 */
	getUserManager() {
		return this.userManager;
	}

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

	/**
	 * Retrieve the contact manager.
	 * @return {ContactManager}
	 */
	getContactManager() {
		if (this.contactManager === undefined) {
			/** @private */
			this.contactManager = new ContactManager(this.wacProxy, this.userManager, this.getPresenceManager());
		}
		return this.contactManager;
	}

	/**
	 * 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) => {
				return sessions.filter(x => x.id !== this.session.id).map((x) => {
					x.name = 'wac';
					return x;
				});
			});

		}).then((sessions) => {
			return sessions.map((session) => {
				return 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 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 userManager = this.getUserManager();
			/** @private */
			this.groupManager = GroupManager.newInstance(groupService, phonebookService, userManager);
		}
		return this.groupManager;
	}

	/**
	 * Retrieves the presence manager.
	 * @return {PresenceManager}
	 */
	getPresenceManager() {
		if (!this.presenceManager) {
			const presenceService = this.wacProxy.getPresenceService();
			this.presenceManager = PresenceManager.of(presenceService);
		}
		return this.presenceManager;
	}

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