Skip to content

Commit

Permalink
feat(pvpteam): add support for fetching the name and ID of a pvp team…
Browse files Browse the repository at this point in the history
… associated to a character

Implements #25
  • Loading branch information
ReidWeb committed Jan 2, 2022
1 parent ee12e9b commit cdc2f0b
Show file tree
Hide file tree
Showing 14 changed files with 1,496 additions and 35 deletions.
1 change: 1 addition & 0 deletions docs/api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@
### Modules

- [client/LodestoneClient](modules/client_LodestoneClient.md)
- [interface/IPlayerGroup](modules/interface_IPlayerGroup.md)
7 changes: 4 additions & 3 deletions docs/api/classes/client_LodestoneClient.default.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,19 @@ Client for interfacing with the Final Fantasy XIV Lodestone.

### constructor

**new default**(`axiosInstance?`, `cheerioInstance?`)
**new default**(`axiosInstance?`, `cheerioInstance?`, `parallelismLimit?`)

#### Parameters

| Name | Type |
| :------ | :------ |
| `axiosInstance?` | `AxiosInstance` |
| `cheerioInstance?` | `CheerioAPI` |
| `parallelismLimit?` | `number` |

#### Defined in

[LodestoneClient.ts:55](https://github.com/XIVStats/lodestone/blob/31dd347/src/client/LodestoneClient.ts#L55)
[client/LodestoneClient.ts:56](https://github.com/XIVStats/lodestone/blob/ee12e9b/src/client/LodestoneClient.ts#L56)

## Properties

Expand All @@ -43,4 +44,4 @@ An instance will be generated by default, but as a consumer you can provide your

#### Defined in

[LodestoneClient.ts:51](https://github.com/XIVStats/lodestone/blob/31dd347/src/client/LodestoneClient.ts#L51)
[client/LodestoneClient.ts:50](https://github.com/XIVStats/lodestone/blob/ee12e9b/src/client/LodestoneClient.ts#L50)
5 changes: 5 additions & 0 deletions docs/api/interfaces/interface_IPlayerGroup.IPlayerGroup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Interface: IPlayerGroup

[interface/IPlayerGroup](../modules/interface_IPlayerGroup.md).IPlayerGroup

A PlayerGroup is a group of players such as a Free Company or PvP Team.
7 changes: 7 additions & 0 deletions docs/api/modules/interface_IPlayerGroup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Module: interface/IPlayerGroup

## Table of contents

### Interfaces

- [IPlayerGroup](../interfaces/interface_IPlayerGroup.IPlayerGroup.md)
47 changes: 26 additions & 21 deletions src/client/LodestoneClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,14 @@

import Axios, { AxiosError, AxiosInstance } from 'axios'
import Cheerio, { CheerioAPI } from 'cheerio'
import pLimit from 'p-limit'
import pLimit, { Limit } from 'p-limit'
import Character from '../entity/Character'
import Servers from '../entity/Servers'
import CharacterNotFoundError from '../errors/CharacterNotFoundError'
import { ICharacterFetchError } from '../interface/ICharacterFetchError'
import CharacterFetchError from '../errors/CharacterFetchError'
import CharacterFetchTimeoutError from '../errors/CharacterFetchTimeoutError'
import ICharacterSetFetchResult from '../interface/ICharacterSetFetchResult'
import IItem from '../interface/IItem'
import Creature from '../entity/Creature'

export type OnSuccessFunction = (id: number, character?: Character) => void
Expand All @@ -52,14 +51,17 @@ export default class LodestoneClient {

cheerioInstance: CheerioAPI

public constructor(axiosInstance?: AxiosInstance, cheerioInstance?: CheerioAPI) {
limit: Limit

public constructor(axiosInstance?: AxiosInstance, cheerioInstance?: CheerioAPI, parallelismLimit?: number) {
this.axiosInstance =
axiosInstance ||
Axios.create({
baseURL: 'https://eu.finalfantasyxiv.com/lodestone',
timeout: 5000,
})
this.cheerioInstance = cheerioInstance || Cheerio
this.limit = pLimit(parallelismLimit || 10)
}

public async getCharacter(id: number): Promise<Character> {
Expand All @@ -82,35 +84,39 @@ export default class LodestoneClient {
}
}

public async getCharacterMounts(id: number, fetchNames?: boolean): Promise<IItem[]> {
// eslint-disable-next-line no-useless-catch
try {
const response = await this.axiosInstance.get(`/character/${id}/mount/`)
const tooltipUrls = Character.getMountTooltipUrlsFromPage(id, response.data, this.cheerioInstance)
return []
} catch (e) {
throw e
}
}
// public async getCharacterMounts(id: number, itemIdsOnly?: boolean): Promise<Creature[]> {
// // eslint-disable-next-line no-useless-catch
// try {
// const response = await this.axiosInstance.get(`/character/${id}/mount/`)
// // TODO: break off below to allow pre-flight function for smarter lookup
// const tooltipUrls = Character.getMountTooltipUrlsFromPage(id, response.data, this.cheerioInstance)
// const promises: Promise<Creature>[] = []
// tooltipUrls.forEach((url) => {
// promises.push(this.limit(() => this.getCreatureToolTip(url)))
// })
// // TODO: work on this more
// const results = await Promise.allSettled(promises)
// return []
// } catch (e) {
// throw e
// }
// }

public async getCreatureToolTip(path: string, fetchName?: string): Promise<Creature> {
public async getCreatureToolTip(path: string, itemIdsOnly?: boolean): Promise<Creature> {
const shortenedPath = path.replace('/lodestone', '')
const response = await this.axiosInstance.get(shortenedPath)
return Creature.fromToolTip(response.data, this.cheerioInstance)
return Creature.fromToolTip(path.split('/tooltip/')[1], response.data, this.cheerioInstance, itemIdsOnly)
}

public async getCharacters(
characterIds: number[],
parallelismLimit: number,
onSuccess?: OnSuccessFunction,
onDeleted?: OnSuccessFunction,
onError?: OnErrorFunction
): Promise<ICharacterSetFetchResult> {
const limit = pLimit(parallelismLimit)

const promises: Promise<Character>[] = []
characterIds.forEach((currentId) => {
promises.push(limit(() => this.getCharacter(currentId)))
promises.push(this.limit(() => this.getCharacter(currentId)))
})
const successfulCharacters: Character[] = []
const failedCharacters: ICharacterFetchError[] = []
Expand Down Expand Up @@ -147,7 +153,6 @@ export default class LodestoneClient {
public async getCharacterRange(
start: number,
end: number,
parallelismLimit: number,
onSuccess?: OnSuccessFunction,
onDeleted?: OnSuccessFunction,
onError?: OnErrorFunction
Expand All @@ -156,7 +161,7 @@ export default class LodestoneClient {
for (let currentId = start; currentId < end; currentId += 1) {
ids.push(currentId)
}
return this.getCharacters(ids, parallelismLimit, onSuccess, onDeleted, onError)
return this.getCharacters(ids, onSuccess, onDeleted, onError)
}

public async getServers(loadCategory?: boolean, loadStatus?: boolean): Promise<Servers> {
Expand Down
6 changes: 5 additions & 1 deletion src/config/DomConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,11 @@ export default class DomConfig {
'#character > div.frame__chara.js__toggle_wrapper > a > div.frame__chara__box > p.frame__chara__world',
transformationFunction: (value: string) => value.split('(')[1].replace(')', '').trim(),
},
freeCompany: 'div.character__freecompany__name > h4',
freeCompanyName: 'div.character__freecompany__name > h4',
pvpTeam: {
selector: 'div.character__pvpteam__name > h4',
isGroupLink: true,
},
gear: {},
gender: {
selector:
Expand Down
25 changes: 23 additions & 2 deletions src/entity/Character.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ import Class from './Class'
import GearCategory from './GearCategory'
import Level from './Level'
import IItem from '../interface/IItem'
import { IPlayerGroup } from '../interface/IPlayerGroup'
import UnparseableGroupIdError from '../errors/UnparseableGroupIdError'

export default class Character implements ICharacter {
constructor(readonly id: number, readonly name: string) {}
Expand Down Expand Up @@ -65,13 +67,18 @@ export default class Character implements ICharacter {

grandCompanyRank?: string | undefined

freeCompany?: string | undefined
freeCompanyName?: string | undefined

pvpTeam?: IPlayerGroup | undefined

minionIds?: string[] | undefined

mounts?: IItem[] | undefined

private static processAttribute($: CheerioAPI, config: string | IAttributeMapping | undefined): string | undefined {
private static processAttribute(
$: CheerioAPI,
config: string | IAttributeMapping | undefined
): string | IPlayerGroup | undefined {
if (typeof config === 'object') {
const transformConfig: IAttributeMapping = config

Expand All @@ -83,6 +90,20 @@ export default class Character implements ICharacter {
if (text === undefined) {
throw new Error()
}
} else if (config.isGroupLink) {
const href = $(`${config.selector} > a`)?.attr('href')
if (href) {
const id = href.split('/')[3]
if (!id) {
throw new UnparseableGroupIdError($(`${config.selector} > a`).attr('href'))
} else
return {
id,
name: $(config.selector).text(),
}
} else {
return undefined
}
} else {
text = $(config.selector).text()
}
Expand Down
41 changes: 35 additions & 6 deletions src/entity/__tests__/Character.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ import Cheerio from 'cheerio'
import Character from '../Character'
import Class from '../Class'
import GearCategory from '../GearCategory'
import IItem from '../../interface/IItem'

describe('Character', () => {
describe('when loading character information from HTML', () => {
Expand All @@ -46,7 +45,7 @@ describe('Character', () => {
guardian: 'Thaliak, the Scholar',
grandCompany: 'Order of the Twin Adder',
grandCompanyRank: 'First Serpent Lieutenant',
freeCompany: 'Archadian Moogles',
freeCompanyName: 'Archadian Moogles',
activeClass: Class.Conjurer,
gear: {
arm: {
Expand Down Expand Up @@ -262,7 +261,7 @@ describe('Character', () => {
nameDay: '4th Sun of the 4th Astral Moon',
guardian: 'Nymeia, the Spinner',
gender: 'Female',
freeCompany: "Gandalf's Gangstas",
freeCompanyName: "Gandalf's Gangstas",
activeClass: Class.Arcanist,
grandCompany: undefined,
}
Expand All @@ -277,7 +276,7 @@ describe('Character', () => {
homeWorld: 'Cerberus',
title: 'Outlander',
dataCenter: 'Chaos',
freeCompany: 'Archadian Moogles',
freeCompanyName: 'Archadian Moogles',
gear: {
shield: {
category: GearCategory.Shield,
Expand All @@ -304,7 +303,7 @@ describe('Character', () => {
homeWorld: 'Cerberus',
title: 'Monster Hunter',
dataCenter: 'Chaos',
freeCompany: 'Archadian Moogles',
freeCompanyName: 'Archadian Moogles',
gender: 'Male',
grandCompany: 'Order of the Twin Adder',
grandCompanyRank: 'Serpent Captain',
Expand All @@ -329,7 +328,7 @@ describe('Character', () => {
homeWorld: 'Cerberus',
title: 'The Liberator',
dataCenter: 'Chaos',
freeCompany: 'Archadian Moogles',
freeCompanyName: 'Archadian Moogles',
gender: 'Female',
grandCompany: 'Maelstrom',
grandCompanyRank: 'Second Storm Lieutenant',
Expand All @@ -344,6 +343,35 @@ describe('Character', () => {
},
}

// Character with PvP team, Free Company, Grand Company
const expectedCharacterSix: Character = {
id: 8283,
name: 'Windows Vista',
activeClass: Class.Armorer,
cityState: "Ul'dah",
clan: 'Plainsfolk',
homeWorld: 'Aegis',
title: 'Peacemaker',
dataCenter: 'Elemental',
freeCompanyName: 'DawnGarden',
gender: 'Male',
grandCompany: 'Immortal Flames',
grandCompanyRank: 'Flame Captain',
guardian: 'Rhalgr, the Destroyer',
nameDay: '16th Sun of the 1st Umbral Moon',
race: 'Lalafell',
pvpTeam: {
id: '754a1662b54d9fe4d0c6beefa0e61e856fff4d10',
name: 'Hushigi Yugi',
},
classes: {
armorer: {
class: Class.Armorer,
level: 90,
},
},
}

// TODO: Test character with shield
// TODO: test character with no free company

Expand All @@ -353,6 +381,7 @@ describe('Character', () => {
[27218992, 'Shamir Kotmine', expectedCharacterThree],
[18001255, 'Sey Moore', expectedCharacterFour],
[28309293, 'Refler Desu', expectedCharacterFive],
[8283, 'Windows Vista', expectedCharacterSix],
])('for character %s - %s', (charId, name, expected) => {
let resultantCharacter: Character
const nonObjectAttributes = Object.entries(expected).filter((pair) => typeof pair[1] !== 'object')
Expand Down
Loading

0 comments on commit cdc2f0b

Please sign in to comment.