Compare commits

..

1 Commits

Author SHA1 Message Date
maxswa
e77737f422 Add Guardians of the Rift. 2022-04-18 15:44:45 -04:00
17 changed files with 6050 additions and 7652 deletions

View File

@@ -10,19 +10,19 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3.3.0
- uses: borales/actions-yarn@v4.2.0
- uses: actions/checkout@v2
- uses: borales/actions-yarn@v2.0.0
with:
cmd: install
- uses: borales/actions-yarn@v4.2.0
- uses: borales/actions-yarn@v2.0.0
with:
cmd: lint
- uses: borales/actions-yarn@v4.2.0
- uses: borales/actions-yarn@v2.0.0
with:
cmd: format
- uses: borales/actions-yarn@v4.2.0
- uses: borales/actions-yarn@v2.0.0
with:
cmd: build
- uses: borales/actions-yarn@v4.2.0
- uses: borales/actions-yarn@v2.0.0
with:
cmd: test

3
.gitignore vendored
View File

@@ -1,4 +1,3 @@
.idea/
node_modules
/lib
.vscode
/lib

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/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)
[![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)
**The Old School Runescape API wrapper that does more!**
@@ -104,16 +104,13 @@ Activities consist of all levels of clue scrolls as well as minigames and bosses
### Minigames
| Minigame | Param |
| ------------------------------- | :---------------: |
| Bounty Hunter (Legacy - Rogue) | `rogueBH` |
| Bounty Hunter (Legacy - Hunter) | `hunterBH` |
| Bounty Hunter (Rogue) | `rogueBHV2` |
| Bounty Hunter (Hunter) | `hunterBHV2` |
| LMS - Rank | `lastManStanding` |
| PvP Arena - Rank | `pvpArena` |
| Soul Wars Zeal | `soulWarsZeal` |
| Rifts closed | `riftsClosed` |
| Minigame | Param |
| ---------------------- | :---------------: |
| Bounty Hunter (Rogue) | `rogueBH` |
| Bounty Hunter (Hunter) | `hunterBH` |
| Last Man Standing | `lastManStanding` |
| Soul Wars Zeal | `soulWarsZeal` |
| Rifts Closed | `riftsClosed` |
### Leagues
@@ -127,11 +124,9 @@ 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` |
| Calvar'ion | `calvarion` |
| Cerberus | `cerberus` |
| Chambers Of Xeric | `chambersOfXeric` |
| Chambers Of Xeric: Challenge Mode | `chambersOfXericChallengeMode` |
@@ -144,7 +139,6 @@ Activities consist of all levels of clue scrolls as well as minigames and bosses
| Dagannoth Rex | `dagannothRex` |
| Dagannoth Supreme | `dagannothSupreme` |
| Deranged Archaeologist | `derangedArchaeologist` |
| Duke Sucellus | `dukeSucellus` |
| General Graardor | `generalGraardor` |
| Giant Mole | `giantMole` |
| Grotesque Guardians | `grotesqueGuardians` |
@@ -156,27 +150,20 @@ Activities consist of all levels of clue scrolls as well as minigames and bosses
| K'ril Tsutsaroth | `krilTsutsaroth` |
| Mimic | `mimic` |
| Nex | `nex` |
| Nightmare | `nightmare` |
| 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` |
| The Leviathan | `leviathan` |
| The Whisperer | `whisperer` |
| Theatre Of Blood | `theatreOfBlood` |
| Theatre Of Blood: Hard Mode | `theatreOfBloodHardMode` |
| Thermonuclear Smoke Devil | `thermonuclearSmokeDevil` |
| Tombs of Amascut | `tombsOfAmascut` |
| Tombs of Amascut: Expert Mode | `tombsOfAmascutExpertMode` |
| TzKal-Zuk | `tzKalZuk` |
| TzTok-Jad | `tzTokJad` |
| Vardorvis | `vardorvis` |
| Venenatis | `venenatis` |
| Vetion | `vetion` |
| Vorkath | `vorkath` |
@@ -206,7 +193,6 @@ Activities consist of all levels of clue scrolls as well as minigames and bosses
leaguePoints: {},
bountyHunter: {},
lastManStanding: {},
pvpArena: {},
soulWarsZeal: {},
riftsClosed: {},
bosses: {}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,98 +0,0 @@
3923,2277,792976020
23024,99,29963318
12244,99,28197154
2261,99,134139762
4074,99,112054504
7166,99,101083246
3747,99,15110682
4516,99,50824811
126288,99,13353110
24800,99,16366466
49293,99,13453226
20784,99,16654289
16376,99,19114676
42069,99,14055874
9305,99,16527135
25135,99,14657869
35831,99,13233130
6197,99,15739327
48215,99,13963265
4786,99,33830972
3402,99,62531484
4314,99,22733194
14826,99,17399368
1009,99,17989158
-1,-1
22270,10
10481,3
149217,8
103672,7
253,9508
713,825
71,6000
1943,1600
42364,287
467,436
1634,360
12937,2521
-1,-1
5222,14780
3007,1060
2578,2192
84703,594
19500,143
3335,1830
191697,5
27457,293
43607,58
17384,1905
171607,33
2074,501
78908,50
135830,26
48781,287
2112,2000
138748,41
31297,707
40908,698
31273,714
76034,25
2391,688
17409,1228
60841,474
1895,2140
19948,159
1855,2046
46423,579
107879,1908
9791,920
2050,1444
848,33
24797,620
4157,744
4154,500
880,217
15860,251
60424,119
100429,26
358191,9
1131,1569
33908,273
165651,7
64278,201
1533,583
748,700
21700,250
13200,7
6622,3066
87093,35
2449,502
14541,2
-1,-1
1048,1432
138,7096
58203,51
13949,2780
827440,62
153191,25
216690,274
Can't render this file because it has a wrong number of fields in line 25.

View File

@@ -1,194 +0,0 @@
{
"skills": [
{
"id": 0,
"name": "Overall",
"rank": 3923,
"level": 2277,
"xp": 792976020
},
{ "id": 1, "name": "Attack", "rank": 23024, "level": 99, "xp": 29963318 },
{ "id": 2, "name": "Defence", "rank": 12244, "level": 99, "xp": 28197154 },
{ "id": 3, "name": "Strength", "rank": 2261, "level": 99, "xp": 134139762 },
{
"id": 4,
"name": "Hitpoints",
"rank": 4074,
"level": 99,
"xp": 112054504
},
{ "id": 5, "name": "Ranged", "rank": 7166, "level": 99, "xp": 101083246 },
{ "id": 6, "name": "Prayer", "rank": 3747, "level": 99, "xp": 15110682 },
{ "id": 7, "name": "Magic", "rank": 4516, "level": 99, "xp": 50824811 },
{ "id": 8, "name": "Cooking", "rank": 126288, "level": 99, "xp": 13353110 },
{
"id": 9,
"name": "Woodcutting",
"rank": 24800,
"level": 99,
"xp": 16366466
},
{
"id": 10,
"name": "Fletching",
"rank": 49293,
"level": 99,
"xp": 13453226
},
{ "id": 11, "name": "Fishing", "rank": 20784, "level": 99, "xp": 16654289 },
{
"id": 12,
"name": "Firemaking",
"rank": 16376,
"level": 99,
"xp": 19114676
},
{
"id": 13,
"name": "Crafting",
"rank": 42069,
"level": 99,
"xp": 14055874
},
{ "id": 14, "name": "Smithing", "rank": 9305, "level": 99, "xp": 16527135 },
{ "id": 15, "name": "Mining", "rank": 25135, "level": 99, "xp": 14657869 },
{
"id": 16,
"name": "Herblore",
"rank": 35831,
"level": 99,
"xp": 13233130
},
{ "id": 17, "name": "Agility", "rank": 6197, "level": 99, "xp": 15739327 },
{
"id": 18,
"name": "Thieving",
"rank": 48215,
"level": 99,
"xp": 13963265
},
{ "id": 19, "name": "Slayer", "rank": 4786, "level": 99, "xp": 33830972 },
{ "id": 20, "name": "Farming", "rank": 3402, "level": 99, "xp": 62531484 },
{
"id": 21,
"name": "Runecraft",
"rank": 4314,
"level": 99,
"xp": 22733194
},
{ "id": 22, "name": "Hunter", "rank": 14826, "level": 99, "xp": 17399368 },
{
"id": 23,
"name": "Construction",
"rank": 1009,
"level": 99,
"xp": 17989158
}
],
"activities": [
{ "id": 0, "name": "League Points", "rank": -1, "score": -1 },
{ "id": 1, "name": "Bounty Hunter - Hunter", "rank": 22270, "score": 10 },
{ "id": 2, "name": "Bounty Hunter - Rogue", "rank": 10481, "score": 3 },
{
"id": 3,
"name": "Bounty Hunter (Legacy) - Hunter",
"rank": 149217,
"score": 8
},
{
"id": 4,
"name": "Bounty Hunter (Legacy) - Rogue",
"rank": 103672,
"score": 7
},
{ "id": 5, "name": "Clue Scrolls (all)", "rank": 253, "score": 9508 },
{ "id": 6, "name": "Clue Scrolls (beginner)", "rank": 713, "score": 825 },
{ "id": 7, "name": "Clue Scrolls (easy)", "rank": 71, "score": 6000 },
{ "id": 8, "name": "Clue Scrolls (medium)", "rank": 1943, "score": 1600 },
{ "id": 9, "name": "Clue Scrolls (hard)", "rank": 42364, "score": 287 },
{ "id": 10, "name": "Clue Scrolls (elite)", "rank": 467, "score": 436 },
{ "id": 11, "name": "Clue Scrolls (master)", "rank": 1634, "score": 360 },
{ "id": 12, "name": "LMS - Rank", "rank": 12937, "score": 2521 },
{ "id": 13, "name": "PvP Arena - Rank", "rank": -1, "score": -1 },
{ "id": 14, "name": "Soul Wars Zeal", "rank": 5222, "score": 14780 },
{ "id": 15, "name": "Rifts closed", "rank": 3007, "score": 1060 },
{ "id": 16, "name": "Abyssal Sire", "rank": 2578, "score": 2192 },
{ "id": 17, "name": "Alchemical Hydra", "rank": 84703, "score": 594 },
{ "id": 18, "name": "Artio", "rank": 19500, "score": 143 },
{ "id": 19, "name": "Barrows Chests", "rank": 3335, "score": 1830 },
{ "id": 20, "name": "Bryophyta", "rank": 191697, "score": 5 },
{ "id": 21, "name": "Callisto", "rank": 27457, "score": 293 },
{ "id": 22, "name": "Calvar'ion", "rank": 43607, "score": 58 },
{ "id": 23, "name": "Cerberus", "rank": 17384, "score": 1905 },
{ "id": 24, "name": "Chambers of Xeric", "rank": 171607, "score": 33 },
{
"id": 25,
"name": "Chambers of Xeric: Challenge Mode",
"rank": 2074,
"score": 501
},
{ "id": 26, "name": "Chaos Elemental", "rank": 78908, "score": 50 },
{ "id": 27, "name": "Chaos Fanatic", "rank": 135830, "score": 26 },
{ "id": 28, "name": "Commander Zilyana", "rank": 48781, "score": 287 },
{ "id": 29, "name": "Corporeal Beast", "rank": 2112, "score": 2000 },
{ "id": 30, "name": "Crazy Archaeologist", "rank": 138748, "score": 41 },
{ "id": 31, "name": "Dagannoth Prime", "rank": 31297, "score": 707 },
{ "id": 32, "name": "Dagannoth Rex", "rank": 40908, "score": 698 },
{ "id": 33, "name": "Dagannoth Supreme", "rank": 31273, "score": 714 },
{ "id": 34, "name": "Deranged Archaeologist", "rank": 76034, "score": 25 },
{ "id": 35, "name": "Duke Sucellus", "rank": 2391, "score": 688 },
{ "id": 36, "name": "General Graardor", "rank": 17409, "score": 1228 },
{ "id": 37, "name": "Giant Mole", "rank": 60841, "score": 474 },
{ "id": 38, "name": "Grotesque Guardians", "rank": 1895, "score": 2140 },
{ "id": 39, "name": "Hespori", "rank": 19948, "score": 159 },
{ "id": 40, "name": "Kalphite Queen", "rank": 1855, "score": 2046 },
{ "id": 41, "name": "King Black Dragon", "rank": 46423, "score": 579 },
{ "id": 42, "name": "Kraken", "rank": 107879, "score": 1908 },
{ "id": 43, "name": "Kree'Arra", "rank": 9791, "score": 920 },
{ "id": 44, "name": "K'ril Tsutsaroth", "rank": 2050, "score": 1444 },
{ "id": 45, "name": "Mimic", "rank": 848, "score": 33 },
{ "id": 46, "name": "Nex", "rank": 24797, "score": 620 },
{ "id": 47, "name": "Nightmare", "rank": 4157, "score": 744 },
{ "id": 48, "name": "Phosani's Nightmare", "rank": 4154, "score": 500 },
{ "id": 49, "name": "Obor", "rank": 880, "score": 217 },
{ "id": 50, "name": "Phantom Muspah", "rank": 15860, "score": 251 },
{ "id": 51, "name": "Sarachnis", "rank": 60424, "score": 119 },
{ "id": 52, "name": "Scorpia", "rank": 100429, "score": 26 },
{ "id": 53, "name": "Skotizo", "rank": 358191, "score": 9 },
{ "id": 54, "name": "Spindel", "rank": 1131, "score": 1569 },
{ "id": 55, "name": "Tempoross", "rank": 33908, "score": 273 },
{ "id": 56, "name": "The Gauntlet", "rank": 165651, "score": 7 },
{ "id": 57, "name": "The Corrupted Gauntlet", "rank": 64278, "score": 201 },
{ "id": 58, "name": "The Leviathan", "rank": 1533, "score": 583 },
{ "id": 59, "name": "The Whisperer", "rank": 748, "score": 700 },
{ "id": 60, "name": "Theatre of Blood", "rank": 21700, "score": 250 },
{
"id": 61,
"name": "Theatre of Blood: Hard Mode",
"rank": 13200,
"score": 7
},
{
"id": 62,
"name": "Thermonuclear Smoke Devil",
"rank": 6622,
"score": 3066
},
{ "id": 63, "name": "Tombs of Amascut", "rank": 87093, "score": 35 },
{
"id": 64,
"name": "Tombs of Amascut: Expert Mode",
"rank": 2449,
"score": 502
},
{ "id": 65, "name": "TzKal-Zuk", "rank": 14541, "score": 2 },
{ "id": 66, "name": "TzTok-Jad", "rank": -1, "score": -1 },
{ "id": 67, "name": "Vardorvis", "rank": 1048, "score": 1432 },
{ "id": 68, "name": "Venenatis", "rank": 138, "score": 7096 },
{ "id": 69, "name": "Vet'ion", "rank": 58203, "score": 51 },
{ "id": 70, "name": "Vorkath", "rank": 13949, "score": 2780 },
{ "id": 71, "name": "Wintertodt", "rank": 827440, "score": 62 },
{ "id": 72, "name": "Zalcano", "rank": 153191, "score": 25 },
{ "id": 73, "name": "Zulrah", "rank": 216690, "score": 274 }
]
}

View File

@@ -12,10 +12,7 @@ import {
getSkillPageURL,
getStatsURL,
BOSSES,
INVALID_FORMAT_ERROR,
BH_MODES,
parseJsonStats,
HiscoresResponse
INVALID_FORMAT_ERROR
} from '../src/index';
const B0ATY_NAME = 'B0ATY';
@@ -27,13 +24,7 @@ const LYNX_TITAN_FORMATTED_NAME = 'Lynx Titan';
const attackTopPage = readFileSync(`${__dirname}/attackTopPage.html`, 'utf8');
const b0atyNamePage = readFileSync(`${__dirname}/b0atyNamePage.html`, 'utf8');
const b0atyStatsCsv = readFileSync(`${__dirname}/b0atyStats.csv`, 'utf8');
const b0atyStatsJson: HiscoresResponse = JSON.parse(
readFileSync(`${__dirname}/b0atyStats.json`, 'utf8')
);
const lynxTitanStats = JSON.parse(
readFileSync(`${__dirname}/lynxTitanStats.json`, 'utf8')
);
const lynxTitanStats = readFileSync(`${__dirname}/lynxTitanStats.csv`, 'utf8');
const lynxTitanNamePage = readFileSync(
`${__dirname}/lynxTitanNamePage.html`,
'utf8'
@@ -54,7 +45,7 @@ jest.spyOn(axios, 'get').mockImplementation((url) => {
if (getSkillPageURL('main', 'attack', 1) === url) {
return Promise.resolve({ data: attackTopPage });
}
if (getStatsURL('main', LYNX_TITAN_FORMATTED_NAME, true) === url) {
if (getStatsURL('main', LYNX_TITAN_FORMATTED_NAME) === url) {
return Promise.resolve({ status: 200, data: lynxTitanStats });
}
throw new Error(`No mock response for URL: ${url}`);
@@ -86,8 +77,6 @@ test('Parse CSV to json', () => {
810,99,37688883
92,99,32005622
23423,478
89914,35
99834,25
99831,23
89912,37
32,12148
@@ -98,16 +87,13 @@ test('Parse CSV to json', () => {
392,250
1,6143
4814,898
13,4057
37,225
25,1110
23,467
382,2780
944,3000
704,892
1981,1452
4981,23
888,1046
444,792
613,4856
102,4038
156,334
@@ -120,7 +106,6 @@ test('Parse CSV to json', () => {
4342,1655
966,2951
10151,1
1289,2477
1288,2407
377,4669
545,1567
@@ -135,24 +120,17 @@ 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
2871,7
2872,8
6984,138
23,923141
4043,2000
4073,1020
289,13070
489,8
967,47
968,48
11155,223
1940,272
8623,1340
@@ -189,15 +167,12 @@ test('Parse CSV to json', () => {
},
leaguePoints: { rank: 23423, score: 478 },
bountyHunter: {
hunterV2: { rank: 89914, score: 35 },
rogueV2: { rank: 99834, score: 25 },
hunter: { rank: 99831, score: 23 },
rogue: { rank: 89912, score: 37 }
},
lastManStanding: { rank: 4814, score: 898 },
pvpArena: { rank: 13, score: 4057 },
soulWarsZeal: { rank: 37, score: 225 },
riftsClosed: { rank: 25, score: 1110 },
riftsClosed: { rank: 23, score: 467 },
clues: {
all: { rank: 32, score: 12148 },
beginner: { rank: 3105, score: 76 },
@@ -210,11 +185,9 @@ 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 },
@@ -227,7 +200,6 @@ test('Parse CSV to json', () => {
dagannothRex: { rank: 4342, score: 1655 },
dagannothSupreme: { rank: 966, score: 2951 },
derangedArchaeologist: { rank: 10151, score: 1 },
dukeSucellus: { rank: 1289, score: 2477 },
generalGraardor: { rank: 1288, score: 2407 },
giantMole: { rank: 377, score: 4669 },
grotesqueGuardians: { rank: 545, score: 1567 },
@@ -242,24 +214,17 @@ 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 },
leviathan: { rank: 2871, score: 7 },
whisperer: { rank: 2872, score: 8 },
theatreOfBlood: { rank: 6984, score: 138 },
theatreOfBloodHardMode: { rank: 23, score: 923141 },
thermonuclearSmokeDevil: { rank: 4043, score: 2000 },
tombsOfAmascut: { rank: 4073, score: 1020 },
tombsOfAmascutExpertMode: { rank: 289, score: 13070 },
tzKalZuk: { rank: 489, score: 8 },
tzTokJad: { rank: 967, score: 47 },
vardorvis: { rank: 968, score: 48 },
venenatis: { rank: 11155, score: 223 },
vetion: { rank: 1940, score: 272 },
vorkath: { rank: 8623, score: 1340 },
@@ -496,7 +461,7 @@ test('Get non-existent player', async () => {
});
test('Get stats by gamemode', async () => {
const { skills, bosses, bountyHunter } = await getStatsByGamemode(
const { skills, bosses } = await getStatsByGamemode(
LYNX_TITAN_FORMATTED_NAME
);
@@ -529,61 +494,6 @@ test('Get stats by gamemode', async () => {
const bossKeys = Object.keys(bosses);
expect(bossKeys).toStrictEqual(BOSSES);
const bountyHunterKeys = Object.keys(bountyHunter);
expect(bountyHunterKeys).toStrictEqual(BH_MODES);
expect.assertions(3);
});
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, true),
getPlayerTableURL('main', rsn),
getStatsURL('ironman', rsn, true),
getStatsURL('hardcore', rsn, true),
getStatsURL('ultimate', rsn, true)
]);
});
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();
});
});
test('CSV and JSON parsing outputs identical object', async () => {
const csvOutput = parseStats(b0atyStatsCsv);
const jsonOutput = parseJsonStats(b0atyStatsJson);
expect(csvOutput).toEqual(jsonOutput);
expect.assertions(2);
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,85 @@
1,2277,4600000000
15,99,200000000
28,99,200000000
18,99,200000000
7,99,200000000
8,99,200000000
11,99,200000000
32,99,200000000
159,99,200000000
15,99,200000000
12,99,200000000
9,99,200000000
48,99,200000000
4,99,200000000
3,99,200000000
25,99,200000000
5,99,200000000
23,99,200000000
12,99,200000000
2,99,200000000
19,99,200000000
7,99,200000000
4,99,200000000
4,99,200000000
-1,-1
-1,-1
-1,-1
-1,-1
-1,-1
-1,-1
-1,-1
-1,-1
347584,22
-1,-1
-1,-1
-1,-1
-1,-1
-1,-1
-1,-1
-1,-1
-1,-1
-1,-1
-1,-1
-1,-1
-1,-1
-1,-1
-1,-1
-1,-1
-1,-1
-1,-1
-1,-1
-1,-1
-1,-1
-1,-1
-1,-1
-1,-1
-1,-1
-1,-1
-1,-1
-1,-1
-1,-1
-1,-1
-1,-1
-1,-1
-1,-1
-1,-1
-1,-1
-1,-1
-1,-1
-1,-1
-1,-1
-1,-1
-1,-1
-1,-1
-1,-1
-1,-1
-1,-1
-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,147 +0,0 @@
{
"skills": [
{ "id": 0, "name": "Overall", "rank": 1, "level": 2277, "xp": 4600000000 },
{ "id": 1, "name": "Attack", "rank": 15, "level": 99, "xp": 200000000 },
{ "id": 2, "name": "Defence", "rank": 28, "level": 99, "xp": 200000000 },
{ "id": 3, "name": "Strength", "rank": 18, "level": 99, "xp": 200000000 },
{ "id": 4, "name": "Hitpoints", "rank": 7, "level": 99, "xp": 200000000 },
{ "id": 5, "name": "Ranged", "rank": 8, "level": 99, "xp": 200000000 },
{ "id": 6, "name": "Prayer", "rank": 11, "level": 99, "xp": 200000000 },
{ "id": 7, "name": "Magic", "rank": 32, "level": 99, "xp": 200000000 },
{ "id": 8, "name": "Cooking", "rank": 157, "level": 99, "xp": 200000000 },
{
"id": 9,
"name": "Woodcutting",
"rank": 15,
"level": 99,
"xp": 200000000
},
{ "id": 10, "name": "Fletching", "rank": 12, "level": 99, "xp": 200000000 },
{ "id": 11, "name": "Fishing", "rank": 9, "level": 99, "xp": 200000000 },
{
"id": 12,
"name": "Firemaking",
"rank": 48,
"level": 99,
"xp": 200000000
},
{ "id": 13, "name": "Crafting", "rank": 4, "level": 99, "xp": 200000000 },
{ "id": 14, "name": "Smithing", "rank": 3, "level": 99, "xp": 200000000 },
{ "id": 15, "name": "Mining", "rank": 25, "level": 99, "xp": 200000000 },
{ "id": 16, "name": "Herblore", "rank": 5, "level": 99, "xp": 200000000 },
{ "id": 17, "name": "Agility", "rank": 24, "level": 99, "xp": 200000000 },
{ "id": 18, "name": "Thieving", "rank": 12, "level": 99, "xp": 200000000 },
{ "id": 19, "name": "Slayer", "rank": 2, "level": 99, "xp": 200000000 },
{ "id": 20, "name": "Farming", "rank": 19, "level": 99, "xp": 200000000 },
{ "id": 21, "name": "Runecraft", "rank": 7, "level": 99, "xp": 200000000 },
{ "id": 22, "name": "Hunter", "rank": 4, "level": 99, "xp": 200000000 },
{
"id": 23,
"name": "Construction",
"rank": 4,
"level": 99,
"xp": 200000000
}
],
"activities": [
{ "id": 0, "name": "League Points", "rank": -1, "score": -1 },
{ "id": 1, "name": "Bounty Hunter - Hunter", "rank": -1, "score": -1 },
{ "id": 2, "name": "Bounty Hunter - Rogue", "rank": -1, "score": -1 },
{
"id": 3,
"name": "Bounty Hunter (Legacy) - Hunter",
"rank": -1,
"score": -1
},
{
"id": 4,
"name": "Bounty Hunter (Legacy) - Rogue",
"rank": -1,
"score": -1
},
{ "id": 5, "name": "Clue Scrolls (all)", "rank": 769527, "score": 22 },
{ "id": 6, "name": "Clue Scrolls (beginner)", "rank": -1, "score": -1 },
{ "id": 7, "name": "Clue Scrolls (easy)", "rank": -1, "score": -1 },
{ "id": 8, "name": "Clue Scrolls (medium)", "rank": -1, "score": -1 },
{ "id": 9, "name": "Clue Scrolls (hard)", "rank": 483333, "score": 22 },
{ "id": 10, "name": "Clue Scrolls (elite)", "rank": -1, "score": -1 },
{ "id": 11, "name": "Clue Scrolls (master)", "rank": -1, "score": -1 },
{ "id": 12, "name": "LMS - Rank", "rank": -1, "score": -1 },
{ "id": 13, "name": "PvP Arena - Rank", "rank": -1, "score": -1 },
{ "id": 14, "name": "Soul Wars Zeal", "rank": -1, "score": -1 },
{ "id": 15, "name": "Rifts closed", "rank": -1, "score": -1 },
{ "id": 16, "name": "Abyssal Sire", "rank": -1, "score": -1 },
{ "id": 17, "name": "Alchemical Hydra", "rank": -1, "score": -1 },
{ "id": 18, "name": "Artio", "rank": -1, "score": -1 },
{ "id": 19, "name": "Barrows Chests", "rank": -1, "score": -1 },
{ "id": 20, "name": "Bryophyta", "rank": -1, "score": -1 },
{ "id": 21, "name": "Callisto", "rank": -1, "score": -1 },
{ "id": 22, "name": "Calvar'ion", "rank": -1, "score": -1 },
{ "id": 23, "name": "Cerberus", "rank": -1, "score": -1 },
{ "id": 24, "name": "Chambers of Xeric", "rank": -1, "score": -1 },
{
"id": 25,
"name": "Chambers of Xeric: Challenge Mode",
"rank": -1,
"score": -1
},
{ "id": 26, "name": "Chaos Elemental", "rank": -1, "score": -1 },
{ "id": 27, "name": "Chaos Fanatic", "rank": -1, "score": -1 },
{ "id": 28, "name": "Commander Zilyana", "rank": -1, "score": -1 },
{ "id": 29, "name": "Corporeal Beast", "rank": -1, "score": -1 },
{ "id": 30, "name": "Crazy Archaeologist", "rank": -1, "score": -1 },
{ "id": 31, "name": "Dagannoth Prime", "rank": -1, "score": -1 },
{ "id": 32, "name": "Dagannoth Rex", "rank": -1, "score": -1 },
{ "id": 33, "name": "Dagannoth Supreme", "rank": -1, "score": -1 },
{ "id": 34, "name": "Deranged Archaeologist", "rank": -1, "score": -1 },
{ "id": 35, "name": "Duke Sucellus", "rank": -1, "score": -1 },
{ "id": 36, "name": "General Graardor", "rank": -1, "score": -1 },
{ "id": 37, "name": "Giant Mole", "rank": -1, "score": -1 },
{ "id": 38, "name": "Grotesque Guardians", "rank": -1, "score": -1 },
{ "id": 39, "name": "Hespori", "rank": -1, "score": -1 },
{ "id": 40, "name": "Kalphite Queen", "rank": -1, "score": -1 },
{ "id": 41, "name": "King Black Dragon", "rank": -1, "score": -1 },
{ "id": 42, "name": "Kraken", "rank": -1, "score": -1 },
{ "id": 43, "name": "Kree'Arra", "rank": -1, "score": -1 },
{ "id": 44, "name": "K'ril Tsutsaroth", "rank": -1, "score": -1 },
{ "id": 45, "name": "Mimic", "rank": -1, "score": -1 },
{ "id": 46, "name": "Nex", "rank": -1, "score": -1 },
{ "id": 47, "name": "Nightmare", "rank": -1, "score": -1 },
{ "id": 48, "name": "Phosani's Nightmare", "rank": -1, "score": -1 },
{ "id": 49, "name": "Obor", "rank": -1, "score": -1 },
{ "id": 50, "name": "Phantom Muspah", "rank": -1, "score": -1 },
{ "id": 51, "name": "Sarachnis", "rank": -1, "score": -1 },
{ "id": 52, "name": "Scorpia", "rank": -1, "score": -1 },
{ "id": 53, "name": "Skotizo", "rank": -1, "score": -1 },
{ "id": 54, "name": "Spindel", "rank": -1, "score": -1 },
{ "id": 55, "name": "Tempoross", "rank": -1, "score": -1 },
{ "id": 56, "name": "The Gauntlet", "rank": -1, "score": -1 },
{ "id": 57, "name": "The Corrupted Gauntlet", "rank": -1, "score": -1 },
{ "id": 58, "name": "The Leviathan", "rank": -1, "score": -1 },
{ "id": 59, "name": "The Whisperer", "rank": -1, "score": -1 },
{ "id": 60, "name": "Theatre of Blood", "rank": -1, "score": -1 },
{
"id": 61,
"name": "Theatre of Blood: Hard Mode",
"rank": -1,
"score": -1
},
{ "id": 62, "name": "Thermonuclear Smoke Devil", "rank": -1, "score": -1 },
{ "id": 63, "name": "Tombs of Amascut", "rank": -1, "score": -1 },
{
"id": 64,
"name": "Tombs of Amascut: Expert Mode",
"rank": -1,
"score": -1
},
{ "id": 65, "name": "TzKal-Zuk", "rank": -1, "score": -1 },
{ "id": 66, "name": "TzTok-Jad", "rank": 198, "score": 186 },
{ "id": 67, "name": "Vardorvis", "rank": -1, "score": -1 },
{ "id": 68, "name": "Venenatis", "rank": -1, "score": -1 },
{ "id": 69, "name": "Vet'ion", "rank": -1, "score": -1 },
{ "id": 70, "name": "Vorkath", "rank": -1, "score": -1 },
{ "id": 71, "name": "Wintertodt", "rank": -1, "score": -1 },
{ "id": 72, "name": "Zalcano", "rank": -1, "score": -1 },
{ "id": 73, "name": "Zulrah", "rank": -1, "score": -1 }
]
}

View File

@@ -1,6 +1,6 @@
{
"name": "osrs-json-hiscores",
"version": "2.16.0",
"version": "2.7.0",
"description": "The Old School Runescape API wrapper that does more!",
"main": "lib/index.js",
"types": "lib/index.d.ts",
@@ -30,9 +30,6 @@
"stats",
"skills"
],
"publishConfig": {
"registry": "https://registry.npmjs.org"
},
"author": "maxswa",
"license": "ISC",
"bugs": {
@@ -61,8 +58,7 @@
"@typescript-eslint"
],
"extends": [
"airbnb-base",
"airbnb-typescript/base",
"eslint-config-airbnb-typescript",
"prettier"
],
"ignorePatterns": [
@@ -73,8 +69,7 @@
"trailingComma": "none",
"tabWidth": 2,
"semi": true,
"singleQuote": true,
"endOfLine": "auto"
"singleQuote": true
},
"jest": {
"transform": {
@@ -93,24 +88,27 @@
},
"dependencies": {
"axios": "^0.21.1",
"jsdom": "^22.1.0",
"jsdom": "^16.3.0",
"useragent-generator": "^1.1.0"
},
"devDependencies": {
"@types/jest": "^29.5.3",
"@types/jsdom": "^21.1.1",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"eslint": "^8.44.0",
"eslint-config-airbnb-typescript": "^17.1.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-import": "^2.27.5",
"@types/jest": "^26.0.21",
"@types/jsdom": "^16.2.3",
"@typescript-eslint/eslint-plugin": "^4.19.0",
"@typescript-eslint/parser": "^4.19.0",
"eslint": "^7.22.0",
"eslint-config-airbnb-typescript": "^12.3.1",
"eslint-config-prettier": "^8.1.0",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-react": "^7.23.1",
"eslint-plugin-react-hooks": "^4.2.0",
"husky": "^5.2.0",
"jest": "^29.6.1",
"lint-staged": "^13.2.3",
"np": "^7.7.0",
"prettier": "^3.0.0",
"ts-jest": "^29.1.1",
"typescript": "^5.1.6"
"jest": "^26.6.3",
"lint-staged": "^10.5.4",
"np": "6.5.0",
"prettier": "^2.2.1",
"ts-jest": "^26.5.4",
"typescript": "^4.2.3"
}
}

View File

@@ -1,5 +1,4 @@
import { AxiosRequestConfig, AxiosResponse } from 'axios';
import { BinaryData, JSDOM } from 'jsdom';
import { JSDOM } from 'jsdom';
import {
Player,
Activity,
@@ -13,9 +12,7 @@ import {
PlayerSkillRow,
ActivityName,
PlayerActivityRow,
Bosses,
GetStatsOptions,
HiscoresResponse
Bosses
} from './types';
import {
getStatsURL,
@@ -32,145 +29,41 @@ import {
httpGet,
BOSSES,
INVALID_FORMAT_ERROR,
validateRSN,
PLAYER_NOT_FOUND_ERROR,
FORMATTED_SKILL_NAMES,
FORMATTED_BH_NAMES,
FORMATTED_CLUE_NAMES,
FORMATTED_BOSS_NAMES,
FORMATTED_LEAGUE_POINTS,
FORMATTED_LMS,
FORMATTED_PVP_ARENA,
FORMATTED_SOUL_WARS,
FORMATTED_RIFTS_CLOSED
EXTRA_ACTIVITY_COUNT
} from './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.
*/
export async function getOfficialStats(
rsn: string,
mode: Gamemode = 'main',
config?: AxiosRequestConfig
): Promise<HiscoresResponse> {
validateRSN(rsn);
const url = getStatsURL(mode, rsn, true);
try {
const response = await httpGet<HiscoresResponse>(url, config);
return response.data;
} catch {
throw Error(PLAYER_NOT_FOUND_ERROR);
}
}
/**
* 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.
*/
export async function getRSNFormat(
rsn: string,
config?: AxiosRequestConfig
): Promise<string> {
validateRSN(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');
}
const url = getPlayerTableURL('main', rsn);
try {
const response = await httpGet<string | Buffer | BinaryData | undefined>(
url,
config
);
const response = await httpGet(url);
const dom = new JSDOM(response.data);
const anchor = dom.window.document.querySelector(
'.personal-hiscores__row.personal-hiscores__row--type-highlight a'
const spans = dom.window.document.querySelectorAll(
'span[style="color:#AA0022;"]'
);
if (anchor) {
return rsnFromElement(anchor);
if (spans.length >= 2) {
const nameSpan = spans[1];
return rsnFromElement(nameSpan);
}
throw Error(PLAYER_NOT_FOUND_ERROR);
throw Error('Player not found');
} catch {
throw Error(PLAYER_NOT_FOUND_ERROR);
throw Error('Player not found');
}
}
/**
* 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.
*/
export function parseJsonStats(json: HiscoresResponse): Stats {
const getActivity = (formattedName: string): Activity => {
const hiscoresActivity = json.activities.find(
// We must match on name here since id is not guaranteed to be the same between updates
({ name }) => name.toLowerCase() === formattedName.toLowerCase()
);
return {
rank: hiscoresActivity?.rank ?? -1,
score: hiscoresActivity?.score ?? -1
};
};
const reduceActivity = <Key extends string, Reduced = Record<Key, Activity>>(
keys: Key[],
formattedNames: Record<Key, string>
): Reduced =>
keys.reduce<Reduced>(
(reducer, key) => ({
...reducer,
[key]: getActivity(formattedNames[key])
}),
{} as Reduced
);
const skills = SKILLS.reduce<Skills>((skillsObject, skillName) => {
const hiscoresSkill = json.skills.find(
// We must match on name here since id is not guaranteed to be the same between updates
({ name }) =>
name.toLowerCase() === FORMATTED_SKILL_NAMES[skillName].toLowerCase()
);
return {
...skillsObject,
[skillName]: {
rank: hiscoresSkill?.rank ?? -1,
level: hiscoresSkill?.level ?? -1,
xp: hiscoresSkill?.xp ?? -1
}
};
}, {} as Skills);
const bountyHunter = reduceActivity(BH_MODES, FORMATTED_BH_NAMES);
const clues = reduceActivity(CLUES, FORMATTED_CLUE_NAMES);
const bosses = reduceActivity(BOSSES, FORMATTED_BOSS_NAMES);
const leaguePoints = getActivity(FORMATTED_LEAGUE_POINTS);
const lastManStanding = getActivity(FORMATTED_LMS);
const pvpArena = getActivity(FORMATTED_PVP_ARENA);
const soulWarsZeal = getActivity(FORMATTED_SOUL_WARS);
const riftsClosed = getActivity(FORMATTED_RIFTS_CLOSED);
const stats: Stats = {
skills,
leaguePoints,
bountyHunter,
lastManStanding,
pvpArena,
soulWarsZeal,
riftsClosed,
clues,
bosses
};
return stats;
}
/**
* Parses CSV string of raw stats and returns a stats object.
*
@@ -185,7 +78,11 @@ export function parseStats(csv: string): Stats {
if (
splitCSV.length !==
SKILLS.length + BH_MODES.length + CLUES.length + BOSSES.length + 5
SKILLS.length +
BH_MODES.length +
CLUES.length +
BOSSES.length +
EXTRA_ACTIVITY_COUNT
) {
throw Error(INVALID_FORMAT_ERROR);
}
@@ -216,8 +113,10 @@ export function parseStats(csv: string): Stats {
const [leaguePoints] = activityObjects.splice(0, 1);
const bhObjects = activityObjects.splice(0, BH_MODES.length);
const clueObjects = activityObjects.splice(0, CLUES.length);
const [lastManStanding, pvpArena, soulWarsZeal, riftsClosed] =
activityObjects.splice(0, 4);
const [lastManStanding, soulWarsZeal, riftsClosed] = activityObjects.splice(
0,
3
);
const bossObjects = activityObjects.splice(0, BOSSES.length);
const skills: Skills = skillObjects.reduce<Skills>((prev, curr, index) => {
@@ -249,7 +148,6 @@ export function parseStats(csv: string): Stats {
leaguePoints,
bountyHunter,
lastManStanding,
pvpArena,
soulWarsZeal,
riftsClosed,
clues,
@@ -268,44 +166,25 @@ export function parseStats(csv: string): Stats {
* @param rsn Username of the player.
* @returns Player object.
*/
export async function getStats(
rsn: string,
options?: GetStatsOptions
): Promise<Player> {
validateRSN(rsn);
const otherGamemodes = options?.otherGamemodes ?? [
'ironman',
'hardcore',
'ultimate'
];
const shouldGetFormattedRsn = options?.shouldGetFormattedRsn ?? true;
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');
}
const mainRes = await httpGet<HiscoresResponse>(
getStatsURL('main', rsn, true),
options?.axiosConfigs?.main
);
const mainRes = await httpGet(getStatsURL('main', rsn));
if (mainRes.status === 200) {
const emptyResponse: AxiosResponse<HiscoresResponse> = {
status: 404,
data: { skills: [], activities: [] },
statusText: '',
headers: {},
config: {}
};
const getModeStats = async (
mode: Extract<Gamemode, 'ironman' | 'hardcore' | 'ultimate'>
): Promise<AxiosResponse<HiscoresResponse>> =>
otherGamemodes.includes(mode)
? httpGet<HiscoresResponse>(
getStatsURL(mode, rsn, true),
options?.axiosConfigs?.[mode]
).catch((err) => err)
: emptyResponse;
const formattedName = shouldGetFormattedRsn
? await getRSNFormat(rsn, options?.axiosConfigs?.rsn).catch(
() => undefined
)
: undefined;
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 player: Player = {
name: formattedName ?? rsn,
@@ -314,16 +193,13 @@ export async function getStats(
deulted: false,
deironed: false
};
player.main = parseJsonStats(mainRes.data);
player.main = parseStats(mainRes.data);
const ironRes = await getModeStats('ironman');
if (ironRes.status === 200) {
player.ironman = parseJsonStats(ironRes.data);
const hcRes = await getModeStats('hardcore');
const ultRes = await getModeStats('ultimate');
player.ironman = parseStats(ironRes.data);
if (hcRes.status === 200) {
player.mode = 'hardcore';
player.hardcore = parseJsonStats(hcRes.data);
player.hardcore = parseStats(hcRes.data);
if (
player.ironman.skills.overall.xp !== player.hardcore.skills.overall.xp
) {
@@ -338,7 +214,7 @@ export async function getStats(
}
} else if (ultRes.status === 200) {
player.mode = 'ultimate';
player.ultimate = parseJsonStats(ultRes.data);
player.ultimate = parseStats(ultRes.data);
if (
player.ironman.skills.overall.xp !== player.ultimate.skills.overall.xp
) {
@@ -364,7 +240,7 @@ export async function getStats(
return player;
}
throw Error(PLAYER_NOT_FOUND_ERROR);
throw Error('Player not found');
}
/**
@@ -372,20 +248,26 @@ export async function getStats(
*
* @param rsn Username of the player.
* @param mode Gamemode to fetch ranks for.
* @param config Optional axios request config object.
* @returns Stats object.
*/
export async function getStatsByGamemode(
rsn: string,
mode: Gamemode = 'main',
config?: AxiosRequestConfig
mode: Gamemode = 'main'
): Promise<Stats> {
validateRSN(rsn);
if (!GAMEMODES.includes(mode)) {
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)) {
throw Error('Invalid game mode');
}
const response = await getOfficialStats(rsn, mode, config);
const stats = parseJsonStats(response);
const response = await httpGet(getStatsURL(mode, rsn));
if (response.status !== 200) {
throw Error('Player not found');
}
const stats = parseStats(response.data);
return stats;
}
@@ -393,8 +275,7 @@ export async function getStatsByGamemode(
export async function getSkillPage(
skill: SkillName,
mode: Gamemode = 'main',
page: number = 1,
config?: AxiosRequestConfig
page: number = 1
): Promise<PlayerSkillRow[]> {
if (!GAMEMODES.includes(mode)) {
throw Error('Invalid game mode');
@@ -405,10 +286,7 @@ export async function getSkillPage(
}
const url = getSkillPageURL(mode, skill, page);
const response = await httpGet<string | Buffer | BinaryData | undefined>(
url,
config
);
const response = await httpGet(url);
const dom = new JSDOM(response.data);
const playersHTML = dom.window.document.querySelectorAll(
'.personal-hiscores__row'
@@ -440,14 +318,12 @@ export async function getSkillPage(
* @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.
*/
export async function getActivityPage(
activity: ActivityName,
mode: Gamemode = 'main',
page: number = 1,
config?: AxiosRequestConfig
page: number = 1
): Promise<PlayerActivityRow[]> {
if (!GAMEMODES.includes(mode)) {
throw Error('Invalid game mode');
@@ -458,10 +334,7 @@ export async function getActivityPage(
}
const url = getActivityPageURL(mode, activity, page);
const response = await httpGet<string | Buffer | BinaryData | undefined>(
url,
config
);
const response = await httpGet(url);
const dom = new JSDOM(response.data);
const playersHTML = dom.window.document.querySelectorAll(
'.personal-hiscores__row'

View File

@@ -1,5 +1,3 @@
import { AxiosRequestConfig } from 'axios';
export type Gamemode =
| 'main'
| 'ironman'
@@ -7,10 +5,7 @@ export type Gamemode =
| 'hardcore'
| 'deadman'
| 'seasonal'
| 'tournament'
| 'skiller'
| 'oneDefence'
| 'freshStart';
| 'tournament';
export interface Skill {
rank: number;
@@ -62,18 +57,16 @@ export type ClueType =
export type Clues = { [Type in ClueType]: Activity };
export type BHType = 'rogue' | 'hunter' | 'rogueV2' | 'hunterV2';
export type BHType = 'rogue' | 'hunter';
export type BH = { [Type in BHType]: Activity };
export type Boss =
| 'abyssalSire'
| 'alchemicalHydra'
| 'artio'
| 'barrows'
| 'bryophyta'
| 'callisto'
| 'calvarion'
| 'cerberus'
| 'chambersOfXeric'
| 'chambersOfXericChallengeMode'
@@ -86,7 +79,6 @@ export type Boss =
| 'dagannothRex'
| 'dagannothSupreme'
| 'derangedArchaeologist'
| 'dukeSucellus'
| 'generalGraardor'
| 'giantMole'
| 'grotesqueGuardians'
@@ -101,24 +93,17 @@ export type Boss =
| 'nightmare'
| 'phosanisNightmare'
| 'obor'
| 'phantomMuspah'
| 'sarachnis'
| 'scorpia'
| 'skotizo'
| 'spindel'
| 'tempoross'
| 'gauntlet'
| 'corruptedGauntlet'
| 'leviathan'
| 'whisperer'
| 'theatreOfBlood'
| 'theatreOfBloodHardMode'
| 'thermonuclearSmokeDevil'
| 'tombsOfAmascut'
| 'tombsOfAmascutExpertMode'
| 'tzKalZuk'
| 'tzTokJad'
| 'vardorvis'
| 'venenatis'
| 'vetion'
| 'vorkath'
@@ -130,12 +115,9 @@ export type Bosses = { [Type in Boss]: Activity };
export type ActivityName =
| 'leaguePoints'
| 'hunterBHV2'
| 'rogueBHV2'
| 'hunterBH'
| 'rogueBH'
| 'lastManStanding'
| 'pvpArena'
| 'soulWarsZeal'
| 'riftsClosed'
| 'allClues'
@@ -153,7 +135,6 @@ export interface Stats {
leaguePoints: Activity;
bountyHunter: BH;
lastManStanding: Activity;
pvpArena: Activity;
soulWarsZeal: Activity;
riftsClosed: Activity;
bosses: Bosses;
@@ -177,63 +158,3 @@ 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;
/**
* Map of configs for each requests that can take place in the `getStats` function.
*/
axiosConfigs?: Partial<Record<Gamemode, AxiosRequestConfig>> & {
/**
* The axios request config object to use for the RSN format request.
*/
rsn?: AxiosRequestConfig;
};
}
export interface HiscoresCommon {
/**
* This field behaves more like an index than a true unique ID.
*/
id: number;
/**
* The display name of this skill / activity.
*/
name: string;
/**
* The player's official hiscores rank in this skill / activity.
*/
rank: number;
}
export interface HiscoresSkill extends HiscoresCommon {
/**
* The player's current level in this skill.
*/
level: number;
/**
* The player's current experience in this skill.
*/
xp: number;
}
export interface HiscoresActivity extends HiscoresCommon {
/**
* The player's current score in this activity.
*/
score: number;
}
export interface HiscoresResponse {
skills: HiscoresSkill[];
activities: HiscoresActivity[];
}

View File

@@ -9,7 +9,6 @@ import {
export const BASE_URL = 'https://secure.runescape.com/m=hiscore_oldschool';
export const STATS_URL = 'index_lite.ws?player=';
export const JSON_STATS_URL = 'index_lite.json?player=';
export const SCORES_URL = 'overall.ws?';
export type GamemodeUrl = {
@@ -23,10 +22,7 @@ export const GAMEMODE_URL: GamemodeUrl = {
ultimate: `${BASE_URL}_ultimate/`,
deadman: `${BASE_URL}_deadman/`,
seasonal: `${BASE_URL}_seasonal/`,
tournament: `${BASE_URL}_tournament/`,
skiller: `${BASE_URL}_skiller/`,
oneDefence: `${BASE_URL}_skiller_defence/`,
freshStart: `${BASE_URL}_fresh_start/`
tournament: `${BASE_URL}_tournament/`
};
export const SKILLS: SkillName[] = [
'overall',
@@ -63,7 +59,7 @@ export const CLUES: ClueType[] = [
'elite',
'master'
];
export const BH_MODES: BHType[] = ['hunterV2', 'rogueV2', 'hunter', 'rogue'];
export const BH_MODES: BHType[] = ['hunter', 'rogue'];
export const GAMEMODES: Gamemode[] = [
'main',
'ironman',
@@ -76,11 +72,9 @@ export const GAMEMODES: Gamemode[] = [
export const BOSSES: Boss[] = [
'abyssalSire',
'alchemicalHydra',
'artio',
'barrows',
'bryophyta',
'callisto',
'calvarion',
'cerberus',
'chambersOfXeric',
'chambersOfXericChallengeMode',
@@ -93,7 +87,6 @@ export const BOSSES: Boss[] = [
'dagannothRex',
'dagannothSupreme',
'derangedArchaeologist',
'dukeSucellus',
'generalGraardor',
'giantMole',
'grotesqueGuardians',
@@ -108,24 +101,17 @@ export const BOSSES: Boss[] = [
'nightmare',
'phosanisNightmare',
'obor',
'phantomMuspah',
'sarachnis',
'scorpia',
'skotizo',
'spindel',
'tempoross',
'gauntlet',
'corruptedGauntlet',
'leviathan',
'whisperer',
'theatreOfBlood',
'theatreOfBloodHardMode',
'thermonuclearSmokeDevil',
'tombsOfAmascut',
'tombsOfAmascutExpertMode',
'tzKalZuk',
'tzTokJad',
'vardorvis',
'venenatis',
'vetion',
'vorkath',
@@ -135,8 +121,6 @@ export const BOSSES: Boss[] = [
];
export const ACTIVITIES: ActivityName[] = [
'leaguePoints',
'hunterBHV2',
'rogueBHV2',
'hunterBH',
'rogueBH',
'allClues',
@@ -147,7 +131,6 @@ export const ACTIVITIES: ActivityName[] = [
'eliteClues',
'masterClues',
'lastManStanding',
'pvpArena',
'soulWarsZeal',
'riftsClosed',
...BOSSES
@@ -160,11 +143,9 @@ 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: "Calvar'ion",
cerberus: 'Cerberus',
chambersOfXeric: 'Chambers of Xeric',
chambersOfXericChallengeMode: 'Chambers of Xeric: Challenge Mode',
@@ -177,7 +158,6 @@ export const FORMATTED_BOSS_NAMES: FormattedBossNames = {
dagannothRex: 'Dagannoth Rex',
dagannothSupreme: 'Dagannoth Supreme',
derangedArchaeologist: 'Deranged Archaeologist',
dukeSucellus: 'Duke Sucellus',
generalGraardor: 'General Graardor',
giantMole: 'Giant Mole',
grotesqueGuardians: 'Grotesque Guardians',
@@ -189,27 +169,20 @@ export const FORMATTED_BOSS_NAMES: FormattedBossNames = {
krilTsutsaroth: "K'ril Tsutsaroth",
mimic: 'Mimic',
nex: 'Nex',
nightmare: 'Nightmare',
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',
leviathan: 'The Leviathan',
whisperer: 'The Whisperer',
theatreOfBlood: 'Theatre of Blood',
theatreOfBloodHardMode: 'Theatre of Blood: Hard Mode',
thermonuclearSmokeDevil: 'Thermonuclear Smoke Devil',
tombsOfAmascut: 'Tombs of Amascut',
tombsOfAmascutExpertMode: 'Tombs of Amascut: Expert Mode',
tzKalZuk: 'TzKal-Zuk',
tzTokJad: 'TzTok-Jad',
vardorvis: 'Vardorvis',
venenatis: 'Venenatis',
vetion: "Vet'ion",
vorkath: 'Vorkath',
@@ -268,17 +241,18 @@ export type FormattedBHNames = {
};
export const FORMATTED_BH_NAMES: FormattedBHNames = {
rogue: 'Bounty Hunter (Legacy) - Rogue',
hunter: 'Bounty Hunter (Legacy) - Hunter',
rogueV2: 'Bounty Hunter - Rogue',
hunterV2: 'Bounty Hunter - Hunter'
rogue: 'Bounty Hunter - Rogue',
hunter: 'Bounty Hunter - Hunter'
};
export const FORMATTED_LMS = 'LMS - Rank';
export const FORMATTED_PVP_ARENA = 'PvP Arena - Rank';
export const FORMATTED_LMS = 'Last Man Standing';
export const FORMATTED_SOUL_WARS = 'Soul Wars Zeal';
export const FORMATTED_LEAGUE_POINTS = 'League Points';
export const FORMATTED_RIFTS_CLOSED = 'Rifts closed';
export const FORMATTED_RIFTS_CLOSED = 'Rifts Closed';
/**
* Count of activities not including bosses, bounty hunter, or clues.
*/
export const EXTRA_ACTIVITY_COUNT = 4;
export const INVALID_FORMAT_ERROR = 'Invalid hiscores format';
export const PLAYER_NOT_FOUND_ERROR = 'Player not found';

View File

@@ -1,4 +1,4 @@
import axios, { AxiosRequestConfig } from 'axios';
import axios from 'axios';
import * as ua from 'useragent-generator';
import { Gamemode, SkillName, ActivityName } from '../types';
import {
@@ -6,8 +6,7 @@ import {
STATS_URL,
SCORES_URL,
SKILLS,
ACTIVITIES,
JSON_STATS_URL
ACTIVITIES
} from './constants';
/**
@@ -15,13 +14,10 @@ import {
*
* @param gamemode Gamemode to fetch ranks for.
* @param rsn Username of the player.
* @param json If the JSON endpoint is desired instead of CSV.
* @returns Encoded stats URL.
*/
export const getStatsURL = (gamemode: Gamemode, rsn: string, json = false) =>
`${GAMEMODE_URL[gamemode]}${
json ? JSON_STATS_URL : STATS_URL
}${encodeURIComponent(rsn)}`;
export const getStatsURL = (gamemode: Gamemode, rsn: string) =>
`${GAMEMODE_URL[gamemode]}${STATS_URL}${encodeURIComponent(rsn)}`;
/**
* Will generate a player table URL for the official OSRS hiscores website.
@@ -100,29 +96,10 @@ export const rsnFromElement = (el: Element | null) => {
* @param url URL to run a `GET` request against.
* @returns Axios response.
*/
export const httpGet = <Response>(
url: string,
config: AxiosRequestConfig = {}
) =>
axios.get<Response>(url, {
export const httpGet = (url: string) =>
axios.get(url, {
headers: {
// without User-Agent header requests may be rejected by DDoS protection mechanism
'User-Agent': ua.firefox(80)
},
...config
}
});
/**
* 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');
}
};

5389
yarn.lock

File diff suppressed because it is too large Load Diff