Skip to content

Commit

Permalink
I should really check this stuff in more often
Browse files Browse the repository at this point in the history
  • Loading branch information
GravlLift committed Aug 24, 2023
1 parent 07ad868 commit e6c483f
Show file tree
Hide file tree
Showing 19 changed files with 382 additions and 19 deletions.
23 changes: 23 additions & 0 deletions .github/workflows/npm-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
# For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages

name: Node.js Package

on:
release:
types: [published]

jobs:
publish-npm:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16
registry-url: https://registry.npmjs.org/
- run: npm ci
- run: npm publish
env:
NODE_AUTH_TOKEN: ${{secrets.npm_token}}
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ The package is currently limited to the endpoints I've needed to use in other pr

- GET https://profile.svc.halowaypoint.com/users/{gamerTag}
- GET https://skill.svc.halowaypoint.com/hi/playlist/{playlistId}/csrs?players={playerIds}
- GET https://halostats.svc.halowaypoint.com/hi/playlist/{playlistId}/csrs?players={playerIds}
- GET https://gamecms-hacs.svc.halowaypoint.com/hi/multiplayer/file/playlists/assets/{playlistId}.json
- GET https://halostats.svc.halowaypoint.com/hi/playlist/{playlistId}/csrs?players={playerIds}
- GET https://halostats.svc.halowaypoint.com/hi/players/xuid({playerId})/matches
- GET https://skill.svc.halowaypoint.com/hi/matches/{matchId}/skill

### Getting Started

Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"name": "halo-infinite-api",
"version": "1.0.2",
"type": "module",
"version": "1.0.3",
"description": "An NPM package for accessing the official Halo Infinite API.",
"main": "dist/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"prepack": "tsc --project tsconfig.app.json"
},
"repository": {
Expand Down
20 changes: 14 additions & 6 deletions src/authentication/halo-authentication-client.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import axios, { AxiosInstance } from "axios";
import axios from "axios";
import { DateTime } from "luxon";
import type { SpartanToken } from "../models/spartan-token";
import type { SpartanTokenRequest } from "../models/spartan-token-request";
import { DateTime } from "luxon";
import { coalesceDateTime } from "../util/date-time";

export interface Token {
token: string;
Expand All @@ -13,7 +14,10 @@ export class HaloAuthenticationClient {

constructor(
private readonly fetchXstsToken: () => Promise<string>,
private readonly loadToken: () => Promise<Token | null>,
private readonly loadToken: () => Promise<{
token?: string;
expiresAt?: unknown;
} | null>,
private readonly saveToken: (token: Token) => Promise<void>
) {}

Expand Down Expand Up @@ -56,11 +60,15 @@ export class HaloAuthenticationClient {
});

try {
const currentToken = await this.loadToken();
const loadedToken = await this.loadToken();
const currentToken = {
token: loadedToken?.token ?? "",
expiresAt: coalesceDateTime(loadedToken?.expiresAt),
};

if (currentToken && currentToken.expiresAt > DateTime.now()) {
if (currentToken.expiresAt && currentToken.expiresAt > DateTime.now()) {
// Current token is valid, return it and alert other callers if applicable
promiseResolver(currentToken);
promiseResolver(currentToken as Token);
return currentToken.token;
} else {
const xstsToken = await this.fetchXstsToken();
Expand Down
24 changes: 20 additions & 4 deletions src/authentication/xbox-authentication-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,15 @@ import axios, { AxiosInstance } from "axios";
import getPkce from "oauth-pkce";
import { DateTime } from "luxon";
import { XboxTicket } from "../models/xbox-ticket";
import { coalesceDateTime } from "../util/date-time";

const SCOPES = ["Xboxlive.signin", "Xboxlive.offline_access"];
// polyfill crypto for oauth-pkce
if (!globalThis.window) globalThis.window = {} as any;
if (!globalThis.window.crypto) {
globalThis.window.crypto = (await import("node:crypto"))
.webcrypto as typeof globalThis.window.crypto;
}

export enum RelyingParty {
Xbox = "http://xboxlive.com",
Expand All @@ -25,7 +32,11 @@ export class XboxAuthenticationClient {
private readonly clientId: string,
private readonly redirectUri: string,
private readonly getAuthCode: (authorizeUrl: string) => Promise<string>,
private readonly loadToken: () => Promise<XboxAuthenticationToken | null>,
private readonly loadToken: () => Promise<{
token?: string;
expiresAt?: unknown;
refreshToken?: string;
} | null>,
private readonly saveToken: (
token: XboxAuthenticationToken
) => Promise<void>
Expand Down Expand Up @@ -92,11 +103,16 @@ export class XboxAuthenticationClient {
);

try {
const currentToken = await this.loadToken();
const loadedToken = await this.loadToken();
const currentToken = {
...loadedToken,
token: loadedToken?.token ?? "",
expiresAt: coalesceDateTime(loadedToken?.expiresAt),
};

if (currentToken && currentToken.expiresAt > DateTime.now()) {
if (currentToken.expiresAt && currentToken.expiresAt > DateTime.now()) {
// Current token is valid, return it and alert other callers if applicable
promiseResolver(currentToken);
promiseResolver(currentToken as XboxAuthenticationToken);
return currentToken.token;
} else {
const newToken = await this.fetchOauth2Token();
Expand Down
62 changes: 60 additions & 2 deletions src/core/halo-infinite-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ import {
} from "../authentication/xbox-authentication-client";
import { HaloAuthenticationClient } from "../authentication/halo-authentication-client";
import { Playlist } from "../models/halo-infinite/playlist";
import { MatchType } from "../models/halo-infinite/match-type";
import { MatchStats } from "src/models/halo-infinite/match-stats";
import { PlayerMatchHistory } from "src/models/halo-infinite/player-match-history";
import { MatchSkill } from "src/models/halo-infinite/match-skill";

interface ResultContainer<TValue> {
Id: string;
Expand All @@ -23,6 +27,13 @@ interface ResultsContainer<TValue> {
Value: ResultContainer<TValue>[];
}

interface PaginationContainer<TValue> {
Start: number;
Count: number;
ResultCount: number;
Results: TValue[];
}

interface TokenPersister {
load: <T>(tokenName: string) => Promise<T>;
save: (tokenName: string, token: unknown) => Promise<void>;
Expand Down Expand Up @@ -111,20 +122,39 @@ export class HaloInfiniteClient {
return response.data;
}

private async executeArrayRequest<T>(
private async executeResultsRequest<T>(
...args: Parameters<HaloInfiniteClient["executeRequest"]>
) {
const result = await this.executeRequest<ResultsContainer<T>>(...args);

return result.Value;
}

private async executePaginationRequest<T>(
count: number,
start: number,
queryParameters: Record<string, string>,
...args: Parameters<HaloInfiniteClient["executeRequest"]>
) {
const [url, ...rest] = args;
const result = await this.executeRequest<PaginationContainer<T>>(
`${url}?${new URLSearchParams({
...queryParameters,
count: count.toString(),
start: start.toString(),
})}`,
...rest
);

return result.Results;
}

/** Gets playlist Competitive Skill Rank (CSR) for a player or a set of players.
* @param playlistId - Unique ID for the playlist.
* @param playerIds - Array of player xuids.
*/
public getPlaylistCsr = (playlistId: string, playerIds: string[]) =>
this.executeArrayRequest<PlaylistCsrContainer>(
this.executeResultsRequest<PlaylistCsrContainer>(
`https://${HaloCoreEndpoints.SkillOrigin}.${
HaloCoreEndpoints.ServiceDomain
}/hi/playlist/${playlistId}/csrs?players=xuid(${playerIds.join(
Expand Down Expand Up @@ -159,4 +189,32 @@ export class HaloInfiniteClient {
`https://${HaloCoreEndpoints.GameCmsOrigin}.${HaloCoreEndpoints.ServiceDomain}/hi/multiplayer/file/playlists/assets/${playlistId}.json`,
"get"
);

public getPlayerMatches = (
playerXuid: string,
type: MatchType = MatchType.All,
count: number = 25,
start: number = 0
) =>
this.executePaginationRequest<PlayerMatchHistory>(
count,
start,
{ type: type.toString() },
`https://${HaloCoreEndpoints.StatsOrigin}.${HaloCoreEndpoints.ServiceDomain}/hi/players/xuid(${playerXuid})/matches`,
"get"
);

public getMatchStats = (matchId: string) =>
this.executeRequest<MatchStats>(
`https://${HaloCoreEndpoints.StatsOrigin}.${HaloCoreEndpoints.ServiceDomain}/hi/matches/${matchId}/stats`,
"get"
);

public getMatchSkill = (matchId: string, playerIds: string[]) =>
this.executeResultsRequest<MatchSkill>(
`https://${HaloCoreEndpoints.SkillOrigin}.${
HaloCoreEndpoints.ServiceDomain
}/hi/matches/${matchId}/skill?players=xuid(${playerIds.join("),xuid(")})`,
"get"
);
}
5 changes: 5 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,8 @@ export { Playlist } from "./models/halo-infinite/playlist";
export { PlaylistCsrContainer } from "./models/halo-infinite/playlist-csr-container";
export { UserInfo } from "./models/halo-infinite/user-info";
export { ServiceRecord } from "./models/halo-infinite/service-record";
export { MatchType } from "./models/halo-infinite/match-type";
export { GameVariantCategory } from "./models/halo-infinite/game-variant-category";
export { MatchStats } from "./models/halo-infinite/match-stats";
export { PlayerMatchHistory } from "./models/halo-infinite/player-match-history";
export { Stats } from "./models/halo-infinite/stats";
11 changes: 11 additions & 0 deletions src/models/halo-infinite/game-variant-category.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export enum GameVariantCategory {
MultiplayerSlayer = 6,
MultiplayerAttrition = 7,
MultiplayerFiesta = 9,
MultiplayerStrongholds = 11,
MultiplayerKingOfTheHill = 12,
MultiplayerCtf = 15,
MultiplayerOddball = 18,
MultiplayerGrifball = 25,
MultiplayerLandGrab = 39,
}
38 changes: 38 additions & 0 deletions src/models/halo-infinite/match-info.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { GameVariantCategory } from "./game-variant-category";
import { PlaylistExperience } from "./playlist-experience";
export interface MatchInfo<
TCategory extends GameVariantCategory = GameVariantCategory
> {
StartTime: string;
EndTime: string;
Duration: string;
LifecycleMode: number;
GameVariantCategory: TCategory;
LevelId: string;
MapVariant: {
AssetKind: number;
AssetId: string;
VersionId: string;
};
UgcGameVariant: {
AssetKind: number;
AssetId: string;
VersionId: string;
};
ClearanceId: string;
Playlist: {
AssetKind: number;
AssetId: string;
VersionId: string;
};
PlaylistExperience: PlaylistExperience;
PlaylistMapModePair: {
AssetKind: number;
AssetId: string;
VersionId: string;
};
SeasonId: string;
PlayableDuration: string;
TeamsEnabled: boolean;
TeamScoringEnabled: boolean;
}
4 changes: 4 additions & 0 deletions src/models/halo-infinite/match-outcome.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export enum MatchOutcome {
Win = 2,
Loss = 3,
}
48 changes: 48 additions & 0 deletions src/models/halo-infinite/match-skill.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
interface CsrObject {
Value: number;
MeasurementMatchesRemaining: number;
Tier: string;
TierStart: number;
NextTier: string;
NextTierStart: number;
NextSubTier: number;
InitialMeasurementMatches: number;
}

interface StatPerformance {
Count: number;
Expected: number;
StdDev: number;
}

interface Counterfactual {
Kills: number;
Deaths: number;
}

export interface MatchSkill {
TeamId: number;
TeamMmr: number;
TeamMmrs: {
[key: number]: number;
};
RankRecap: {
PreMatchCsr: CsrObject;
PostMatchCsr: CsrObject;
};
StatPerformances: {
Kills: StatPerformance;
Deaths: StatPerformance;
};
Counterfactuals: {
SelfCounterfactual: Counterfactual;
TierCounterfactuals: {
Bronze: Counterfactual;
Silver: Counterfactual;
Gold: Counterfactual;
Platinum: Counterfactual;
Diamond: Counterfactual;
Onyx: Counterfactual;
};
};
}
35 changes: 35 additions & 0 deletions src/models/halo-infinite/match-stats.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { GameVariantCategory } from "./game-variant-category";
import { MatchInfo } from "./match-info";
import { Stats } from "./stats";

export interface MatchStats<
TCategory extends GameVariantCategory = GameVariantCategory
> {
MatchId: string;
MatchInfo: MatchInfo<TCategory>;
Teams: {
TeamId: number;
Outcome: number;
Rank: number;
Stats: Stats<TCategory>;
}[];
Players: {
PlayerId: string;
LastTeamId: number;
Rank: number;
ParticipationInfo: {
FirstJoinedTime: string;
LastLeaveTime: string | null;
PresentAtBeginning: boolean;
JoinedInProgress: boolean;
LeftInProgress: boolean;
PresentAtCompletion: boolean;
TimePlayed: string;
ConfirmedParticipation: boolean | null;
};
PlayerTeamStats: {
TeamId: number;
Stats: Stats<TCategory>;
}[];
}[];
}
6 changes: 6 additions & 0 deletions src/models/halo-infinite/match-type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export enum MatchType {
All = 0,
Matchmaking = 1,
Custom = 2,
Local = 3,
}
Loading

0 comments on commit e6c483f

Please sign in to comment.