Compare commits

...

26 Commits

Author SHA1 Message Date
maxswa
f80b417121 v2.13.0 2023-04-12 12:53:39 -04:00
maxswa
415fe2f02b Update np 2023-04-12 12:50:41 -04:00
Max Swartwout
968799176f Merge pull request #70 from maxswa/wilderness-boss-variants
Add wilderness boss variants.
2023-04-12 12:46:11 -04:00
maxswa
f45ae67813 Add wilderness boss variants. 2023-04-12 12:42:37 -04:00
maxswa
7724636b40 v2.12.1 2023-03-19 23:33:33 -04:00
maxswa
3954b04acc Add publishConfig to package.json 2023-03-19 23:33:03 -04:00
Max Swartwout
ee7389455c Merge pull request #68 from maxswa/add-options
Add `validateRSN`, `GetStatsOptions`
2023-03-19 23:28:34 -04:00
maxswa
54c11635e0 Add GetStatsOptions for getStats 2023-03-19 23:14:10 -04:00
maxswa
dc799cf363 Add validateRSN helper function. 2023-03-19 22:38:45 -04:00
maxswa
d4c7aaff2a v2.12.0 2023-02-19 18:31:43 -05:00
Max Swartwout
03c089ebff Merge pull request #66 from maxswa/account-builds
Add new `Gamemode`s
2023-02-19 18:31:00 -05:00
Max Swartwout
96641d875a Merge pull request #65 from maxswa/dependabot/npm_and_yarn/http-cache-semantics-4.1.1
Bump http-cache-semantics from 4.1.0 to 4.1.1
2023-02-19 18:28:33 -05:00
maxswa
b2bdd3d2ce Add new Gamemodes 2023-02-19 18:27:56 -05:00
dependabot[bot]
a4ddfaf199 Bump http-cache-semantics from 4.1.0 to 4.1.1
Bumps [http-cache-semantics](https://github.com/kornelski/http-cache-semantics) from 4.1.0 to 4.1.1.
- [Release notes](https://github.com/kornelski/http-cache-semantics/releases)
- [Commits](https://github.com/kornelski/http-cache-semantics/compare/v4.1.0...v4.1.1)

---
updated-dependencies:
- dependency-name: http-cache-semantics
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-03 04:54:08 +00:00
maxswa
efbcdfc709 v2.11.0 2023-01-13 11:21:51 -05:00
Max Swartwout
fe913aba1f Merge pull request #64 from davidvorona/add-muspah
Add phantom muspah
2023-01-12 15:30:21 -05:00
Max Swartwout
1497653bab Merge pull request #63 from maxswa/dependabot/npm_and_yarn/json5-1.0.2
Bump json5 from 1.0.1 to 1.0.2
2023-01-12 15:29:38 -05:00
David Vorona
f76de38eb1 Add phantom muspah 2023-01-11 11:45:59 -08:00
dependabot[bot]
2f1d10b309 Bump json5 from 1.0.1 to 1.0.2
Bumps [json5](https://github.com/json5/json5) from 1.0.1 to 1.0.2.
- [Release notes](https://github.com/json5/json5/releases)
- [Changelog](https://github.com/json5/json5/blob/main/CHANGELOG.md)
- [Commits](https://github.com/json5/json5/compare/v1.0.1...v1.0.2)

---
updated-dependencies:
- dependency-name: json5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-06 22:36:14 +00:00
Max Swartwout
f08424cb81 Update build badge in README 2022-12-19 12:24:03 -05:00
maxswa
69088db3b6 v2.10.2 2022-12-15 23:24:59 +00:00
Max Swartwout
9cc89735cd Merge pull request #61 from maxswa/get-rsn-format-highlight-row
Fix `getRSNFormat`
2022-12-15 18:15:34 -05:00
Max Swartwout
a126ae4fc7 Merge pull request #58 from maxswa/dependabot/npm_and_yarn/minimatch-3.1.2
Bump minimatch from 3.0.4 to 3.1.2
2022-12-15 18:11:34 -05:00
Max Swartwout
52f3df1838 Merge pull request #59 from maxswa/dependabot/npm_and_yarn/qs-6.5.3
Bump qs from 6.5.2 to 6.5.3
2022-12-15 18:11:23 -05:00
dependabot[bot]
e2ea2fe33d Bump qs from 6.5.2 to 6.5.3
Bumps [qs](https://github.com/ljharb/qs) from 6.5.2 to 6.5.3.
- [Release notes](https://github.com/ljharb/qs/releases)
- [Changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ljharb/qs/compare/v6.5.2...v6.5.3)

---
updated-dependencies:
- dependency-name: qs
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-12-09 06:28:03 +00:00
dependabot[bot]
240628d9cc Bump minimatch from 3.0.4 to 3.1.2
Bumps [minimatch](https://github.com/isaacs/minimatch) from 3.0.4 to 3.1.2.
- [Release notes](https://github.com/isaacs/minimatch/releases)
- [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/minimatch/compare/v3.0.4...v3.1.2)

---
updated-dependencies:
- dependency-name: minimatch
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-12-04 19:14:24 +00:00
9 changed files with 549 additions and 314 deletions

View File

@@ -3,7 +3,7 @@
[![npm](https://img.shields.io/npm/v/osrs-json-hiscores.svg?style=flat-square)](https://www.npmjs.com/package/osrs-json-hiscores)
[![downloads](https://img.shields.io/npm/dm/osrs-json-hiscores.svg?style=flat-square)](https://npm-stat.com/charts.html?package=osrs-json-hiscores)
[![types](https://img.shields.io/npm/types/osrs-json-hiscores.svg?style=flat-square)](https://github.com/maxswa/osrs-json-hiscores/blob/master/src/types.ts)
[![build](https://img.shields.io/github/workflow/status/maxswa/osrs-json-hiscores/CI?style=flat-square)](https://github.com/maxswa/osrs-json-hiscores/actions/workflows/main.yml?query=branch%3Amain)
[![build](https://img.shields.io/github/actions/workflow/status/maxswa/osrs-json-hiscores/main.yml?style=flat-square&branch=main)](https://github.com/maxswa/osrs-json-hiscores/actions/workflows/main.yml?query=branch%3Amain)
**The Old School Runescape API wrapper that does more!**
@@ -125,9 +125,11 @@ Activities consist of all levels of clue scrolls as well as minigames and bosses
| --------------------------------- | :----------------------------: |
| Abyssal Sire | `abyssalSire` |
| Alchemical Hydra | `alchemicalHydra` |
| Artio | `artio` |
| Barrows Chests | `barrows` |
| Bryophyta | `bryophyta` |
| Callisto | `callisto` |
| Cal'varion | `calvarion` |
| Cerberus | `cerberus` |
| Chambers Of Xeric | `chambersOfXeric` |
| Chambers Of Xeric: Challenge Mode | `chambersOfXericChallengeMode` |
@@ -154,9 +156,11 @@ Activities consist of all levels of clue scrolls as well as minigames and bosses
| The Nightmare of Ashihama | `nightmare` |
| Phosani's Nightmare | `phosanisNightmare` |
| Obor | `obor` |
| Phantom Muspah | `phantomMuspah` |
| Sarachnis | `sarachnis` |
| Scorpia | `scorpia` |
| Skotizo | `skotizo` |
| Spindel | `spindel` |
| Tempoross | `tempoross` |
| The Gauntlet | `gauntlet` |
| The Corrupted Gauntlet | `corruptedGauntlet` |

View File

@@ -92,9 +92,11 @@ test('Parse CSV to json', () => {
25,1110
382,2780
944,3000
704,892
1981,1452
4981,23
888,1046
444,792
613,4856
102,4038
156,334
@@ -121,9 +123,11 @@ test('Parse CSV to json', () => {
3,22666
2,84
26,323
44,6233
201,1101
82,3404
5085,61
678,903
18823,23
63,375
2870,6
@@ -189,9 +193,11 @@ test('Parse CSV to json', () => {
bosses: {
abyssalSire: { rank: 382, score: 2780 },
alchemicalHydra: { rank: 944, score: 3000 },
artio: { rank: 704, score: 892 },
barrows: { rank: 1981, score: 1452 },
bryophyta: { rank: 4981, score: 23 },
callisto: { rank: 888, score: 1046 },
calvarion: { rank: 444, score: 792 },
cerberus: { rank: 613, score: 4856 },
chambersOfXeric: { rank: 102, score: 4038 },
chambersOfXericChallengeMode: { rank: 156, score: 334 },
@@ -218,9 +224,11 @@ test('Parse CSV to json', () => {
nightmare: { rank: 3, score: 22666 },
phosanisNightmare: { rank: 2, score: 84 },
obor: { rank: 26, score: 323 },
phantomMuspah: { rank: 44, score: 6233 },
sarachnis: { rank: 201, score: 1101 },
scorpia: { rank: 82, score: 3404 },
skotizo: { rank: 5085, score: 61 },
spindel: { rank: 678, score: 903 },
tempoross: { rank: 18823, score: 23 },
gauntlet: { rank: 63, score: 375 },
corruptedGauntlet: { rank: 2870, score: 6 },
@@ -503,3 +511,50 @@ test('Get stats by gamemode', async () => {
expect.assertions(2);
});
describe('Get stats options', () => {
const rsn = 'player';
let axiosMock: jest.Mock;
beforeEach(() => {
axios.get = jest.fn(
(url) =>
new Promise<any>((resolve) =>
resolve(
url === getPlayerTableURL('main', rsn)
? { data: lynxTitanNamePage }
: { status: 200, data: lynxTitanStats }
)
)
);
axiosMock = axios.get as any;
axiosMock.mockClear();
});
it('fetches all gamemodes and formatted RSN when no options provided', async () => {
await getStats(rsn);
expect(axiosMock.mock.calls.map((val) => val[0])).toEqual([
getStatsURL('main', rsn),
getPlayerTableURL('main', rsn),
getStatsURL('ironman', rsn),
getStatsURL('hardcore', rsn),
getStatsURL('ultimate', rsn)
]);
});
it('skips fetching formatted RSN when option is provided', async () => {
await getStats(rsn, { shouldGetFormattedRsn: false });
expect(
axiosMock.mock.calls.some(
(val) => val[0] === getPlayerTableURL('main', rsn)
)
).toBeFalsy();
});
it('skips fetching game mode when option is provided', async () => {
await getStats(rsn, {
otherGamemodes: ['ironman', 'ultimate']
});
expect(
axiosMock.mock.calls.some(
(val) => val[0] === getStatsURL('hardcore', rsn)
)
).toBeFalsy();
});
});

View File

@@ -85,4 +85,8 @@
-1,-1
-1,-1
-1,-1
-1,-1
-1,-1
-1,-1
-1,-1
-1,-1
Can't render this file because it has a wrong number of fields in line 25.

View File

@@ -1,6 +1,6 @@
{
"name": "osrs-json-hiscores",
"version": "2.10.1",
"version": "2.13.0",
"description": "The Old School Runescape API wrapper that does more!",
"main": "lib/index.js",
"types": "lib/index.d.ts",
@@ -30,6 +30,9 @@
"stats",
"skills"
],
"publishConfig": {
"registry": "https://registry.npmjs.org"
},
"author": "maxswa",
"license": "ISC",
"bugs": {
@@ -106,7 +109,7 @@
"husky": "^5.2.0",
"jest": "^26.6.3",
"lint-staged": "^10.5.4",
"np": "6.5.0",
"np": "^7.7.0",
"prettier": "^2.2.1",
"ts-jest": "^26.5.4",
"typescript": "^4.2.3"

View File

@@ -1,4 +1,5 @@
import { JSDOM } from 'jsdom';
import { AxiosResponse } from 'axios';
import { BinaryData, JSDOM } from 'jsdom';
import {
Player,
Activity,
@@ -12,7 +13,8 @@ import {
PlayerSkillRow,
ActivityName,
PlayerActivityRow,
Bosses
Bosses,
GetStatsOptions
} from './types';
import {
getStatsURL,
@@ -28,7 +30,8 @@ import {
getActivityPageURL,
httpGet,
BOSSES,
INVALID_FORMAT_ERROR
INVALID_FORMAT_ERROR,
validateRSN
} from './utils';
/**
@@ -38,17 +41,13 @@ import {
* @returns Formatted version of the rsn.
*/
export async function getRSNFormat(rsn: string): Promise<string> {
if (typeof rsn !== 'string') {
throw Error('RSN must be a string');
} else if (!/^[a-zA-Z0-9 _-]+$/.test(rsn)) {
throw Error('RSN contains invalid character');
} else if (rsn.length > 12 || rsn.length < 1) {
throw Error('RSN must be between 1 and 12 characters');
}
validateRSN(rsn);
const url = getPlayerTableURL('main', rsn);
try {
const response = await httpGet(url);
const response = await httpGet<string | Buffer | BinaryData | undefined>(
url
);
const dom = new JSDOM(response.data);
const anchor = dom.window.document.querySelector(
'.personal-hiscores__row.personal-hiscores__row--type-highlight a'
@@ -163,25 +162,36 @@ export function parseStats(csv: string): Stats {
* @param rsn Username of the player.
* @returns Player object.
*/
export async function getStats(rsn: string): Promise<Player> {
if (typeof rsn !== 'string') {
throw Error('RSN must be a string');
} else if (!/^[a-zA-Z0-9 _-]+$/.test(rsn)) {
throw Error('RSN contains invalid character');
} else if (rsn.length > 12 || rsn.length < 1) {
throw Error('RSN must be between 1 and 12 characters');
}
export async function getStats(
rsn: string,
options?: GetStatsOptions
): Promise<Player> {
validateRSN(rsn);
const otherGamemodes = options?.otherGamemodes ?? [
'ironman',
'hardcore',
'ultimate'
];
const shouldGetFormattedRsn = options?.shouldGetFormattedRsn ?? true;
const mainRes = await httpGet(getStatsURL('main', rsn));
const mainRes = await httpGet<string>(getStatsURL('main', rsn));
if (mainRes.status === 200) {
const otherResponses = await Promise.all([
httpGet(getStatsURL('ironman', rsn)).catch((err) => err),
httpGet(getStatsURL('hardcore', rsn)).catch((err) => err),
httpGet(getStatsURL('ultimate', rsn)).catch((err) => err),
getRSNFormat(rsn).catch(() => undefined)
]);
const [ironRes, hcRes, ultRes, formattedName] = otherResponses;
const emptyResponse: AxiosResponse<string> = {
status: 404,
data: '',
statusText: '',
headers: {},
config: {}
};
const getModeStats = async (
mode: Extract<Gamemode, 'ironman' | 'hardcore' | 'ultimate'>
): Promise<AxiosResponse<string>> =>
otherGamemodes.includes(mode)
? httpGet<string>(getStatsURL(mode, rsn)).catch((err) => err)
: emptyResponse;
const formattedName = shouldGetFormattedRsn
? await getRSNFormat(rsn).catch(() => undefined)
: undefined;
const player: Player = {
name: formattedName ?? rsn,
@@ -192,8 +202,11 @@ export async function getStats(rsn: string): Promise<Player> {
};
player.main = parseStats(mainRes.data);
const ironRes = await getModeStats('ironman');
if (ironRes.status === 200) {
player.ironman = parseStats(ironRes.data);
const hcRes = await getModeStats('hardcore');
const ultRes = await getModeStats('ultimate');
if (hcRes.status === 200) {
player.mode = 'hardcore';
player.hardcore = parseStats(hcRes.data);
@@ -251,16 +264,11 @@ export async function getStatsByGamemode(
rsn: string,
mode: Gamemode = 'main'
): Promise<Stats> {
if (typeof rsn !== 'string') {
throw Error('RSN must be a string');
} else if (!/^[a-zA-Z0-9 _-]+$/.test(rsn)) {
throw Error('RSN contains invalid character');
} else if (rsn.length > 12 || rsn.length < 1) {
throw Error('RSN must be between 1 and 12 characters');
} else if (!GAMEMODES.includes(mode)) {
validateRSN(rsn);
if (!GAMEMODES.includes(mode)) {
throw Error('Invalid game mode');
}
const response = await httpGet(getStatsURL(mode, rsn));
const response = await httpGet<string>(getStatsURL(mode, rsn));
if (response.status !== 200) {
throw Error('Player not found');
}
@@ -283,7 +291,7 @@ export async function getSkillPage(
}
const url = getSkillPageURL(mode, skill, page);
const response = await httpGet(url);
const response = await httpGet<string | Buffer | BinaryData | undefined>(url);
const dom = new JSDOM(response.data);
const playersHTML = dom.window.document.querySelectorAll(
'.personal-hiscores__row'
@@ -331,7 +339,7 @@ export async function getActivityPage(
}
const url = getActivityPageURL(mode, activity, page);
const response = await httpGet(url);
const response = await httpGet<string | Buffer | BinaryData | undefined>(url);
const dom = new JSDOM(response.data);
const playersHTML = dom.window.document.querySelectorAll(
'.personal-hiscores__row'

View File

@@ -5,7 +5,10 @@ export type Gamemode =
| 'hardcore'
| 'deadman'
| 'seasonal'
| 'tournament';
| 'tournament'
| 'skiller'
| 'oneDefence'
| 'freshStart';
export interface Skill {
rank: number;
@@ -64,9 +67,11 @@ export type BH = { [Type in BHType]: Activity };
export type Boss =
| 'abyssalSire'
| 'alchemicalHydra'
| 'artio'
| 'barrows'
| 'bryophyta'
| 'callisto'
| 'calvarion'
| 'cerberus'
| 'chambersOfXeric'
| 'chambersOfXericChallengeMode'
@@ -93,9 +98,11 @@ export type Boss =
| 'nightmare'
| 'phosanisNightmare'
| 'obor'
| 'phantomMuspah'
| 'sarachnis'
| 'scorpia'
| 'skotizo'
| 'spindel'
| 'tempoross'
| 'gauntlet'
| 'corruptedGauntlet'
@@ -162,3 +169,16 @@ export interface PlayerActivityRow extends Activity {
name: string;
dead: boolean;
}
export interface GetStatsOptions {
/**
* Other game modes to fetch ranks for.
* @defaultvalue `['ironman', 'hardcore', 'ultimate']`
*/
otherGamemodes?: Extract<Gamemode, 'ironman' | 'hardcore' | 'ultimate'>[];
/**
* If true, the formatted RSN will be fetched. Otherwise it will return the provided, unformatted RSN.
* @defaultvalue `true`
*/
shouldGetFormattedRsn?: boolean;
}

View File

@@ -22,7 +22,10 @@ export const GAMEMODE_URL: GamemodeUrl = {
ultimate: `${BASE_URL}_ultimate/`,
deadman: `${BASE_URL}_deadman/`,
seasonal: `${BASE_URL}_seasonal/`,
tournament: `${BASE_URL}_tournament/`
tournament: `${BASE_URL}_tournament/`,
skiller: `${BASE_URL}_skiller/`,
oneDefence: `${BASE_URL}_skiller_defence/`,
freshStart: `${BASE_URL}_fresh_start/`
};
export const SKILLS: SkillName[] = [
'overall',
@@ -72,9 +75,11 @@ export const GAMEMODES: Gamemode[] = [
export const BOSSES: Boss[] = [
'abyssalSire',
'alchemicalHydra',
'artio',
'barrows',
'bryophyta',
'callisto',
'calvarion',
'cerberus',
'chambersOfXeric',
'chambersOfXericChallengeMode',
@@ -101,9 +106,11 @@ export const BOSSES: Boss[] = [
'nightmare',
'phosanisNightmare',
'obor',
'phantomMuspah',
'sarachnis',
'scorpia',
'skotizo',
'spindel',
'tempoross',
'gauntlet',
'corruptedGauntlet',
@@ -146,9 +153,11 @@ export type FormattedBossNames = {
export const FORMATTED_BOSS_NAMES: FormattedBossNames = {
abyssalSire: 'Abyssal Sire',
alchemicalHydra: 'Alchemical Hydra',
artio: 'Artio',
barrows: 'Barrows Chests',
bryophyta: 'Bryophyta',
callisto: 'Callisto',
calvarion: "Cal'varion",
cerberus: 'Cerberus',
chambersOfXeric: 'Chambers of Xeric',
chambersOfXericChallengeMode: 'Chambers of Xeric: Challenge Mode',
@@ -175,9 +184,11 @@ export const FORMATTED_BOSS_NAMES: FormattedBossNames = {
nightmare: 'The Nightmare of Ashihama',
phosanisNightmare: "Phosani's Nightmare",
obor: 'Obor',
phantomMuspah: 'Phantom Muspah',
sarachnis: 'Sarachnis',
scorpia: 'Scorpia',
skotizo: 'Skotizo',
spindel: 'Spindel',
tempoross: 'Tempoross',
gauntlet: 'The Gauntlet',
corruptedGauntlet: 'The Corrupted Gauntlet',

View File

@@ -96,10 +96,25 @@ export const rsnFromElement = (el: Element | null) => {
* @param url URL to run a `GET` request against.
* @returns Axios response.
*/
export const httpGet = (url: string) =>
axios.get(url, {
export const httpGet = <Response>(url: string) =>
axios.get<Response>(url, {
headers: {
// without User-Agent header requests may be rejected by DDoS protection mechanism
'User-Agent': ua.firefox(80)
}
});
/**
* Validates that a provided RSN has the same username restrictions as Jagex.
* @param rsn Username to validate.
* @throws Error if the RSN fails validation.
*/
export const validateRSN = (rsn: string) => {
if (typeof rsn !== 'string') {
throw Error('RSN must be a string');
} else if (!/^[a-zA-Z0-9 _-]+$/.test(rsn)) {
throw Error('RSN contains invalid character');
} else if (rsn.length > 12 || rsn.length < 1) {
throw Error('RSN must be between 1 and 12 characters');
}
};

653
yarn.lock

File diff suppressed because it is too large Load Diff