Source: SteamWebAPI.js

/**
 * steamwebapi
 *
 * Unofficial Steam Node.js SDK for interfacing with Steam's Web API
 *
 * This is free and unencumbered software released into the public domain.
 *
 * Anyone is free to copy, modify, publish, use, compile, sell, or
 * distribute this software, either in source code form or as a compiled
 * binary, for any purpose, commercial or non-commercial, and by any
 * means.
 *
 * In jurisdictions that recognize copyright laws, the author or authors
 * of this software dedicate any and all copyright interest in the
 * software to the public domain. We make this dedication for the benefit
 * of the public at large and to the detriment of our heirs and
 * successors. We intend this dedication to be an overt act of
 * relinquishment in perpetuity of all present and future rights to this
 * software under copyright law.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
 * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
 * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 *
 * For more information, please refer to <http://unlicense.org/>
 *
 * @author Kennedy Software Solutions Inc.
 * @version 0.0.4
 */


/**********************************************************************************************************************
                                          Includes & letiable declaration
 **********************************************************************************************************************/
// Required modules
let XMLHttpRequest = require('xmlhttprequest').XMLHttpRequest;

// Helper modules
let config = require('./config');
let util = require('./util');
let endpoints = require('./endpoints');

// Instance variables
let responseFormat = 'json';
let apiKey;

/**
 * Validate parameters
 *
 * Does light validation on the provided set of parameters to an API request
 *
 * @param parameters    {object}    The object containing the parameters for the request
 * @returns             {boolean}   True if all parameters are valid, else false
 */
function parametersValid(parameters) {

    // Iterate through all the provided parameters
    for (let property in parameters) {
        
        // Filter out prototype properties
        if (parameters.hasOwnProperty(property)) {
            
            // Each parameter must be a string
            if (typeof property !== 'string') {
                return false;
            }

            // Store the value of the property
            let value = parameters[property];

            switch (property.toLowerCase()) {

                case 'count':
                case 'maxlength':
                case 'appid':
                case 'appid_playing':
                case 'gameid':

                    if (!util.isNumber(value) || value < 0) {

                        return false;
                    }

                    break;

                case 'name':

                    if (!util.isArray(value)) {

                        return false;
                    }

                    break;

                case 'steamids':

                    let steamids = value.split(',');

                    if (steamids.length > 100) {

                        return false;

                    } else {

                        // Iterate through all provided IDs
                        for (let id in steamids) {

                            // Filter prototype properties
                            if (steamids.hasOwnProperty(id)) {

                                if (!util.isNumber(id) || id.length !== 17) {

                                    return false;
                                }
                            }
                        }
                    }

                    break;

                case 'steamid':

                    if (!util.isNumber(value) || value.length !== 17) {

                        return false;
                    }

                    break;

                case 'relationship':

                    if (config.VALID_RELATIONSHIPS.indexOf(value) < 0) {

                        return false;
                    }

                    break;

                case 'appids_filter':

                    if (!util.isArray(value)) {
                        return false;
                    }

                    for (let appid in value) {

                        // Filter prototype properties
                        if (value.hasOwnProperty(appid)) {

                            if (!util.isNumber(appid) || appid < 0) {

                                return false;
                            }
                        }
                    }

                    // Special case handling to pass appids_filter as "input_json"
                    parameters['input_json'] = encodeURIComponent(JSON.stringify(value));
                    delete parameters[property];

                    break;

                default:
                    break;
            }
        }
    }

    return true;
}


/**********************************************************************************************************************
                                             API SDK module exports
 **********************************************************************************************************************/
/**
 * This module exports the API helper functions
 * @module SteamWebAPI
 */
module.exports = {
    /**
     * Set response format
     *
     * Sets the default response format for responses from the Steam Web API
     *
     * @param newFormat {string}    The new preferred response format
     */
    setFormat: function(newFormat) {

        // If it's valid then set it as the new default
        if (config.VALID_RESPONSE_FORMATS.includes(newFormat)) {
            responseFormat = newFormat;
        }
    },

    /**
     * Set API Key
     *
     * Sets the API key that will be used to validate the API requests
     *
     * @param newAPIKey {string}    The new API key to use
     * @returns         {boolean}   If this function successfully set the provided API key
     */
    setAPIKey: function(newAPIKey) {
        // Verify the inputted api key is valid
        let isValidAPIKey = (typeof newAPIKey === 'string' && newAPIKey.length === 32);

        // If it's valid then set it as the API Key
        if (isValidAPIKey) {
            apiKey = newAPIKey;
        }

        return isValidAPIKey;
    },

    /**
     * Send API request
     *
     * Send the specified request to the Steam Web API and execute the callback upon success or failure
     *
     * @param method        {string}    The method name on the Steam Web API
     * @param parameters    {object}    An object containing all the required parameters for the API request
     * @param callback      {function}  The function to call upon success or failure
     */
    send: function(method, parameters, callback) {
        // Use XMLHttpRequest for sending requests to the API
        let request = new XMLHttpRequest();

        // Check if attempting to send valid request method to the API
        if (!(endpoints.hasOwnProperty(method))) {
            callback({'error': config.METHOD_NOT_EXIST})
            return;
        }

        // Load the specification for the method
        let requestSpecification = endpoints[method];

        // Check if all required letiables have been provided
        if (!(typeof parameters === 'object')) {

            callback({'error': config.PARAMETERS_INVALID_FORMAT});
            return;
        }

        let requirementsMet = true;
        let urlParameters = '?';

        // If the API method requires a key, check that it has been defined
        if (requestSpecification.key === true) {

            if (!(util.isDefined(apiKey))) {
                callback({'error': config.API_KEY_NOT_SET});
                return;
            }

            urlParameters += 'key=' + apiKey + '&';
        }

        // Call this validation before checking if meeting requirements for special case handling
        // (i.e. appids_filter for GetOwnedGames must be formatted as JSON string and passed as "input_json")
        if (!(parametersValid(parameters))) {
            callback({'error': config.PARAMETERS_INVALID_VALUE});
        }

        // Iterate through all required letiables from the specification
        for (let requiredParameter in requestSpecification.parameters) {

            // Filter out prototype properties
            if (requestSpecification.parameters.hasOwnProperty(requiredParameter)) {

                // Check if the provided params have the required letiables
                if (parameters.hasOwnProperty(requiredParameter)) {

                    requestSpecification[requiredParameter] = parameters[requiredParameter];
                    urlParameters += requiredParameter + '=' + parameters[requiredParameter] + '&';
                } else {

                    // Check if this missing parameter is required
                    if (requestSpecification.parameters[requiredParameter] === true) {

                        callback({'error': config.PARAMETERS_MISSING});
                        return;
                    }
                }
            }
        }

        // Remove trailing & from url parameters string
        urlParameters = urlParameters.substring(0, urlParameters.length - 1);

        // Set request details
        request.open('GET', config.STEAM_API_BASE_URL + requestSpecification.category + method + '/' +
            requestSpecification.version + urlParameters, true);

        // Handle request response
        request.onreadystatechange = function () {

            // Ignore incomplete requests
            if (!(request.readyState === 4 && request.status === 200)) {

                return;
            }

            try {

                // Handle the different preferred response types
                if (responseFormat === 'json') {

                    let jsonObj = JSON.parse(request.responseText);
                    callback(jsonObj);

                } else if (responseFormat === 'xml') {

                    callback(request.responseXML);
                } else {

                    callback(request.responseText);
                }
            } catch (error) {
                callback({'error': error});
            }
        };

        // Send the request
        request.send();
    },

    /******************************************************************************************************************
                                                    SDK interfaces
     ******************************************************************************************************************/
    // Pre-built interfaces for the send function for the supported API endpoints
    /**
     * Get news for an app ID
     *
     * GetNewsForApp returns the latest of a game specified by its appID
     *
     * @param appid     {int}       AppID of the game you want the news of
     * @param count     {int}       How many news enties you want to get returned
     * @param maxLength {int}       Maximum length of each news entry
     * @param callback  {function}  Function to handle the response
     */
    getNewsForApp: function(appid, count, maxLength, callback) {
        this.send('GetNewsForApp', {
            'appid': appid,
            'count': count,
            'maxLength': maxLength
        }, callback);
    },

    /**
     * Get global achievement percentages for an app ID
     *
     * Returns on global achievements overview of a specific game in percentages
     *
     * @param gameid    {int}       AppID of the game you want the percentages of
     * @param callback  {function}  Function to handle the response
     */
    getGlobalAchievementPercentagesForApp: function(gameid, callback) {
        this.send('GetGlobalAchievementPercentagesForApp', {
            'gameid': gameid
        }, callback);
    },

    /**
     * Get global stats for game
     *
     * Get the global stats for the provided achievement names
     *
     * @param gameid    {int}       AppID of the game you want the stats of
     * @param count     {int}       Length of the array of global stat names you will be passing
     * @param name      {array}     Name of the achievement as defined in Steamworks
     * @param callback  {function}  Function to handle the response
     */
    getGlobalStatsForGame: function(gameid, count, name, callback) {
        this.send('GetGlobalStatsForGame', {
            'gameid': gameid,
            'count': count,
            'name': name
        }, callback);
    },

    /**
     * Get player summaries
     *
     * Returns basic profile information for a list of 64-bit Steam IDs
     *
     * @param steamids  {string}    Comma-delimited list of 64 bit Steam IDs to return profile information for. Up to
     *                              100 Steam IDs can be requested
     * @param callback  {function}  Function to handle the response
     */
    getPlayerSummaries: function(steamids, callback) {
        this.send('GetPlayerSummaries', {
            'steamids': steamids
        }, callback);
    },

    /**
     * Get friend list
     *
     * Returns the friend list of any Steam user, provided his Steam Community profile visibility is set to "Public"
     *
     * @param steamid       {string}    64 bit Steam ID to return friend list for
     * @param relationship  {string}    Relationship filter. Possibles values: all, friend
     * @param callback      {function}  Function to handle the response
     */
    getFriendList: function(steamid, relationship, callback) {
        this.send('GetFriendList', {
            'steamid': steamid,
            'relationship': relationship
        }, callback);
    },

    /**
     * Get player achievements
     *
     * Returns a list of achievements for this user by app id
     *
     * @param steamid   {string}    64 bit Steam ID to return friend list for
     * @param appid     {int}       The ID for the game you're requesting
     * @param callback  {function}  Function to handle the response
     */
    getPlayerAchievements: function(steamid, appid, callback) {
        this.send('GetPlayerAchievements', {
            'steamid': steamid,
            'appid': appid
        }, callback);
    },

    /**
     * Get user stats for game
     *
     * Returns a list of achievements for this user by app id
     *
     * @param steamid   {string}    64 bit Steam ID to return friend list for
     * @param appid     {int}       The ID for the game you're requesting
     * @param callback  {function}  Function to handle the response
     */
    getUserStatsForGame: function(steamid, appid, callback) {
        this.send('GetUserStatsForGame', {
            'steamid': steamid,
            'appid': appid
        }, callback);
    },

    /**
     * Get owned games
     *
     * GetOwnedGames returns a list of games a player owns along with some playtime information, if the profile is
     * publicly visible. Private, friends-only, and other privacy settings are not supported unless you are asking for
     * your own personal details (ie the WebAPI key you are using is linked to the steamid you are requesting).
     *
     * @param steamid                       {string}    The SteamID of the account
     * @param include_appinfo               {string}    Include game name and logo information in the output. The
     *                                                  default is to return appids only
     * @param include_played_free_games     {boolean}   By default, free games like Team Fortress 2 are excluded (as
     *                                                  technically everyone owns them). If include_played_free_games is
     *                                                  set, they will be returned if the player has played them at some
     *                                                  point. This is the same behavior as the games list on the Steam
     *                                                  Community
     * @param appids_filter                 {array}     You can optionally filter the list to a set of appids. Note that
     *                                                  these cannot be passed as a URL parameter, instead you must use
     *                                                  the JSON format described in
     *                                                  Steam_Web_API#Calling_Service_interfaces. The expected input is
     *                                                  an array of integers (in JSON: "appids_filter: [ 440, 500, 550 ]
     *                                                  " )
     * @param callback                      {function}  Function to handle the response
     */
    getOwnedGames: function(steamid, include_appinfo, include_played_free_games, appids_filter, callback) {
        let parameters = {
            'steamid': steamid
        };

        // Optional parameters
        if (util.isDefined(include_appinfo)) {
            parameters.include_appinfo = include_appinfo;
        }
        if (util.isDefined(include_played_free_games) && util.isDefined(include_played_free_games.length) && include_played_free_games.length > 0) {
            parameters.include_played_free_games = include_played_free_games;
        }
        if (util.isDefined(appids_filter)) {
            parameters.appids_filter = appids_filter;
        }

        this.send('GetOwnedGames', parameters, callback);
    },

    /**
     * Get recently played games
     *
     * GetRecentlyPlayedGames returns a list of games a player has played in the last two weeks, if the profile is
     * publicly visible. Private, friends-only, and other privacy settings are not supported unless you are asking for
     * your own personal details (ie the WebAPI key you are using is linked to the steamid you are requesting).
     *
     * @param steamid   {string}    The SteamID of the account
     * @param count     {int}       Optionally limit to a certain number of games (the number of games a person has
     *                              played in the last 2 weeks is typically very small)
     * @param callback  {function}  Function to handle the response
     */
    getRecentlyPlayedGames: function(steamid, count, callback) {
        let parameters = {
            'steamid': steamid
        };

        // Optional parameters
        if (util.isDefined(count)) {
            parameters.count = count;
        }

        this.send('GetRecentlyPlayedGames', parameters, callback);
    },

    /**
     * Is playing shared game
     *
     * IsPlayingSharedGame returns the original owner's SteamID if a borrowing account is currently playing this game.
     * If the game is not borrowed or the borrower currently doesn't play this game, the result is always 0.
     *
     * @param steamid           {string}    The SteamID of the account playing
     * @param appid_playing     {int}       The AppID of the game currently playing
     * @param callback          {function}  Function to handle the response
     */
    isPlayingSharedGame: function(steamid, appid_playing, callback) {
        this.send('GetUserStatsForGame', {
            'steamid': steamid,
            'appid_playing': appid_playing
        }, callback);
    }
};