2021-01-03 20:21:34 -08:00
|
|
|
const beeline = require("honeycomb-beeline")({
|
|
|
|
writeKey: process.env["HONEYCOMB_WRITE_KEY"],
|
|
|
|
dataset:
|
|
|
|
process.env["NODE_ENV"] === "production"
|
|
|
|
? "Dress to Impress (2020)"
|
|
|
|
: "Dress to Impress (2020, dev)",
|
|
|
|
serviceName: "impress-2020-gql-server",
|
|
|
|
});
|
|
|
|
|
|
|
|
const { renderOutfitImage } = require("../src/server/outfit-images");
|
|
|
|
|
2021-01-03 21:58:19 -08:00
|
|
|
const VALID_LAYER_URLS = [
|
2021-01-04 00:10:35 -08:00
|
|
|
/^https:\/\/impress-asset-images\.s3\.amazonaws\.com\/(biology|object)\/[0-9]{3}\/[0-9]{3}\/[0-9]{3}\/[0-9]+\/(150|300)x(150|300)\.png(\?[a-zA-Z0-9_-]+)?$/,
|
2021-03-12 04:30:00 -08:00
|
|
|
/^http:\/\/images\.neopets\.com\/cp\/(bio|object|items)\/data\/[0-9]{3}\/[0-9]{3}\/[0-9]{3}\/[a-zA-Z0-9_-]+\/[a-zA-Z0-9_-]+\.(svg|png)(\?.*)?$/,
|
2021-01-03 21:58:19 -08:00
|
|
|
];
|
|
|
|
|
2021-01-03 20:21:34 -08:00
|
|
|
async function handle(req, res) {
|
2021-01-03 21:58:19 -08:00
|
|
|
if (!req.query.layerUrls) {
|
2021-01-03 22:58:09 -08:00
|
|
|
res.setHeader("Content-Type", "text/plain");
|
|
|
|
return res.status(400).send(`Missing required parameter: layerUrls`);
|
2021-01-03 21:58:19 -08:00
|
|
|
}
|
|
|
|
const layerUrls = req.query.layerUrls.split(",");
|
|
|
|
|
2021-01-04 00:10:35 -08:00
|
|
|
const size = parseInt(req.query.size);
|
|
|
|
if (size !== 150 && size !== 300) {
|
|
|
|
res.setHeader("Content-Type", "text/plain");
|
|
|
|
return res.status(400).send(`Size must be 150 or 300`);
|
|
|
|
}
|
|
|
|
|
2021-01-03 21:58:19 -08:00
|
|
|
for (const layerUrl of layerUrls) {
|
|
|
|
if (!VALID_LAYER_URLS.some((pattern) => layerUrl.match(pattern))) {
|
2021-01-03 22:58:09 -08:00
|
|
|
return res.status(400).send(`Unexpected layer URL format: ${layerUrl}`);
|
2021-01-03 21:58:19 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let imageResult;
|
|
|
|
try {
|
2021-01-04 00:10:35 -08:00
|
|
|
imageResult = await renderOutfitImage(layerUrls, size);
|
2021-01-03 21:58:19 -08:00
|
|
|
} catch (e) {
|
|
|
|
console.error(e);
|
2021-01-03 22:58:09 -08:00
|
|
|
res.setHeader("Content-Type", "text/plain");
|
|
|
|
return res.status(400).send(`Error rendering image: ${e.message}`);
|
2021-01-03 21:58:19 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
const { image, status } = imageResult;
|
|
|
|
|
|
|
|
if (status === "success") {
|
|
|
|
// On success, we use very aggressive caching, on the assumption that
|
|
|
|
// layers are ~immutable too, and that our rendering algorithm will almost
|
|
|
|
// never change in a way that requires pushing changes. If it does, we
|
|
|
|
// should add a cache-buster to the URL!
|
2021-01-03 22:58:09 -08:00
|
|
|
res.setHeader("Cache-Control", "public, max-age=604800, immutable");
|
|
|
|
res.status(200);
|
2021-01-03 21:58:19 -08:00
|
|
|
} else {
|
|
|
|
// On partial failure, we still send the image, but with a 500 status. We
|
|
|
|
// send a long-lived cache header, but in such a way that the user can
|
|
|
|
// refresh the page to try again. (`private` means the CDN won't cache it,
|
|
|
|
// and we don't send `immutable`, which would save it even across reloads.)
|
|
|
|
// The 500 won't really affect the client, which will still show the image
|
|
|
|
// without feedback to the user - but it's a helpful debugging hint.
|
2021-01-03 22:58:09 -08:00
|
|
|
res.setHeader("Cache-Control", "private, max-age=604800");
|
|
|
|
res.status(500);
|
2021-01-03 21:58:19 -08:00
|
|
|
}
|
|
|
|
|
2021-01-03 22:58:09 -08:00
|
|
|
res.setHeader("Content-Type", "image/png");
|
|
|
|
return res.send(image);
|
2021-01-03 20:21:34 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
export default async (req, res) => {
|
2021-04-23 12:31:41 -07:00
|
|
|
beeline.withTrace(
|
|
|
|
{ name: "api/outfitImage", operation_name: "api/outfitImage" },
|
|
|
|
() => handle(req, res)
|
|
|
|
);
|
2021-01-03 20:21:34 -08:00
|
|
|
};
|