diff --git a/src/server/load-pet-data.js b/src/server/load-pet-data.js index bb520ab5..f5c42a00 100644 --- a/src/server/load-pet-data.js +++ b/src/server/load-pet-data.js @@ -6,20 +6,38 @@ async function neopetsAmfphpCall(methodName, args) { encodeURIComponent(methodName) + "/" + args.map(encodeURIComponent).join("/"); - return await fetch(url).then((res) => res.json()); + + const res = await fetch(url); + if (!res.ok) { + throw new Error( + `[AMFPHP] HTTP request failed, got status ${res.status} ${res.statusText}` + ); + } + + return res.json(); } export async function loadPetMetaData(petName) { - const response = await neopetsAmfphpCall("PetService.getPet", [petName]); - return response; + return neopetsAmfphpCall("PetService.getPet", [petName]); } export async function loadCustomPetData(petName) { + // HACK: The json.php amfphp endpoint is known not to support string + // arguments with leading digits. (It aggressively parses them as ints lmao.) + // So, we work around it by converting the pet name to its image hash, then + // prepending "@", which is a special code that can *also* be used in the + // CustomPetService in place of name, to get a pet's appearance from its image + // hash. + if (petName.match(/^[0-9]/)) { + const imageHash = await loadImageHashFromPetName(petName); + console.debug( + `[loadCustomPetData] Converted pet name ${petName} to @${imageHash}` + ); + petName = "@" + imageHash; + } + try { - const response = await neopetsAmfphpCall("CustomPetService.getViewerData", [ - petName, - ]); - return response; + return neopetsAmfphpCall("CustomPetService.getViewerData", [petName]); } catch (error) { // If Neopets.com fails to find valid customization data, we return null. if ( @@ -33,6 +51,30 @@ export async function loadCustomPetData(petName) { } } +const PETS_CP_URL_PATTERN = /https?:\/\/pets\.neopets\.com\/cp\/([a-z0-9]+)\/[0-9]+\/[0-9]+\.png/; +async function loadImageHashFromPetName(petName) { + const res = await fetch(`https://pets.neopets.com/cpn/${petName}/1/1.png`, { + redirect: "manual", + }); + if (res.status !== 302) { + throw new Error( + `[loadImageHashFromPetName] expected /cpn/ URL to redirect with status ` + + `302, but instead got status ${res.status} ${res.statusText}` + ); + } + + const newUrl = res.headers.get("location"); + const newUrlMatch = newUrl.match(PETS_CP_URL_PATTERN); + if (newUrlMatch == null) { + throw new Error( + `[loadImageHashFromPetName] expected /cpn/ URL to redirect to a /cp/ ` + + `URL matching ${PETS_CP_URL_PATTERN}, but got ${newUrl}` + ); + } + + return newUrlMatch[1]; +} + export async function loadNCMallPreviewImageHash(basicImageHash, itemIds) { const query = new URLSearchParams(); query.append("selPetsci", basicImageHash); diff --git a/src/server/types/Pet.js b/src/server/types/Pet.js index 8cd1f18d..17c3e555 100644 --- a/src/server/types/Pet.js +++ b/src/server/types/Pet.js @@ -57,16 +57,22 @@ const resolvers = { } // Next, look for a pet state matching the same pose. (This can happen if - // modeling data for this pet hasn't saved yet.) - const pose = getPoseFromPetData(petMetaData, customPetData); - petState = petStates.find((ps) => getPoseFromPetState(ps) === pose); - if (petState) { - console.warn( - `Warning: For pet "${name}", fell back to pet state ${petState.id} ` + - `because it matches pose ${pose}. Actual pet state for these ` + - `assets not found: ${swfAssetIdsString}` - ); - return { id: petState.id }; + // modeling data for this pet hasn't saved yet.) (We might skip this step + // if we couldn't load either the custom pet data or metadata, which is + // expected if e.g. the pet name starts with a leading digit, so we can + // use a workaround for the custom pet data but the JSON endpoint for + // metadata fails.) + if (petMetaData != null && customPetData != null) { + const pose = getPoseFromPetData(petMetaData, customPetData); + petState = petStates.find((ps) => getPoseFromPetState(ps) === pose); + if (petState) { + console.warn( + `Warning: For pet "${name}", fell back to pet state ${petState.id} ` + + `because it matches pose ${pose}. Actual pet state for these ` + + `assets not found: ${swfAssetIdsString}` + ); + return { id: petState.id }; + } } // Finally, look for an UNKNOWN pet state. (This can happen if modeling @@ -119,10 +125,17 @@ const resolvers = { ) => { const [customPetData, petMetaData] = await Promise.all([ loadCustomPetData(petName), - loadPetMetaData(petName), + loadPetMetaData(petName).catch((error) => { + console.warn(`Couldn't load metadata for pet ${petName}: `, error); + return null; + }), ]); - if (customPetData != null && process.env["USE_NEW_MODELING"] === "1") { + if ( + customPetData != null && + petMetaData != null && + process.env["USE_NEW_MODELING"] === "1" + ) { await saveModelingData(customPetData, petMetaData, { db, petTypeBySpeciesAndColorLoader,