UNPKG

osrs-json-hiscores

Version:

The Old School Runescape API wrapper that does more!

501 lines (500 loc) 23.7 kB
"use strict"; var __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __generator = (this && this.__generator) || function (thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (g && (g = 0, op[0] && (_ = 0)), _) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } }; Object.defineProperty(exports, "__esModule", { value: true }); exports.getActivityPage = exports.getSkillPage = exports.getStatsByGamemode = exports.getStats = exports.parseStats = exports.parseJsonStats = exports.getRSNFormat = exports.getOfficialStats = void 0; var axios_1 = require("axios"); var jsdom_1 = require("jsdom"); var utils_1 = require("./utils"); /** * Gets a player's stats from the official OSRS JSON endpoint. * * @param rsn Username of the player. * @param mode Gamemode to fetch ranks for. * @param config Optional axios request config object. * @returns Official JSON stats object. */ function getOfficialStats(rsn, mode, config) { var _a; if (mode === void 0) { mode = 'main'; } return __awaiter(this, void 0, void 0, function () { var url, response, err_1; return __generator(this, function (_b) { switch (_b.label) { case 0: (0, utils_1.validateRSN)(rsn); url = (0, utils_1.getStatsURL)(mode, rsn, true); _b.label = 1; case 1: _b.trys.push([1, 3, , 4]); return [4 /*yield*/, (0, utils_1.httpGet)(url, config)]; case 2: response = _b.sent(); return [2 /*return*/, response.data]; case 3: err_1 = _b.sent(); if (!axios_1.default.isAxiosError(err_1)) throw err_1; if (((_a = err_1.response) === null || _a === void 0 ? void 0 : _a.status) === 404) throw new utils_1.PlayerNotFoundError(); throw new utils_1.HiScoresError(); case 4: return [2 /*return*/]; } }); }); } exports.getOfficialStats = getOfficialStats; /** * Screen scrapes the hiscores to get the formatted rsn of a player. * * @param rsn Username of the player. * @param config Optional axios request config object. * @returns Formatted version of the rsn. */ function getRSNFormat(rsn, config, mode) { var _a, _b; if (mode === void 0) { mode = 'main'; } return __awaiter(this, void 0, void 0, function () { var url, response, dom, row, innerHTML, _c; return __generator(this, function (_d) { switch (_d.label) { case 0: (0, utils_1.validateRSN)(rsn); url = (0, utils_1.getPlayerTableURL)(mode, rsn); _d.label = 1; case 1: _d.trys.push([1, 3, , 4]); return [4 /*yield*/, (0, utils_1.httpGet)(url, config)]; case 2: response = _d.sent(); dom = new jsdom_1.JSDOM(response.data); row = dom.window.document.querySelector('tr.personal-hiscores__row.personal-hiscores__row--type-highlight'); if (row) { innerHTML = (row !== null && row !== void 0 ? row : {}).innerHTML; return [2 /*return*/, ((_b = (_a = innerHTML === null || innerHTML === void 0 ? void 0 : innerHTML.match(new RegExp(rsn.replace(utils_1.WHITESPACE_REGEX, utils_1.WHITESPACE_REGEX_STRING), 'gi'))) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : rsn)]; } return [3 /*break*/, 4]; case 3: _c = _d.sent(); throw new utils_1.HiScoresError(); case 4: throw new utils_1.PlayerNotFoundError(); } }); }); } exports.getRSNFormat = getRSNFormat; /** * Parses official JSON object of raw stats and returns a stats object. * * @param csv Raw JSON from the official OSRS API. * @returns Parsed stats object. */ function parseJsonStats(json) { var getActivity = function (formattedName) { var _a, _b; var hiscoresActivity = json.activities.find( // We must match on name here since id is not guaranteed to be the same between updates function (_a) { var name = _a.name; return name.toLowerCase() === formattedName.toLowerCase(); }); return { rank: (_a = hiscoresActivity === null || hiscoresActivity === void 0 ? void 0 : hiscoresActivity.rank) !== null && _a !== void 0 ? _a : -1, score: (_b = hiscoresActivity === null || hiscoresActivity === void 0 ? void 0 : hiscoresActivity.score) !== null && _b !== void 0 ? _b : -1 }; }; var reduceActivity = function (keys, formattedNames) { return keys.reduce(function (reducer, key) { var _a; return (__assign(__assign({}, reducer), (_a = {}, _a[key] = getActivity(formattedNames[key]), _a))); }, {}); }; var skills = utils_1.SKILLS.reduce(function (skillsObject, skillName) { var _a; var _b, _c, _d; var hiscoresSkill = json.skills.find( // We must match on name here since id is not guaranteed to be the same between updates function (_a) { var name = _a.name; return name.toLowerCase() === utils_1.FORMATTED_SKILL_NAMES[skillName].toLowerCase(); }); return __assign(__assign({}, skillsObject), (_a = {}, _a[skillName] = { rank: (_b = hiscoresSkill === null || hiscoresSkill === void 0 ? void 0 : hiscoresSkill.rank) !== null && _b !== void 0 ? _b : -1, level: (_c = hiscoresSkill === null || hiscoresSkill === void 0 ? void 0 : hiscoresSkill.level) !== null && _c !== void 0 ? _c : -1, xp: (_d = hiscoresSkill === null || hiscoresSkill === void 0 ? void 0 : hiscoresSkill.xp) !== null && _d !== void 0 ? _d : -1 }, _a)); }, {}); var bountyHunter = reduceActivity(utils_1.BH_MODES, utils_1.FORMATTED_BH_NAMES); var clues = reduceActivity(utils_1.CLUES, utils_1.FORMATTED_CLUE_NAMES); var bosses = reduceActivity(utils_1.BOSSES, utils_1.FORMATTED_BOSS_NAMES); var leaguePoints = getActivity(utils_1.FORMATTED_LEAGUE_POINTS); var deadmanPoints = getActivity(utils_1.FORMATTED_DEADMAN_POINTS); var lastManStanding = getActivity(utils_1.FORMATTED_LMS); var pvpArena = getActivity(utils_1.FORMATTED_PVP_ARENA); var soulWarsZeal = getActivity(utils_1.FORMATTED_SOUL_WARS); var riftsClosed = getActivity(utils_1.FORMATTED_RIFTS_CLOSED); var colosseumGlory = getActivity(utils_1.FORMATTED_COLOSSEUM_GLORY); var collectionsLogged = getActivity(utils_1.FORMATTED_COLLECTIONS_LOGGED); var stats = { skills: skills, leaguePoints: leaguePoints, deadmanPoints: deadmanPoints, bountyHunter: bountyHunter, lastManStanding: lastManStanding, pvpArena: pvpArena, soulWarsZeal: soulWarsZeal, riftsClosed: riftsClosed, colosseumGlory: colosseumGlory, collectionsLogged: collectionsLogged, clues: clues, bosses: bosses }; return stats; } exports.parseJsonStats = parseJsonStats; /** * Parses CSV string of raw stats and returns a stats object. * * @param csv Raw CSV from the official OSRS API. * @returns Parsed stats object. */ function parseStats(csv) { var splitCSV = csv .split('\n') .filter(function (entry) { return !!entry; }) .map(function (stat) { return stat.split(','); }); if (splitCSV.length !== utils_1.SKILLS.length + utils_1.ACTIVITIES.length) { throw new utils_1.InvalidFormatError(); } var skillObjects = splitCSV .filter(function (stat) { return stat.length === 3; }) .map(function (stat) { var rank = stat[0], level = stat[1], xp = stat[2]; var skill = { rank: parseInt(rank, 10), level: parseInt(level, 10), xp: parseInt(xp, 10) }; return skill; }); var activityObjects = splitCSV .filter(function (stat) { return stat.length === 2; }) .map(function (stat) { var rank = stat[0], score = stat[1]; var activity = { rank: parseInt(rank, 10), score: parseInt(score, 10) }; return activity; }); var _a = activityObjects.splice(0, 2), leaguePoints = _a[0], deadmanPoints = _a[1]; var bhObjects = activityObjects.splice(0, utils_1.BH_MODES.length); var clueObjects = activityObjects.splice(0, utils_1.CLUES.length); var _b = activityObjects.splice(0, 6), lastManStanding = _b[0], pvpArena = _b[1], soulWarsZeal = _b[2], riftsClosed = _b[3], colosseumGlory = _b[4], collectionsLogged = _b[5]; var bossObjects = activityObjects.splice(0, utils_1.BOSSES.length); var skills = skillObjects.reduce(function (prev, curr, index) { var newSkills = __assign({}, prev); newSkills[utils_1.SKILLS[index]] = curr; return newSkills; }, {}); var bountyHunter = bhObjects.reduce(function (prev, curr, index) { var newBH = __assign({}, prev); newBH[utils_1.BH_MODES[index]] = curr; return newBH; }, {}); var clues = clueObjects.reduce(function (prev, curr, index) { var newClues = __assign({}, prev); newClues[utils_1.CLUES[index]] = curr; return newClues; }, {}); var bosses = bossObjects.reduce(function (prev, curr, index) { var newBosses = __assign({}, prev); newBosses[utils_1.BOSSES[index]] = curr; return newBosses; }, {}); var stats = { skills: skills, leaguePoints: leaguePoints, deadmanPoints: deadmanPoints, bountyHunter: bountyHunter, lastManStanding: lastManStanding, pvpArena: pvpArena, soulWarsZeal: soulWarsZeal, riftsClosed: riftsClosed, colosseumGlory: colosseumGlory, collectionsLogged: collectionsLogged, clues: clues, bosses: bosses }; return stats; } exports.parseStats = parseStats; /** * Fetches stats from the OSRS API and consolidates the info into a player object. * * **Note:** This function will make up to 5 separate network requests. * As such, it is highly subject to the performance of the official OSRS API. * * @param rsn Username of the player. * @returns Player object. */ function getStats(rsn, options) { var _a, _b, _c, _d; return __awaiter(this, void 0, void 0, function () { var otherGamemodes, shouldGetFormattedRsn, main, getModeStats, formattedName, _e, player, iron, hc, ult; var _this = this; return __generator(this, function (_f) { switch (_f.label) { case 0: (0, utils_1.validateRSN)(rsn); otherGamemodes = (_a = options === null || options === void 0 ? void 0 : options.otherGamemodes) !== null && _a !== void 0 ? _a : [ 'ironman', 'hardcore', 'ultimate' ]; shouldGetFormattedRsn = (_b = options === null || options === void 0 ? void 0 : options.shouldGetFormattedRsn) !== null && _b !== void 0 ? _b : true; return [4 /*yield*/, getOfficialStats(rsn, 'main', (_c = options === null || options === void 0 ? void 0 : options.axiosConfigs) === null || _c === void 0 ? void 0 : _c.main)]; case 1: main = _f.sent(); getModeStats = function (mode) { return __awaiter(_this, void 0, void 0, function () { var _a; return __generator(this, function (_b) { return [2 /*return*/, otherGamemodes.includes(mode) ? getOfficialStats(rsn, mode, (_a = options === null || options === void 0 ? void 0 : options.axiosConfigs) === null || _a === void 0 ? void 0 : _a[mode]).catch(function () { return undefined; }) : undefined]; }); }); }; if (!shouldGetFormattedRsn) return [3 /*break*/, 3]; return [4 /*yield*/, getRSNFormat(rsn, (_d = options === null || options === void 0 ? void 0 : options.axiosConfigs) === null || _d === void 0 ? void 0 : _d.rsn).catch(function () { return undefined; })]; case 2: _e = _f.sent(); return [3 /*break*/, 4]; case 3: _e = undefined; _f.label = 4; case 4: formattedName = _e; player = { name: formattedName !== null && formattedName !== void 0 ? formattedName : rsn, mode: 'main', dead: false, deulted: false, deironed: false }; player.main = parseJsonStats(main); return [4 /*yield*/, getModeStats('ironman')]; case 5: iron = _f.sent(); if (!iron) return [3 /*break*/, 8]; player.ironman = parseJsonStats(iron); return [4 /*yield*/, getModeStats('hardcore')]; case 6: hc = _f.sent(); return [4 /*yield*/, getModeStats('ultimate')]; case 7: ult = _f.sent(); if (hc) { player.mode = 'hardcore'; player.hardcore = parseJsonStats(hc); if (player.ironman.skills.overall.xp !== player.hardcore.skills.overall.xp) { player.dead = true; player.mode = 'ironman'; } if (player.main.skills.overall.xp !== player.ironman.skills.overall.xp) { player.deironed = true; player.mode = 'main'; } } else if (ult) { player.mode = 'ultimate'; player.ultimate = parseJsonStats(ult); if (player.ironman.skills.overall.xp !== player.ultimate.skills.overall.xp) { player.deulted = true; player.mode = 'ironman'; } if (player.main.skills.overall.xp !== player.ironman.skills.overall.xp) { player.deironed = true; player.mode = 'main'; } } else { player.mode = 'ironman'; if (player.main.skills.overall.xp !== player.ironman.skills.overall.xp) { player.deironed = true; player.mode = 'main'; } } _f.label = 8; case 8: return [2 /*return*/, player]; } }); }); } exports.getStats = getStats; /** * Fetches stats from the OSRS API and returns them as an object. * * @param rsn Username of the player. * @param mode Gamemode to fetch ranks for. * @param config Optional axios request config object. * @returns Stats object. */ function getStatsByGamemode(rsn, mode, config) { if (mode === void 0) { mode = 'main'; } return __awaiter(this, void 0, void 0, function () { var response, stats; return __generator(this, function (_a) { switch (_a.label) { case 0: (0, utils_1.validateRSN)(rsn); if (!utils_1.GAMEMODES.includes(mode)) { throw Error('Invalid game mode'); } return [4 /*yield*/, getOfficialStats(rsn, mode, config)]; case 1: response = _a.sent(); stats = parseJsonStats(response); return [2 /*return*/, stats]; } }); }); } exports.getStatsByGamemode = getStatsByGamemode; function getSkillPage(skill, mode, page, config) { if (mode === void 0) { mode = 'main'; } if (page === void 0) { page = 1; } return __awaiter(this, void 0, void 0, function () { var url, response, dom, playersHTML, players; return __generator(this, function (_a) { switch (_a.label) { case 0: if (!utils_1.GAMEMODES.includes(mode)) { throw Error('Invalid game mode'); } else if (!Number.isInteger(page) || page < 1) { throw Error('Page must be an integer greater than 0'); } else if (!utils_1.SKILLS.includes(skill)) { throw Error('Invalid skill'); } url = (0, utils_1.getSkillPageURL)(mode, skill, page); return [4 /*yield*/, (0, utils_1.httpGet)(url, config)]; case 1: response = _a.sent(); dom = new jsdom_1.JSDOM(response.data); playersHTML = dom.window.document.querySelectorAll('tr.personal-hiscores__row'); players = []; playersHTML.forEach(function (row) { // Omit first cell (pre-sailing link) var _a = Array.from(row.cells), rankCell = _a[1], nameCell = _a[2], levelCell = _a[3], xpCell = _a[4]; var isDead = !!nameCell.querySelector('img'); var nameElement = nameCell.querySelector('a'); players.push({ name: (0, utils_1.rsnFromElement)(nameElement), rank: (0, utils_1.numberFromElement)(rankCell), level: (0, utils_1.numberFromElement)(levelCell), xp: (0, utils_1.numberFromElement)(xpCell), dead: isDead }); }); return [2 /*return*/, players]; } }); }); } exports.getSkillPage = getSkillPage; /** * Screen scrapes a hiscores page of an activity or boss and returns an array of up to 25 players. * * @param activity Name of the activity or boss to fetch hiscores for. * @param mode Gamemode to fetch ranks for. * @param page Page number. * @param config Optional axios request config object. * @returns Array of `PlayerActivityRow` objects. */ function getActivityPage(activity, mode, page, config) { if (mode === void 0) { mode = 'main'; } if (page === void 0) { page = 1; } return __awaiter(this, void 0, void 0, function () { var url, response, dom, playersHTML, players; return __generator(this, function (_a) { switch (_a.label) { case 0: if (!utils_1.GAMEMODES.includes(mode)) { throw Error('Invalid game mode'); } else if (!Number.isInteger(page) || page < 1) { throw Error('Page must be an integer greater than 0'); } else if (!utils_1.ACTIVITIES.includes(activity)) { throw Error('Invalid activity'); } url = (0, utils_1.getActivityPageURL)(mode, activity, page); return [4 /*yield*/, (0, utils_1.httpGet)(url, config)]; case 1: response = _a.sent(); dom = new jsdom_1.JSDOM(response.data); playersHTML = dom.window.document.querySelectorAll('tr.personal-hiscores__row'); players = []; playersHTML.forEach(function (row) { var _a = Array.from(row.cells), rankCell = _a[0], nameCell = _a[1], scoreCell = _a[2]; var isDead = !!nameCell.querySelector('img'); var nameElement = nameCell.querySelector('a'); players.push({ name: (0, utils_1.rsnFromElement)(nameElement), rank: (0, utils_1.numberFromElement)(rankCell), score: (0, utils_1.numberFromElement)(scoreCell), dead: isDead }); }); return [2 /*return*/, players]; } }); }); } exports.getActivityPage = getActivityPage;