// @flow import _ from 'lodash'; import type { Dispatch } from 'redux'; import { conferenceLeft, conferenceWillLeave, getCurrentConference } from '../conference'; import JitsiMeetJS, { JitsiConnectionEvents } from '../lib-jitsi-meet'; import { parseURIString } from '../util'; import { CONNECTION_DISCONNECTED, CONNECTION_ESTABLISHED, CONNECTION_FAILED, CONNECTION_WILL_CONNECT, SET_LOCATION_URL } from './actionTypes'; import { JITSI_CONNECTION_URL_KEY } from './constants'; const logger = require('jitsi-meet-logger').getLogger(__filename); /** * The error structure passed to the {@link connectionFailed} action. * * Note there was an intention to make the error resemble an Error instance (to * the extent that jitsi-meet needs it). */ export type ConnectionFailedError = { /** * The invalid credentials that were used to authenticate and the * authentication failed. */ credentials?: { /** * The XMPP user's ID. */ jid: string, /** * The XMPP user's password. */ password: string }, /** * The details about the connection failed event. */ details?: Object, /** * Error message. */ message?: string, /** * One of {@link JitsiConnectionError} constants (defined in * lib-jitsi-meet). */ name: string, /** * Indicates whether this event is recoverable or not. */ recoverable?: boolean }; /** * Opens new connection. * * @param {string} [id] - The XMPP user's ID (e.g. {@code user@server.com}). * @param {string} [password] - The XMPP user's password. * @returns {Function} */ export function connect(id: ?string, password: ?string) { return (dispatch: Dispatch, getState: Function) => { const state = getState(); const options = _constructOptions(state); const { locationURL } = state['features/base/connection']; const { issuer, jwt } = state['features/base/jwt']; const connection = new JitsiMeetJS.JitsiConnection( options.appId, jwt && issuer && issuer !== 'anonymous' ? jwt : undefined, options); connection[JITSI_CONNECTION_URL_KEY] = locationURL; dispatch(_connectionWillConnect(connection)); connection.addEventListener( JitsiConnectionEvents.CONNECTION_DISCONNECTED, _onConnectionDisconnected); connection.addEventListener( JitsiConnectionEvents.CONNECTION_ESTABLISHED, _onConnectionEstablished); connection.addEventListener( JitsiConnectionEvents.CONNECTION_FAILED, _onConnectionFailed); return connection.connect({ id, password }); /** * Dispatches {@code CONNECTION_DISCONNECTED} action when connection is * disconnected. * * @param {string} message - Disconnect reason. * @private * @returns {void} */ function _onConnectionDisconnected(message: string) { unsubscribe(); dispatch(_connectionDisconnected(connection, message)); } /** * Resolves external promise when connection is established. * * @private * @returns {void} */ function _onConnectionEstablished() { connection.removeEventListener( JitsiConnectionEvents.CONNECTION_ESTABLISHED, _onConnectionEstablished); dispatch(connectionEstablished(connection, Date.now())); } /** * Rejects external promise when connection fails. * * @param {JitsiConnectionErrors} err - Connection error. * @param {string} [msg] - Error message supplied by lib-jitsi-meet. * @param {Object} [credentials] - The invalid credentials that were * used to authenticate and the authentication failed. * @param {string} [credentials.jid] - The XMPP user's ID. * @param {string} [credentials.password] - The XMPP user's password. * @param {Object} details - Additional information about the error. * @private * @returns {void} */ function _onConnectionFailed( // eslint-disable-line max-params err: string, msg: string, credentials: Object, details: Object) { unsubscribe(); dispatch( connectionFailed( connection, { credentials, details, name: err, message: msg } )); } /** * Unsubscribe the connection instance from * {@code CONNECTION_DISCONNECTED} and {@code CONNECTION_FAILED} events. * * @returns {void} */ function unsubscribe() { connection.removeEventListener( JitsiConnectionEvents.CONNECTION_DISCONNECTED, _onConnectionDisconnected); connection.removeEventListener( JitsiConnectionEvents.CONNECTION_FAILED, _onConnectionFailed); } }; } /** * Create an action for when the signaling connection has been lost. * * @param {JitsiConnection} connection - The {@code JitsiConnection} which * disconnected. * @param {string} message - Error message. * @private * @returns {{ * type: CONNECTION_DISCONNECTED, * connection: JitsiConnection, * message: string * }} */ function _connectionDisconnected(connection: Object, message: string) { return { type: CONNECTION_DISCONNECTED, connection, message }; } /** * Create an action for when the signaling connection has been established. * * @param {JitsiConnection} connection - The {@code JitsiConnection} which was * established. * @param {number} timeEstablished - The time at which the * {@code JitsiConnection} which was established. * @public * @returns {{ * type: CONNECTION_ESTABLISHED, * connection: JitsiConnection, * timeEstablished: number * }} */ export function connectionEstablished( connection: Object, timeEstablished: number) { return { type: CONNECTION_ESTABLISHED, connection, timeEstablished }; } /** * Create an action for when the signaling connection could not be created. * * @param {JitsiConnection} connection - The {@code JitsiConnection} which * failed. * @param {ConnectionFailedError} error - Error. * @public * @returns {{ * type: CONNECTION_FAILED, * connection: JitsiConnection, * error: ConnectionFailedError * }} */ export function connectionFailed( connection: Object, error: ConnectionFailedError) { const { credentials } = error; if (credentials && !Object.keys(credentials).length) { error.credentials = undefined; } return { type: CONNECTION_FAILED, connection, error }; } /** * Create an action for when a connection will connect. * * @param {JitsiConnection} connection - The {@code JitsiConnection} which will * connect. * @private * @returns {{ * type: CONNECTION_WILL_CONNECT, * connection: JitsiConnection * }} */ function _connectionWillConnect(connection) { return { type: CONNECTION_WILL_CONNECT, connection }; } /** * Constructs options to be passed to the constructor of {@code JitsiConnection} * based on the redux state. * * @param {Object} state - The redux state. * @returns {Object} The options to be passed to the constructor of * {@code JitsiConnection}. */ function _constructOptions(state) { // Deep clone the options to make sure we don't modify the object in the // redux store. const options = _.cloneDeep(state['features/base/config']); // Normalize the BOSH URL. let { bosh } = options; if (bosh) { const { locationURL } = state['features/base/connection']; if (bosh.startsWith('//')) { // By default our config.js doesn't include the protocol. bosh = `${locationURL.protocol}${bosh}`; } else if (bosh.startsWith('/')) { // Handle relative URLs, which won't work on mobile. const { protocol, hostname, contextRoot } = parseURIString(locationURL.href); // eslint-disable-next-line max-len bosh = `${protocol}//${hostname}${contextRoot || '/'}${bosh.substr(1)}`; } // Append room to the URL's search. const { room } = state['features/base/conference']; // XXX The Jitsi Meet deployments require the room argument to be in // lower case at the time of this writing but, unfortunately, they do // not ignore case themselves. room && (bosh += `?room=${room.toLowerCase()}`); options.bosh = bosh; } return options; } /** * Closes connection. * * @returns {Function} */ export function disconnect() { return (dispatch: Dispatch, getState: Function): Promise => { const state = getState(); // The conference we have already joined or are joining. const conference_ = getCurrentConference(state); // Promise which completes when the conference has been left and the // connection has been disconnected. let promise; // Leave the conference. if (conference_) { // In a fashion similar to JitsiConference's CONFERENCE_LEFT event // (and the respective Redux action) which is fired after the // conference has been left, notify the application about the // intention to leave the conference. dispatch(conferenceWillLeave(conference_)); promise = conference_.leave() .catch(error => { logger.warn( 'JitsiConference.leave() rejected with:', error); // The library lib-jitsi-meet failed to make the // JitsiConference leave. Which may be because // JitsiConference thinks it has already left. // Regardless of the failure reason, continue in // jitsi-meet as if the leave has succeeded. dispatch(conferenceLeft(conference_)); }); } else { promise = Promise.resolve(); } // Disconnect the connection. const { connecting, connection } = state['features/base/connection']; // The connection we have already connected or are connecting. const connection_ = connection || connecting; if (connection_) { promise = promise.then(() => connection_.disconnect()); } else { logger.info('No connection found while disconnecting.'); } return promise; }; } /** * Sets the location URL of the application, connecton, conference, etc. * * @param {URL} [locationURL] - The location URL of the application, * connection, conference, etc. * @returns {{ * type: SET_LOCATION_URL, * locationURL: URL * }} */ export function setLocationURL(locationURL: ?URL) { return { type: SET_LOCATION_URL, locationURL }; }