From cb78b3f5a761317d39e570bdb23c23e578f53297 Mon Sep 17 00:00:00 2001 From: Samuel Elliott Date: Wed, 14 Dec 2022 19:50:02 +0000 Subject: [PATCH] Automatically upgrade SplatNet 3 persisted queries --- src/api/splatnet3.ts | 62 +++++++++++++++++++++++++++++++ src/cli/splatnet3/dump-fests.ts | 5 ++- src/cli/splatnet3/dump-results.ts | 9 +++-- src/exports/coral.ts | 3 ++ src/exports/index.ts | 2 +- src/exports/nooklink.ts | 5 +++ src/exports/splatnet2.ts | 1 + src/exports/splatnet3.ts | 8 ++++ 8 files changed, 89 insertions(+), 6 deletions(-) diff --git a/src/api/splatnet3.ts b/src/api/splatnet3.ts index 85face6..002a322 100644 --- a/src/api/splatnet3.ts +++ b/src/api/splatnet3.ts @@ -9,6 +9,9 @@ import { BulletToken } from './splatnet3-types.js'; import { defineResponse, ErrorResponse, HasResponse, ResponseSymbol } from './util.js'; const debug = createDebug('nxapi:api:splatnet3'); +const debugGraphQl = createDebug('nxapi:api:splatnet3:graphql'); +debugGraphQl.enabled = true; +const debugUpgradeQuery = createDebug('nxapi:api:splatnet3:upgrade-query'); export const SPLATNET3_WEBSERVICE_ID = 4834290508791808; export const SPLATNET3_WEBSERVICE_URL = 'https://api.lp1.av5ja.srv.nintendo.net'; @@ -48,6 +51,17 @@ export interface PersistedQueryResultData { [VariablesSymbol]: {}; } +enum MapQueriesMode { + /** NXAPI_SPLATNET3_UPGRADE_QUERIES=0 - never upgrade persisted query IDs (not recommended) */ + NEVER, + /** NXAPI_SPLATNET3_UPGRADE_QUERIES=1 - upgrade persisted query IDs that do not contain potentially breaking changes (not recommended) */ + ONLY_SAFE_NO_REJECT, + /** NXAPI_SPLATNET3_UPGRADE_QUERIES=2 - upgrade persisted query IDs, but reject requests that contain potentially breaking changes */ + ONLY_SAFE, + /** NXAPI_SPLATNET3_UPGRADE_QUERIES=3 - upgrade persisted query IDs, including requests that contain potentially breaking changes (default) */ + ALL, +} + export default class SplatNet3Api { onTokenShouldRenew: ((remaining: number, res: Response) => Promise) | null = null; onTokenExpired: ((res: Response) => Promise) | null = null; @@ -59,6 +73,8 @@ export default class SplatNet3Api { protected constructor( public bullet_token: string, public version: string, + public map_queries: Partial>, + readonly map_queries_mode: MapQueriesMode, public language: string, public useragent: string, ) {} @@ -135,6 +151,8 @@ export default class SplatNet3Api { _Variables extends (V extends object ? V : _Id extends KnownRequestId ? VariablesTypes[_Id] : unknown) = (V extends object ? V : _Id extends KnownRequestId ? VariablesTypes[_Id] : unknown), >(id: _Id, variables: _Variables): Promise> { + id = this.getUpgradedPersistedQueryId(id) as _Id; + const req: GraphQLRequest<_Variables> = { variables, extensions: { @@ -163,6 +181,37 @@ export default class SplatNet3Api { return data as PersistedQueryResult<_Result>; } + getPersistedQueryId(id: RequestId) { + return this.getUpgradedPersistedQueryId(id, false); + } + + private getUpgradedPersistedQueryId(id: string, reject = true) { + if (this.map_queries_mode === MapQueriesMode.NEVER) return id; + + let new_id = id; + let unsafe = false; + + while (this.map_queries[new_id]) { + const [map_id, map_unsafe] = this.map_queries[new_id]!; + + if (map_unsafe && this.map_queries_mode === MapQueriesMode.ONLY_SAFE && reject) { + throw new Error('[splatnet3] Updated persisted query ' + map_id + ' for ' + id + + ' contains potentially breaking changes'); + } + if (map_unsafe && this.map_queries_mode !== MapQueriesMode.ALL) break; + + new_id = map_id; + unsafe = unsafe || map_unsafe; + } + + if (reject) { + debugUpgradeQuery('Using persisted query %s for %s', new_id, id); + if (unsafe) console.warn('[warn] Upgrading SplatNet 3 persisted query %s with potentially breaking changes', id); + } + + return new_id; + } + /** * */ async getCurrentFest() { return this.persistedQuery(RequestId.CurrentFestQuery, {}); @@ -862,6 +911,8 @@ export default class SplatNet3Api { return new this( data.bullet_token.bulletToken, data.version, + {}, + getMapPersistedQueriesModeFromEnvironment(), data.bullet_token.lang, data.useragent, ); @@ -871,6 +922,8 @@ export default class SplatNet3Api { return new this( data.bullet_token, data.version, + {}, + getMapPersistedQueriesModeFromEnvironment(), data.language, SPLATNET3_WEBSERVICE_USERAGENT, ); @@ -972,6 +1025,15 @@ export default class SplatNet3Api { } } +function getMapPersistedQueriesModeFromEnvironment(): MapQueriesMode { + if (process.env.NXAPI_SPLATNET3_UPGRADE_QUERIES === '0') return MapQueriesMode.NEVER; + if (process.env.NXAPI_SPLATNET3_UPGRADE_QUERIES === '1') return MapQueriesMode.ONLY_SAFE_NO_REJECT; + if (process.env.NXAPI_SPLATNET3_UPGRADE_QUERIES === '2') return MapQueriesMode.ONLY_SAFE; + if (process.env.NXAPI_SPLATNET3_UPGRADE_QUERIES === '3') return MapQueriesMode.ALL; + + return MapQueriesMode.ALL; +} + export interface SplatNet3AuthData { webserviceToken: WebServiceToken; url: string; diff --git a/src/cli/splatnet3/dump-fests.ts b/src/cli/splatnet3/dump-fests.ts index a90f4c7..e2f10a3 100644 --- a/src/cli/splatnet3/dump-fests.ts +++ b/src/cli/splatnet3/dump-fests.ts @@ -104,7 +104,7 @@ export async function dumpFestRecords(splatnet: SplatNet3Api, directory: string, if (fest_record.state !== FestState.CLOSED) { const filename = 'splatnet3-festvotes-' + id + '-' + Date.now() + '-' + - RequestId.DetailVotingStatusQuery + '.json'; + splatnet.getPersistedQueryId(RequestId.DetailVotingStatusQuery) + '.json'; const file = path.join(directory, filename); // Fetch this now to match the behavour of Nintendo's app @@ -128,7 +128,8 @@ export async function dumpFestRecords(splatnet: SplatNet3Api, directory: string, } if (include_rankings) { - const filename = 'splatnet3-festranking-' + id + '-' + RequestId.DetailRankingQuery + '.json'; + const filename = 'splatnet3-festranking-' + id + '-' + + splatnet.getPersistedQueryId(RequestId.DetailRankingQuery) + '.json'; const file = path.join(directory, filename); try { diff --git a/src/cli/splatnet3/dump-results.ts b/src/cli/splatnet3/dump-results.ts index 6aaeb6b..c018304 100644 --- a/src/cli/splatnet3/dump-results.ts +++ b/src/cli/splatnet3/dump-results.ts @@ -157,7 +157,8 @@ export async function dumpResults( latest_unique_ids.push(id); - const filename = 'splatnet3-result-' + id + '-' + RequestId.VsHistoryDetailQuery + '.json'; + const filename = 'splatnet3-result-' + id + '-' + + splatnet.getPersistedQueryId(RequestId.VsHistoryDetailQuery) + '.json'; const file = path.join(directory, filename); try { @@ -195,7 +196,8 @@ export async function dumpResults( if (latest_unique_ids.includes(id)) continue; - const filename = 'splatnet3-result-' + id + '-' + RequestId.VsHistoryDetailQuery + '.json'; + const filename = 'splatnet3-result-' + id + '-' + + splatnet.getPersistedQueryId(RequestId.VsHistoryDetailQuery) + '.json'; const file = path.join(directory, filename); try { @@ -267,7 +269,8 @@ export async function dumpCoopResults( const match = id_str.match(/^CoopHistoryDetail-(u-[0-9a-z]{20}):((\d{8,}T\d{6})_([0-9a-f-]{36}))$/); const id = match ? match[1] + '-' + match[2] : id_str; - const filename = 'splatnet3-coop-result-' + id + '-' + RequestId.CoopHistoryDetailQuery + '.json'; + const filename = 'splatnet3-coop-result-' + id + '-' + + splatnet.getPersistedQueryId(RequestId.CoopHistoryDetailQuery) + '.json'; const file = path.join(directory, filename); try { diff --git a/src/exports/coral.ts b/src/exports/coral.ts index 2c40244..923534b 100644 --- a/src/exports/coral.ts +++ b/src/exports/coral.ts @@ -2,6 +2,9 @@ export { default, CoralAuthData, PartialCoralAuthData, + + ResponseDataSymbol, + CorrelationIdSymbol, } from '../api/coral.js'; export * from '../api/coral-types.js'; diff --git a/src/exports/index.ts b/src/exports/index.ts index 5d4eea6..3c4c994 100644 --- a/src/exports/index.ts +++ b/src/exports/index.ts @@ -1,5 +1,5 @@ export { getTitleIdFromEcUrl } from '../util/misc.js'; -export { ErrorResponse } from '../api/util.js'; +export { ErrorResponse, ResponseSymbol } from '../api/util.js'; export { addUserAgent } from '../util/useragent.js'; export { version } from '../util/product.js'; diff --git a/src/exports/nooklink.ts b/src/exports/nooklink.ts index 74cc391..b434771 100644 --- a/src/exports/nooklink.ts +++ b/src/exports/nooklink.ts @@ -1,6 +1,11 @@ export { default as NooklinkApi, + NooklinkAuthData, + NooklinkUserApi, + NooklinkUserAuthData, + NooklinkUserCliTokenData, + MessageType, } from '../api/nooklink.js'; diff --git a/src/exports/splatnet2.ts b/src/exports/splatnet2.ts index 39ade33..6196161 100644 --- a/src/exports/splatnet2.ts +++ b/src/exports/splatnet2.ts @@ -1,6 +1,7 @@ export { default, SplatNet2AuthData, + SplatNet2CliTokenData, LeagueType, LeagueRegion, diff --git a/src/exports/splatnet3.ts b/src/exports/splatnet3.ts index 18c07f5..148fbb0 100644 --- a/src/exports/splatnet3.ts +++ b/src/exports/splatnet3.ts @@ -1,6 +1,14 @@ export { default, SplatNet3AuthData, + SplatNet3CliTokenData, + + RequestIdSymbol, + VariablesSymbol, + + XRankingRegion, + XRankingLeaderboardType, + XRankingLeaderboardRule, } from '../api/splatnet3.js'; // export * from '../api/splatnet3-types.js';