Home Manual Reference Source

src/administrative-sdk/connection/connection-controller.js

/* eslint-disable
camelcase
 */

import {assembleScope, authenticate, impersonate} from '../../api/auth';
import {authorisedRequest, updateSettings} from '../../api/communication';

import autobahn from 'autobahn';
import ee from 'event-emitter';

/**
 * Controller class for managing connection interaction.
 */
export default class Connection {
  /**
   *
   * @param {Object} options - Options to configure the connection with.
   * Valid options include:
   * * apiUrl - The URL of the REST api.
   * * wsUrl - The URL of the Websocket server.
   * * oAuth2Token - An OAuth2 token string.
   * * adminPrincipal - The username of the admin account.
   * * adminPassword - The password of the admin account.
   */
  constructor(options = {}) {
    /**
     * @type {Object}
     */
    this._settings = Object.assign({
      // ITSL connection parameters.
      apiUrl: 'https://api.itslanguage.nl',
      oAuth2Token: null,
      wsUrl: null,
      wsToken: null
    }, options);
    Connection._sdkCompatibility();
    this._analysisId = null;
    this._recordingId = null;
    this._recognitionId = null;
    this._emitter = ee({});
    this._connection = null;

    // Use the new connection file for future requests.
    updateSettings(Object.assign({}, options, {authorizationToken: options.oAuth2Token}));
  }

  /**
   * Add an event listener. Listens to events emitted from the websocket server connection.
   *
   * @param {string} name - Name of the event.
   * @param {Function} handler - Handler function to add.
   */
  addEventListener(name, handler) {
    this._emitter.on(name, handler);
  }

  /**
   * Remove an event listener of the websocket connection.
   *
   * @param {string} name - Name of the event.
   * @param {Function} handler - Handler function to remove.
   */
  removeEventListener(name, handler) {
    this._emitter.off(name, handler);
  }

  /**
   * Fire an event.
   *
   * @param {string} name - Name of the event.
   * @param {[]} args - Arguments.
   * @private
   */
  fireEvent(name, args = []) {
    this._emitter.emit(name, ...args);
  }

  /**
   * Create a connection to the websocket server.
   *
   */
  webSocketConnect() {
    const self = this;
    /**
     * This callback is fired during Ticket-based authentication.
     *
     * @param {Session} session - Session.
     * @param {string} method - Authentication method.
     */
    function onOAuth2Challenge(session, method) {
      if (method === 'ticket') {
        return self._settings.oAuth2Token;
      }
      throw new Error(`don't know how to authenticate using '${method}'`);
    }

    const authUrl = this._settings.wsUrl;
    let connection = null;
    // Open a websocket connection for streaming audio
    try {
      // Set up WAMP connection to router
      connection = new autobahn.Connection({
        url: authUrl,
        realm: 'default',
        // the following attributes must be set for Ticket-based authentication
        authmethods: ['ticket'],
        authid: 'oauth2',
        details: {
          ticket: this._settings.oAuth2Token
        },
        onchallenge: onOAuth2Challenge
      });
    } catch (e) {
      console.log('WebSocket creation error: ' + e);
      return;
    }
    connection.onerror = function(e) {
      console.log('WebSocket error: ' + e);
      self.fireEvent('websocketError', [e]);
    };
    connection.onopen = function(session) {
      console.log('WebSocket connection opened');
      self._session = session;
      self.fireEvent('websocketOpened');
    };
    connection.onclose = function() {
      console.log('WebSocket disconnected');
      self._session = null;
      self.fireEvent('websocketClosed');
    };
    this._connection = connection;
    this._connection.open();
  }

  /**
   * Make an RPC to active current session.
   *
   * @param {string} rpc - The RPC to call. It will be prefixed with `'nl.itslanguage.'`.
   * @param {...any} args - Any arguments to pass to the RPC.
   * @return {Promise} The result of the call.
   */
  call(rpc, ...args) {
    const url = 'nl.itslanguage.' + rpc;
    console.debug('Calling RPC:', url);
    return this._session.call(url, ...args);
  }

  webSocketDisconnect() {
    this._connection.close(null, 'Requested formal disconnect');
  }

  /**
   * Perform a HTTP GET to the API using authentication.
   *
   * @param {string} url - Url to retrieve.
   * @returns {Promise} Promise containing a result.
   * @throws {Promise.<Error>} If the server returned an error.
   */
  _secureAjaxGet(url) {
    return authorisedRequest('GET', url);
  }

  /**
   * Perform a HTTP POST to the API using authentication.
   *
   * @param {string} url - Url to submit to.
   * @param {FormData} formdata - The form to POST.
   * @returns {Promise} Promise containing a result.
   * @throws {Promise.<Error>} If the server returned an error.
   */
  _secureAjaxPost(url, formdata) {
    return authorisedRequest('POST', url, formdata);
  }

  /**
   * Perform a HTTP DELETE to the API using authentication.
   *
   * @param {string} url - Url to submit to.
   * @returns {Promise} Promise containing a result.
   * @throws {Promise.<Error>} If the server returned an error.
   */
  _secureAjaxDelete(url) {
    return authorisedRequest('DELETE', url);
  }

  /**
   * Add an access token to the given URL.
   *
   * @param {string} url - The URL to add an access token to.
   * @returns {string} An url with the access token appended.
   */
  addAccessToken(url) {
    if (!this._settings.oAuth2Token) {
      throw new Error('Please set oAuth2Token');
    }
    const secureUrl = url + (url.match(/\?/) ? '&' : '?') + 'access_token=' +
      encodeURIComponent(this._settings.oAuth2Token);
    return secureUrl;
  }

  /**
   * Logs browser compatibility for required and optional SDK capabilities.
   *
   * @throws {Error} In case of compatibility issues.
   */
  static _sdkCompatibility() {
    // WebSocket
    // http://caniuse.com/#feat=websockets
    if (!('WebSocket' in window)) {
      throw new Error('No WebSocket capabilities');
    }
  }

  /**
   * Cancel any current streaming audio recording.
   *
   * @param {AudioRecorder} recorder - The audio recorder currently recording.
   */
  cancelStreaming(recorder) {
    const self = this;

    if (this._recordingId === null && this._analysisId === null && this._recognitionId === null) {
      console.info('No session in progress, nothing to cancel.');
      return;
    }

    recorder.removeAllEventListeners();
    if (recorder.isRecording()) {
      recorder.stop();
    }

    // This session is over.
    self._recordingId = null;
    self._analysisId = null;
    self._recognitionId = null;
  }

  /**
   * Log an error caught from an RPC call.
   *
   * @param {Object} result - Error object.
   */
  static logRPCError(result) {
    console.error('RPC error returned:', result.error);
  }

  /**
   * Ask the server for an OAuth2 token.
   *
   * @param {BasicAuth|EmailCredentials} [auth] - Auth to obtain credentials from. If omitted we
   *                                              assume impersonation.
   * @param {string} [scope] - The scope which should be available for the requested token. If
   *                           omitted the current user will be used.
   * @returns {Promise} Promise containing a access_token, token_type and scope.
   * @throws {Promise.<Error>} If the server returned an error.
   */
  getOauth2Token(auth, scope) {
    const handleResponse = response => {
      this._settings.oAuth2Token = response.access_token;
      return response;
    };

    if (!auth) {
      // We didn't get auth, so we need to assume impersonation here!
      return impersonate(scope).then(handleResponse);
    }

    // Auth is either BasicAuth or EmailCredentials.
    // In case of both (which theoretically could not happen, but hey) the BasicAuth takes
    // precedent over the EmailCredentials.
    const {principal, credentials, email, password} = auth;
    return authenticate(principal || email, credentials || password, scope).then(handleResponse);
  }

  /**
   * Request authentication for a {@link User}. This will either work for users with BasicAuth or
   * for users with EmailCredentials (EmailAuth).
   *
   * This method also generates the appropriate scope for the given params.
   *
   * @param {BasicAuth|EmailCredentials} auth - Auth to obtain credentials from.
   * @param {string} [organisationId] - Id of the organisation this user is part of.
   *
   */
  getUserAuth(auth, organisationId) {
    // Auth is either BasicAuth or EmailCredentials.
    // In case of both (which theoretically could not happen, but hey) the BasicAuth takes
    // precedent over the EmailCredentials.
    const {tenantId, principal, email} = auth;
    const scope = assembleScope(tenantId, organisationId, principal || email);
    return this.getOauth2Token(auth, scope);
  }
}