var http = require('http');
var Promise = require('promise');
var request = require('request');
var _ = require('underscore');
var md5 = require('js-md5');
var objAssign = require('object.assign').getPolyfill();
var chunk = require('chunk');
/**
* GW2 API Main interface
*
* @class
* @author cthos <cthos@alextheward.com>
*/
var GW2API = function () {
this.storage = typeof localStorage === "undefined" ? null : localStorage;
this.lang = 'en_US';
this.cache = this.storeInCache = false;
this.useAuthHeader = true;
}
GW2API.prototype = {
/**
* API Base url. Constant
* @type {String}
*/
baseUrl: "https://api.guildwars2.com/v2/",
/**
* Set the storage solution.
*
* The solution should have a getItem and setItem method, but can be anything.
*
* @param {object} storage
* Storage solution. Defaults to localStorage if available. Null if not.
*/
setStorage: function (storage) {
this.storage = storage;
return this;
},
/**
* Gets the storage solution.
*
* @return {object}
* Storage solution.
*/
getStorage: function () {
return this.storage;
},
/**
* Setter for the useAuthHeader property.
*
* Typically you'll set this to false if you're in a browser
* because the API doesn't support OPTIONS.
*
* @param {boolean} useAuthHeader
* @returns {GW2API}
*/
setUseAuthHeader: function (useAuthHeader) {
this.useAuthHeader = useAuthHeader;
return this;
},
/**
* Getter for useAuthHeader.
*
* @returns {boolean}
*/
getUseAuthHeader: function () {
return this.useAuthHeader;
},
/**
* Sets the language code. Should be the ISO code (en_US for example).
*
* @param {string} langCode
* Target language code.
*
* @return this
*/
setLang: function (langCode) {
this.lang = langCode;
return this;
},
/**
* Gets the current language, which some api endpoints use.
*
* @return {string}
* langcode
*/
getLang: function () {
return this.lang;
},
/**
* Gets the boolean cache setting.
*
* @return {boolean}
*/
getCache: function () {
return this.cache;
},
/**
* Turns caching on or off.
*
* @param {boolean} cache
* Enable or disable cache.
*/
setCache: function (cache) {
this.cache = this.storeInCache = cache;
return this;
},
/**
* Enables or disables storing API results in cache.
* This is distinct from setCache, which turns all caching on or off.
*
* This setting would be used to update the cache but not actually return
* the results.
*
* @param {boolean} storeInCache
* Whether or not to store results in cache.
*/
setStoreInCache: function (storeInCache) {
this.storeInCache = storeInCache;
return this;
},
/**
* Stores the API key in storage.
*
* @param {string} key
* API Key to use for requests.
* @return this
*/
setAPIKey: function (key) {
this.storage.setItem('apiKey', key);
return this;
},
/**
* Loads the API key from the local storage.
*
* @return string
*/
getAPIKey: function () {
return this.storage.getItem('apiKey');
},
/**
* Gets account information.
*
* @returns {Promise}
*/
getAccount: function () {
var endpoint = '/account';
return this.callAPI(endpoint);
},
/**
* Loads the characters associated with the assigned API token.
*
* Requires authenticaion
*
* @param {string} characterName
* <optional> Get details on a particular character.
*
* @return Promise
*/
getCharacters: function (characterName) {
var endpoint = '/characters';
if (typeof characterName !== 'undefined') {
endpoint += '/' + encodeURIComponent(characterName);
}
return this.callAPI(endpoint);
},
/**
* Gets Account achievements.
*
* @param {Boolean} autoTranslateAchievements
* If this is set to true, it will automatically call the achievement
* endpoint to get more details as part of the return.
*
* @return {Array}
* Account achievements.
*/
getAccountAchievements: function (autoTranslateAchievements) {
var p = this.callAPI('/account/achievements');
var that = this;
if (!autoTranslateAchievements) {
return p;
}
return p.then(function (accountAchievements) {
return that.getDeeperInfo(that.getAchievements, accountAchievements, 100);
});
},
/**
* Gets items from the account bank.
*
* @param {Boolean} autoTranslateItems
* Whether or not to automatically call the item endpoint.
* @return {Promise}
*/
getAccountBank: function (autoTranslateItems) {
var p = this.callAPI('/account/bank');
var that = this;
if (!autoTranslateItems) {
return p;
}
return p.then(function (bank) {
return that.getDeeperInfo(that.getItems, bank, 100);
});
},
/**
* Gets unlocked account dyes.
* @param {Boolean} autoTranslateItems
* <optional> If passed as true, will automatically get item descriptions
* from the items api.
* @return {Promise}
*/
getAccountDyes: function (autoTranslateItems) {
var p = this.callAPI('/account/dyes');
var that = this;
if (!autoTranslateItems) {
return p;
}
return p.then(function (dyes) {
return that.getDeeperInfo(that.getColors, dyes, 100);
});
},
/**
* Gets the account's material storage.
*
* @param {Boolean} autoTranslateItems
* <optional> If passed as true, will automatically get item descriptions
* from the materials api.
* @return {Promise}
*/
getAccountMaterials: function (autoTranslateItems) {
var p = this.callAPI('/account/materials');
var that = this;
if (!autoTranslateItems) {
return p;
}
return p.then(function (materials) {
return that.getDeeperInfo(that.getItems, materials, 100);
});
},
/**
* Gets the account's masteries.
*
* @param {Boolean} autoTranslateMasteries
*
* @returns {Promise}
*/
getAccountMasteries: function (autoTranslateMasteries) {
var p = this.callAPI('/account/masteries');
var that = this;
if (!autoTranslateMasteries) {
return p;
}
return p.then(function (masteries) {
return that.getDeeperInfo(that.getMasteries, masteries, 100);
});
},
/**
* Gets the account's finishers.
*
* @param {Boolean} autoTranslate
*
* @returns {Promise}
*/
getAccountFinishers: function (autoTranslate) {
var p = this.callAPI('/account/finishers');
var that = this;
if (!autoTranslate) {
return p;
}
return p.then(function (finishers) {
return that.getDeeperInfo(that.getFinishers, finishers, 100);
});
},
/**
* Gets the account's unlocked minis.
*
* @param {Boolean} autoTranslateItems
* <optional> If passed as true, will automatically get item descriptions
* from the items api.
* @return {Promise}
*/
getAccountMinis: function (autoTranslateItems) {
var p = this.callAPI('/account/minis');
var that = this;
if (!autoTranslateItems) {
return p;
}
return p.then(function (minis) {
return that.getDeeperInfo(that.getMinis, minis, 100);
});
},
/**
* Gets the account's item skins.
*
* @param {Boolean} autoTranslateItems
* <optional> If passed as true, will automatically get item descriptions
* from the items api.
* @return {Promise}
*/
getAccountSkins: function (autoTranslateItems) {
var p = this.callAPI('/account/skins');
var that = this;
if (!autoTranslateItems) {
return p;
}
return p.then(function (skins) {
return that.getDeeperInfo(that.getSkins, skins, 100);
});
},
/**
* Gets an account's commerce transactions.
*
* @param {Boolean} current
* If true, this will query current transactions. Otherwise it
* will query historical transactions.
* @param {String} secondLevel
* Either "buys" or "Sells"
* @return {Promise}
*/
getCommerceTransactions: function (current, secondLevel) {
var endpoint = "/commerce/transactions/" + (current ? 'current' : 'history') + '/' + secondLevel;
return this.callAPI(endpoint);
},
/**
* Gets commerce listings. If no item ids are passed, it will return
* a list of all possible ids.
*
* @param {Int|Array} itemIds
* Either an Int or Array of items
* @return {Promise}
*/
getCommerceListings: function (itemIds) {
return this.getOneOrMany('/commerce/listings', itemIds, false);
},
/**
* Returns the current gem buy and sell prices.
*
* Quantity _must_ be higher than needed to buy a single coin or gem.
*
* @param {String} gemOrCoin
* The string 'gem' for gold cost to buy gems.
* 'coin' for gem price for coins.
* @param {Int} quantity
* The number of coins or gems to exchange (this is a required parameter).
* @return {Promise}
*/
getCommerceExchange: function (gemOrCoin, quantity) {
var second = gemOrCoin === 'gems' ? 'gems' : 'coins';
return this.callAPI('/commerce/exchange/' + second, { 'quantity': quantity }, false);
},
/**
* Gets overall account pvp statistics.
*
* @return {Promise}
*/
getPVPStats: function () {
return this.callAPI('/pvp/stats');
},
/**
* Gets PVP Game details. If ids are not passed a list of all game ids
* are returned.
*
* @param {String|Array} gameIds
* <optional> Either a gameId or an array of games you'd like more details
* on. Note that GameId is a uuid.
* @return {Promise}
*/
getPVPGames: function (gameIds) {
return this.getOneOrMany('/pvp/games', gameIds);
},
/**
* Gets WVW Matches.
*
* @param {Int} worldId
* A world who's id is participating in the match.
* @param {String|Array} matchIds
* String match id, or an array of match ids.
*
* @return {Promise}
*/
getWVWMatches: function (worldId, matchIds) {
return this.getOneOrMany('/wvw/matches', matchIds, false, { "world": worldId });
},
/**
* Gets WVW Objectives
*
* @param {String|Array} objectiveIds
* <optional> Either an objectiveId or array of ids.
*
* @return {Promise}
*/
getWVWObjectives: function (objectiveIds) {
return this.getOneOrMany('/wvw/objectives', objectiveIds);
},
/**
* Returns info about a given token. This token must be first set via
* this.setAPIKey.
*
* @return {Promise}
*/
getTokenInfo: function () {
return this.callAPI('/tokeninfo');
},
/**
* Gets the wallet information associated with the current API token.
* @param {boolean} handleCurrencyTranslation
* <optional> If true, will automatically get the currency information.
* Otherwise you'll just get currency id and value.
* @return Promise
*/
getWallet: function (handleCurrencyTranslation) {
if (!handleCurrencyTranslation) {
return this.callAPI('/account/wallet');
}
var that = this;
return this.callAPI('/account/wallet').then(function (res) {
var walletCurrencies = res;
var lookupIds = [];
for (var i = 0, len = res.length; i < len; i++) {
lookupIds.push(res[i].id);
}
return that.getCurrencies(lookupIds).then(function (res) {
for (var i = 0, len = res.length; i < len; i++) {
for (var x = 0, xlen = walletCurrencies.length; x < xlen; x++) {
if (res[i].id == walletCurrencies[x].id) {
objAssign(walletCurrencies[x], res[i]);
break;
}
}
}
return walletCurrencies;
});
});
},
/**
* Loads Masteries
*
* @param {Array<number>} masteryIds
*
* @returns {Promise}
*/
getMasteries: function (masteryIds) {
return this.getOneOrMany('/masteries', masteryIds, false);
},
/**
* Loads Finishers
*
* @param {Array<number>} finisherIds
*
* @returns {Promise}
*/
getFinishers: function (finisherIds) {
return this.getOneOrMany('/finishers', finisherIds, false);
},
/**
* Gets Dye Colors. If no ids are passed, all possible ids are returned.
*
* @param {int|Array} colorIds
* <optional> An int or array of color ids.
* @return {Promise}
*/
getColors: function (colorIds) {
return this.getOneOrMany('/colors', colorIds, false);
},
/**
* Returns the continents list
* @return Promise
*/
getContinents: function () {
return this.callAPI('/continents');
},
/**
* Returns commonly requested files.
*
* @param {String|Array} fileIds
* Either a string file id or an array of ids.
*
* @return {Promise}
*/
getFiles: function (fileIds) {
return this.getOneOrMany('/files', fileIds, false);
},
/**
* Returns the current build id.
*
* @return {Promise}
*/
getBuildId: function () {
return this.callAPI('/build');
},
/**
* Returns a list of Quaggans!
*
* @param {String|Array} quagganIds
* <optional> a String quaggan id or an array of quaggan ids.
*
* @return {Promise}
*/
getQuaggans: function (quagganIds) {
return this.getOneOrMany('/quaggans', quagganIds, false);
},
/**
* Gets a list of items. If no ids are passed, you'll get an array of all ids back.
*
* @param {int|array} itemIds
* <optional> Either an int itemId or an array of itemIds.
*
* @return Promise
*/
getItems: function (itemIds) {
return this.getOneOrMany('/items', itemIds, false);
},
/**
* Gets materials. If no ids are passed, this will return an array of all
* possible material ids.
* @param {int|array} materialIds
* <optional> Either an int materialId or an array of materialIds
* @return Promise
*/
getMaterials: function (materialIds) {
return this.getOneOrMany('/materials', materialIds, false);
},
/**
* Gets minis. If no ids are passed, this will return an array of all
* possible mini ids.
* @param {Int|Array} miniIds
* <optional> Either an int or an array of mini ids.
* @return {Promise}
*/
getMinis: function (miniIds) {
return this.getOneOrMany('/minis', miniIds, false);
},
/**
* Gets recipes. If no ids are passed, this will return an array of all
* possible recipe ids.
* @param {int|array} recipeIds
* <optional> Either an int recipeId or an array of recipeIds
* @return Promise
*/
getRecipes: function (recipeIds) {
return this.getOneOrMany('/recipes', recipeIds, false);
},
/**
* Searches for recipes which match an item id. inputItem and outputItem
* are mutually exclusive.
*
* @param {Int} inputItem
* Search for recipes containing this item.
* @param {Int} outputItem
* Search for recipes which will produce this item.
* @return {Promise}
*/
searchRecipes: function (inputItem, outputItem) {
if (inputItem && outputItem) {
return new Promise(function (fulfill, reject) {
reject('inputItem and outputItem are mutually exclusive options');
});
}
var options = _.omit({ 'input': inputItem, 'output': outputItem }, function (v, k) {
if (!v) {
return true;
}
});
return this.callAPI('/recipes/search', options, false);
},
/**
* Gets Skins. If no ids are passed, this returns an array of all skins.
*
* @param {Int|Array} skinIds
* <optional> Either an int skinId or an array of skin ids
* @return {Promise}
*/
getSkins: function (skinIds) {
return this.getOneOrMany('/skins', skinIds, false);
},
/**
* Gets currencies. If no ids are passed, this will return an array of all
* possible material ids.
* @param {int|array} currencyIds
* <optional> Either an int currencyId or an array of currenciyIds
* @return Promise
*/
getCurrencies: function (currencyIds) {
return this.getOneOrMany('/currencies', currencyIds, false);
},
/**
* Gets achievements. If no ids are passed, this will return an array of all
* possible achievement ids.
* @param {int|array} achievementIds
* <optional> Either an int achievementId or an array of achievementIds
* @return Promise
*/
getAchievements: function (achievementIds) {
return this.getOneOrMany('/achievements', achievementIds, false, { "lang": this.getLang() });
},
/**
* Gets achievement groups. Examples being "Heart of Thorns, Central Tyria"
*
* @param {String|Array} groupIds
* <optional> Either a groupId or array of group ids. Note that for this, ids
* are guids.
*
* @return {Promise}
*/
getAchievementGroups: function (groupIds) {
return this.getOneOrMany('achievements/groups', groupIds, false);
},
/**
* Gets achievement categories. Examples being "Slayer, Hero of Tyria"
*
* @param {Int|Array} categoryIds
* <optional> Either an int or an array of category ids.
*
* @return {Promise}
*/
getAchievementCategories: function (categoryIds) {
return this.getOneOrMany('achievements/categories', categoryIds, false);
},
/**
* Gets daily achievements. This will return an object with the various achievement
* categories as keys. The current keys are "wvw", "pvp", and "pve"
*
* @param {Boolean} autoTranslate
*
* @return Promise
*/
getDailyAchievements: function (autoTranslate) {
var p = this.callAPI('/achievements/daily', { "lang": this.getLang() }, false);
var that = this;
if (!autoTranslate) {
return p;
}
function getDeeperItemInfo(key, items) {
return that.getDeeperInfo(that.getAchievements, items, 100).then(function (res) {
var ob = {};
ob[key] = res;
return ob;
});
}
return p.then(function (achievements) {
var promises = [];
for (var i in achievements) {
if (!achievements.hasOwnProperty) {
continue;
}
promises.push(function (key) { return getDeeperItemInfo(i, achievements[i]) }(i));
}
return Promise.all(promises).then(function (promises) {
return promises.reduce(function (acc, item) {
return Object.assign(acc, item);
}, {});
});
});
},
/**
* Gets skills. If no ids are passed, this will return an array of all possible
* skills.
*
* @param {int|array} skillIds
* <optional> Either an int skillId or an array of skillIds.
* @return {Promise}
*/
getSkills: function (skillIds) {
return this.getOneOrMany('/skills', skillIds, false);
},
/**
* [Helper Method] Gets skills for a particular profession.
*
* @param {String} profession
* The string key to match profession on.
* @param {String} skillType
* <optional> The type of skills to return ("Weapon", "Heal", etc.)
* @param {Boolean} includeBundles
* <optional> Whether or not to include bundles as part of the return list.
* This option is meaningless if skillType == 'Bundle'
* @return {Promise}
*/
getProfessionSkills: function (profession, skillType, includeBundles) {
var that = this;
if (typeof includeBundles == 'undefined') {
includeBundles = false;
}
return this.getSkills().then(function (skillIds) {
// Break skills into chunks.
var chunks = chunk(skillIds, 50);
var promises = [];
chunks.forEach(function (c) {
promises.push(that.getSkills(c).then(function (skills) {
var profSkills = [];
return skills.filter(function (skill) {
if (skill.professions.indexOf(profession) == -1) {
return false;
}
if (!includeBundles && skill.type == 'Bundle') {
return false;
}
if (skillType && skill.type == skillType) {
return true;
} else if (skillType) {
return false;
}
return true;
});
}));
});
return Promise.all(promises).then(function (results) {
return [].concat.apply([], results);
});
});
},
/**
* Gets Specializations. If no ids are passed this will return an array of all
* ids.
*
* @param {Int|Array} specializationIds
* <optional> Either an int specialization id or an array of them.
* @return {Promise}
*/
getSpecializations: function (specializationIds) {
return this.getOneOrMany('/specializations', specializationIds, false);
},
/**
* Gets a list of profession specializations.
*
* @param {String} profession
* Profession name. Remember to uppercase the first letter.
*
* @return {Promise}
*/
getProfessionSpecializations: function (profession) {
var that = this;
return this.getSpecializations().then(function (specializationIds) {
// Doing this for the inherant chunking.
return that.getDeeperInfo(that.getSpecializations, specializationIds);
}).then(function (fullSpecializations) {
var specs = [];
fullSpecializations.forEach(function (spec) {
if (spec.profession !== profession) {
return;
}
specs.push(spec);
});
return specs;
});
},
/**
* Gets a list of traits from the passed ids. If no traitIds are passed
* all trait ids are returned.
*
* @param {Int|Array} traitIds
* <optional> An int or array of trait ids.
*
* @return {Promise}
*/
getTraits: function (traitIds) {
return this.getOneOrMany('/traits', traitIds, false);
},
/**
* Returns the assets required to render emblems.
*
* @param {String} foreOrBack
* Either the string "foregrounds" or "backgrounds"
* @param {Int|Array} assetIds
* <optional> Either an Int or Array assetId
*
* @return {Promise}
*/
getEmblems: function (foreOrBack, assetIds) {
var subpoint = foreOrBack === 'foregrounds' ? 'foregrounds' : 'backgrounds';
return this.getOneOrMany('/emblem/' + subpoint, assetIds);
},
/**
* Gets info about guild permissions (unauthenticated).
*
* @param {String|Array} permissionIds
*
* @return {Promise}
*/
getGuildPermissions: function (permissionIds) {
return this.getOneOrMany('/guild/permissions', permissionIds);
},
/**
* Gets info about guild upgrades (unauthenticated).
*
* @param {Int|Array} upgradeIds
* <optional> Either an int or an array of upgrade ids.
*
* @return {Promise}
*/
getGuildUpgrades: function (upgradeIds) {
return this.getOneOrMany('/guild/upgrades', upgradeIds);
},
/**
* Helper method to convert a list of ids (or objects with an id parameter)
* into more useful information via the passed endpointFunc.
*
* It chunks the array into chunkSize pieces and makes that many api calls
* in parallel.
*
* Usually used for the account calls that don't return full details on equipment,
* for example.
*
* @param {function} endpointFunc
* The function to call to get more details. Must be in the GW2API object
* and must return a promise.
* @param {Array} Items
* An array of items to transform. Either an array of ints or objects.
* @param {Int} chunkSize
* How large each batch call should be. Defaults to 100.
*
* @return {Promise}
*/
getDeeperInfo: function (endpointFunc, items, chunkSize) {
var lookupIds = [];
var promises = [];
var that = this;
if (!chunkSize) {
chunkSize = 100;
}
items.forEach(function (item) {
if (!item) {
// Some endpoints return null, for empty bank slots, for example.
return;
}
if (typeof item == 'number') {
lookupIds.push(item);
return;
}
lookupIds.push(item.id);
});
var chunks = chunk(lookupIds, chunkSize);
chunks.forEach(function (ch) {
promises.push(endpointFunc.call(that, ch));
});
return Promise.all(promises).then(function (results) {
var reses = [].concat.apply([], results);
reses.forEach(function (res) {
for (var i = 0, len = items.length; i < len; i++) {
if (!items[i]) {
continue;
}
if (typeof items[i] == 'number') {
items[i] = { id: items[i] };
}
if (items[i].id === res.id) {
objAssign(items[i], res);
}
}
});
return items;
});
},
/**
* Helper function to do the common endpoint/{id} or ?ids={}
*
* @param string endpoint
* @param mixed ids
* @param boolean requiresAuth
*
* @return Promise
*/
getOneOrMany: function (endpoint, ids, requiresAuth, otherParams) {
var params = {};
if (typeof ids === 'number' || typeof ids === 'string') {
endpoint += '/' + ids;
} else if (Array.isArray(ids)) {
params['ids'] = ids.sort().join(',');
}
if (typeof otherParams === 'object') {
objAssign(params, otherParams);
}
return this.callAPI(endpoint, params, requiresAuth);
},
/**
* Makes a call to the GW2 API.
*
* @param endpoint
* @param params
* @param requiresAuth
*
* @return Promise
*/
callAPI: function (endpoint, params, requiresAuth) {
if (typeof requiresAuth == "undefined") {
requiresAuth = true;
}
if (!params) {
params = {};
}
var options = {
url: this.baseUrl + endpoint
};
if (requiresAuth) {
if (this.useAuthHeader) {
options['headers'] = {
'Authorization': 'Bearer ' + this.getAPIKey()
}
} else {
params['access_token'] = this.getAPIKey();
}
}
options['qs'] = params;
var keys = _.keys(params).sort();
var tmpArr = [];
for (var i = 0, len = keys.length; i < len; i++) {
tmpArr.push(keys[i] + "=" + params[keys[i]]);
}
var keystr = '';
if (tmpArr.length > 0) {
keystr = '?' + tmpArr.join('&');
}
var cacheKey = md5(endpoint + keystr);
var cachedItem;
if (this.cache && (cachedItem = this.storage.getItem(cacheKey))) {
cachedItem = JSON.parse(cachedItem);
return new Promise(function (fulfill, reject) { fulfill(cachedItem); });
}
var that = this;
return new Promise(function (fulfill, reject) {
request.get(options).on('response', function (response) {
var dataStream = '';
response.on('data', function (data) {
dataStream += data;
}).on('end', function () {
var data = JSON.parse(dataStream);
if (that.storeInCache) {
that.storage.setItem(cacheKey, dataStream);
}
fulfill(data);
});
})
.on('error', function (error) {
reject(error);
});
});
}
}
module.exports = GW2API;