services/api-service.js

/**
 * AngularJS Service for managing generic API calls.
 * Also stores default settings for API calls such as `game_id`.
 *
 * @module APIService
 * @see https://docs.angularjs.org/api/ng/type/angular.Module#service
 *
 * @requires debugService
 * @requires domainService
 * @requires instructorService
 * @requires URLParamsService
 * @requires utilityService
 */
angular.module('tgaApp').service('APIService', [
  '$http',
  '$log',
  'URLParamsService',
  'localStorageService',
  'envService',
  'debugService',
  'utilityService',
  'domainService',
  'instructorService',
  'security',
  'messagingService',
  function APIService(
    $http,
    $log,
    URLParamsService,
    localStorageService,
    envService,
    debugService,
    utilityService,
    domainService,
    instructorService,
    security,
    messagingService,
  ) {
    const serverAddress = envService.read('serverAddress');
    const useCachingServer = envService.read('useCaching');
    const cachingServerAddress = useCachingServer ? envService.read('cachingServerAddress') : null;
    /**
     * Stored `game_id` to use as default when API calls do not specify one.
     *
     * @private
     * @member {?number} game_id
     */
    let game_id = null;

    /**
     * Set stored default game ID {@link module:APIService~game_id}
     *
     * @param {?number} id
     */
    this.setGameId = (id) => {
      game_id = id;
    };

    /**
     * Get stored default game ID  {@link module:APIService~game_id}
     *
     * @returns {?number} The default `game_id` currently stored in the module instance.
     */
    this.getGameId = () => game_id;

    this.isSuccess = (status) => status < 400;
    /**
     * Generic wrapper function for API calls using angular $http service. Used by most public methods in the module.
     *
     * If `debugService.get('noAPI')` is true, this will not perform any API request, and will immediately resolve
     * as if the request responded successfully with the data `{ noAPI: true }`.
     *
     * @param {Object} params
     * @param {string} params.method - HTTP method
     * @param {string} params.url - URL endpoint (added to the environment's `serverAddress`)
     * @param {Object} [params.params] - Query parameters.
     * @param {Object} [params.data] - Request message data.
     * @param {Object} [params.config] - Set $http config properties, can override call function args
     * @param {$httpDataCallback} [params.onSuccess] - Callback to run on success (receives response data only).
     * @param {$httpCallback} [params.onError] - Callback to run on error (receives full response object).
     * @returns {Promise} Promise object, either:
     *   - Resolved with the result of the `onSuccess` callback or the HTTP response data.
     *   - Rejected with the result of the `onError` callback, the full HTTP response object, or an error.
     */
    this.call = (req) => {
      const {
        method, url, host, params, data, config = {}, onSuccess, onError,
      } = req;

      if (debugService.enabled() && debugService.get('noAPI')) {
        $log.debug(`noAPI set in debug, skipping call to ${url}`);
        return Promise.resolve(
          onSuccess ? onSuccess({ noAPI: true }) : { noAPI: true },
        );
      }
      return $http({
        method,
        url: `${host ?? serverAddress}${url}`,
        params,
        data,
        ...config,
      }).then(
        // eslint-disable-next-line max-len
        (response) => (onSuccess ? onSuccess(response.data, this.isSuccess(response.status), response.status) : response.data),
        (error) => (host ? this.call({ ...req, ...{ host: null } }) : Promise.reject(onError ? onError(error) : error)),
      );
    };

    /**
     * Get game information.
     * @see https://api.thetrainingarcade.com/documentation/#api-Game-GetGame
     *
     * @param {Object} params
     * @param {string} params.domain
     * @param {number} [params.game_id]
     * @param {string} [params.slug]
     * @param {string} [params.token]
     * @param {$httpDataCallback} [onSuccess]
     * @returns {Promise} Promise object, either:
     *   - Resolved with the result of the `onSuccess` callback or the HTTP response data.
     *   - Rejected with the full HTTP response object or an error.
     */
    this.getGame = (params, onSuccess) => this.call({
      method: 'GET',
      url: 'game',
      host: cachingServerAddress,
      params,
      onSuccess,
    });

    /**
     * Get Game Data from a file
     *
     * @param {string} url
     * @param {$httpDataCallback} [onSuccess]
     * @returns {Promise} Promise object, either:
     *   - Resolved with the result of the `onSuccess` callback or the HTTP response data.
     *   - Rejected with the full HTTP response object or an error.
     */
    this.getGameFile = (url, onSuccess) => $http({
      method: 'GET',
      url,
    }).then(
      (response) => (onSuccess ? onSuccess(response.data) : response.data),
      (error) => Promise.reject(error),
    );

    /**
     * Get player's high score for a game.
     * @see https://api.thetrainingarcade.com/documentation/#api-Player-GetScore
     *
     * @param {Object} params
     * @param {number} [params.game_id={@link module:APIService~game_id}]
     * @param {string} [params.best='highest']
     * @param {$httpDataCallback} [onSuccess]
     * @returns {Promise} Promise object, either:
     *   - Resolved with the result of the `onSuccess` callback or the HTTP response data.
     *   - Rejected with the full HTTP response object or an error.
     */
    this.getScore = (params, onSuccess) => this.call({
      method: 'GET',
      url: 'player/score',
      params: {
        game_id,
        best: 'highest',
        ...params,
      },
      onSuccess,
    });

    /**
     * Add player's score for a game. Only the player's high score for each game
     * is reported in Score and Leaderboard calls.
     * @see https://api.thetrainingarcade.com/documentation/#api-Player-PostScore
     *
     * @param {Object} data Request message data (may include entries besides those listed below).
     * @param {number} [data.game_id={@link module:APIService~game_id}]
     * @param {number} [data.points=0]
     * @param {number} [data.time=0]
     * @param {number} [data.level_id]
     * @param {$httpDataCallback} [onSuccess]
     * @param {boolean} [noSV=false]
     * @returns {Promise} Promise object, either:
     *   - Resolved with the result of the `onSuccess` callback or the HTTP response data.
     *   - Rejected with the full HTTP response object or an error.
     */
    this.reportScore = (data, onSuccess, noSV = false) => {
      messagingService.score(data.points || 0, data.time || 0);
      const {
        hub_id,
        course_id,
        challenge_id,
        session_group_id,
      } = URLParamsService.getUserParams();
      const sendData = {
        ...data,
        hub_id,
        course_id,
        challenge_id,
        session_group_id:
          instructorService.getSessionGroupId() || session_group_id,
      };
      const string = [
        data.game_id || game_id || 0,
        data.points || 0,
        data.time || 0,
        data.hub_id || hub_id || 0,
        data.course_id || course_id || 0,
        sendData.session_group_id || session_group_id || 0,
        data.challenge_id || challenge_id || 0,
        security.r,
      ].join('');
      $log.debug('sv string vars:', {
        game_id: data.game_id || 0,
        points: data.points || 0,
        time: data.time || 0,
        hub_id: data.hub_id || hub_id || 0,
        course_id: data.course_id || course_id || 0,
        session_group_id: sendData.session_group_id || session_group_id || 0,
        challenge_id: data.challenge_id || challenge_id || 0,
        r: security.r,
      });
      if (!noSV) {
        sendData.sv = utilityService.sha256(string);
      }
      return this.call({
        method: 'POST',
        url: 'player/score',
        data: {
          game_id,
          ...sendData,
        },
        onSuccess,
      });
    };

    /**
     * Add an answer attempt.
     * @see https://api.thetrainingarcade.com/documentation/#api-Player-PostAnswer
     *
     * @param {Object} data Request message data (may include entries besides those listed below).
     * @param {number} [data.game_id={@link module:APIService~game_id}]
     * @param {number} [data.level_id]
     * @param {number} data.question_id
     * @param {Boolean} data.correct
     * @param {string} [data.answer_id]
     * @param {string} [data.answer_text]
     * @param {number} data.time
     * @param {$httpDataCallback} [onSuccess]
     * * @param {$httpDataCallback} [onError]
     * @returns {Promise} Promise object, either:
     *   - Resolved with the result of the `onSuccess` callback or the HTTP response data.
     *   - Rejected with the full HTTP response object or an error.
     */
    this.reportAnswer = (data, onSuccess, onError) => {
      const { session_group_id } = URLParamsService.getUserParams();
      return this.call({
        method: 'POST',
        url: 'player/answer',
        data: {
          game_id,
          ...data,
          session_group_id:
            instructorService.getSessionGroupId() || session_group_id,
        },
        onSuccess: (resData, successVal, status) => {
          messagingService.answer(resData.correct, data.time || 0);
          if (onSuccess) {
            onSuccess(resData, successVal, status);
          }
        },
        onError,
      });
    };

    /**
     * Get leaderboard for a game for a specific question
     * @see https://api.thetrainingarcade.com/documentation/#api-Question-GetLeaderboard
     *
     * @param {number} question_id
     * @param {Object} params Query parameters (may include entries besides those listed below).
     * @param {string} [params.game_id={@link module:APIService~game_id}]
     * @param {$httpDataCallback} onSuccess
     * @returns {Promise} Promise object, either:
     *   - Resolved with the result of the `onSuccess` callback or the HTTP response data.
     *   - Rejected with the full HTTP response object or an error.
     */
    this.getQuestionLeaderboard = (question_id, params, onSuccess) => {
      const { session_group_id, hub_id } = URLParamsService.getUserParams();
      return this.call({
        method: 'GET',
        url: `question/leaderboard/${question_id}`,
        params: {
          game_id,
          session_group_id,
          hub_id,
          ...params,
        },
        onSuccess,
      });
    };

    /**
     * Get leaderboard for a game
     * @see https://api.thetrainingarcade.com/documentation/#api-Stats-GetLeaderboard
     *
     * @param {Object} params Query parameters (may include entries besides those listed below).
     * @param {string} [params.game_id={@link module:APIService~game_id}]
     * @param {string} [params.best='highest']
     * @param {number} [params.limit]
     * @param {string} [params.start_date]
     * @param {string} [params.end_date]
     * @param {$httpDataCallback} onSuccess
     * @returns {Promise} Promise object, either:
     *   - Resolved with the result of the `onSuccess` callback or the HTTP response data.
     *   - Rejected with the full HTTP response object or an error.
     */
    this.getLeaderboard = (params, onSuccess) => {
      const { session_group_id, hub_id } = URLParamsService.getUserParams();

      return this.call({
        method: 'GET',
        url: 'stats/leaderboard',
        params: {
          game_id,
          hub_id,
          best: 'highest',
          ...params,
          session_group_id:
            instructorService.getSessionGroupId() || session_group_id,
        },
        onSuccess,
      });
    };

    /**
     * Get instructor leaderboard
     *
     * @param {Object} params Query parameters (may include entries besides those listed below).
     * @param {string} [params.slug]
     * @param {$httpDataCallback} onSuccess
     * @returns {Promise} Promise object, either:
     *   - Resolved with the HTTP response object
     *   - Rejected with the full HTTP response object or an error.
     */

    this.getInstructorLeaderboard = (params, onSuccess) => this.call({
      method: 'GET',
      url: 'instructor/leaderboard',
      params,
      onSuccess,
    });

    /**
     * Get instructor state
     * @see https://api.thetrainingarcade.com/documentation/#api-Instructor-GetInstructor
     *
     * @param {string} slug
     * @returns {Promise} Promise object, either:
     *   - Resolved with the result of {@link module:instructorService.getStates}
     *   - Rejected with the full HTTP response object or an error.
     */
    this.getInstructorState = (slug) => this.call({
      method: 'GET',
      url: 'instructor',
      params: {
        domain: domainService.domain,
        slug,
      },
    });

    /**
     * Get instructor players
     *
     * @param {object} data
     * @param {number} data.session_group_id
     * @returns {Promise} Promise object, either:
     *   - Resolved with the number of players.
     *   - Rejected with the full HTTP response object or an error.
     */
    this.getInstructorPlayers = (data) => this.call({
      method: 'GET',
      url: `instructor/players/${data.session_group_id}`,
      params: {
        total: true,
      },
    });

    /**
     * Get results of question
     *
     * @param {object} data
     * @param {number} data.session_group_id
     * @param {number} data.question_id
     * @returns {Promise} Promise object, either:
     *   - Resolved with the HTTP response object.
     *   - Rejected with the full HTTP response object or an error.
     */
    this.getQuestionResults = (data) => this.call({
      method: 'GET',
      // eslint-disable-next-line max-len
      url: `question/results/${data.question_id}${data.session_group_id ? `?session_group_id=${data.session_group_id}` : ''}`,
    });

    /**
     * Get audio data for a question ID.
     * @see https://api.thetrainingarcade.com/documentation/#api-Question-GetAudio
     *
     * @param {number} question_id
     * @returns {Promise} Promise object, either:
     *   - Resolved with the HTTP response data.
     *   - Rejected with the full HTTP response object or an error.
     */
    this.getQuestionAudio = (question_id) => this.call({
      method: 'GET',
      url: `question/audio/${question_id}`,
    });
  },
]);