Add workaround for pet lookups with leading digits

This was Dice's idea ty!! Now, instead of crashing when looking up any pet whose name starts with a number, we do a clever lil workaround instead! We don't get *all* the data back (we're missing metadata), but that's fine for the main use case of typing your pet name in at the homepage.
This commit is contained in:
Emi Matchu 2023-08-29 15:05:19 -07:00
parent 80428e9834
commit 756ee8dcb2
2 changed files with 74 additions and 19 deletions

View file

@ -6,20 +6,38 @@ async function neopetsAmfphpCall(methodName, args) {
encodeURIComponent(methodName) + encodeURIComponent(methodName) +
"/" + "/" +
args.map(encodeURIComponent).join("/"); 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) { export async function loadPetMetaData(petName) {
const response = await neopetsAmfphpCall("PetService.getPet", [petName]); return neopetsAmfphpCall("PetService.getPet", [petName]);
return response;
} }
export async function loadCustomPetData(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 { try {
const response = await neopetsAmfphpCall("CustomPetService.getViewerData", [ return neopetsAmfphpCall("CustomPetService.getViewerData", [petName]);
petName,
]);
return response;
} catch (error) { } catch (error) {
// If Neopets.com fails to find valid customization data, we return null. // If Neopets.com fails to find valid customization data, we return null.
if ( 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) { export async function loadNCMallPreviewImageHash(basicImageHash, itemIds) {
const query = new URLSearchParams(); const query = new URLSearchParams();
query.append("selPetsci", basicImageHash); query.append("selPetsci", basicImageHash);

View file

@ -57,7 +57,12 @@ const resolvers = {
} }
// Next, look for a pet state matching the same pose. (This can happen if // Next, look for a pet state matching the same pose. (This can happen if
// modeling data for this pet hasn't saved yet.) // 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); const pose = getPoseFromPetData(petMetaData, customPetData);
petState = petStates.find((ps) => getPoseFromPetState(ps) === pose); petState = petStates.find((ps) => getPoseFromPetState(ps) === pose);
if (petState) { if (petState) {
@ -68,6 +73,7 @@ const resolvers = {
); );
return { id: petState.id }; return { id: petState.id };
} }
}
// Finally, look for an UNKNOWN pet state. (This can happen if modeling // Finally, look for an UNKNOWN pet state. (This can happen if modeling
// data for this pet hasn't saved yet, and we haven't manually labeled a // data for this pet hasn't saved yet, and we haven't manually labeled a
@ -119,10 +125,17 @@ const resolvers = {
) => { ) => {
const [customPetData, petMetaData] = await Promise.all([ const [customPetData, petMetaData] = await Promise.all([
loadCustomPetData(petName), 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, { await saveModelingData(customPetData, petMetaData, {
db, db,
petTypeBySpeciesAndColorLoader, petTypeBySpeciesAndColorLoader,