diff --git a/apps/backend/src/app/api/v1/contributors/route.ts b/apps/backend/src/app/api/v1/contributors/route.ts new file mode 100644 index 000000000..faa24ae89 --- /dev/null +++ b/apps/backend/src/app/api/v1/contributors/route.ts @@ -0,0 +1,95 @@ +import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler"; +import * as yup from "yup"; +import sharp from "sharp"; +import { captureError } from "@stackframe/stack-shared/dist/utils/errors"; + +let pngImagePromise: Promise | undefined; + +export const GET = createSmartRouteHandler({ + request: yup.object({}), + response: yup.object({ + statusCode: yup.number().oneOf([200]).required(), + bodyType: yup.string().oneOf(["binary"]).required(), + body: yup.mixed().required(), + headers: yup.object({ + "Content-Type": yup.tuple([ + yup.string().oneOf(["image/png"]).required(), + ]).required(), + }).required(), + }), + handler: async () => { + if (process.env.NODE_ENV === "development" || !pngImagePromise) { + pngImagePromise = (async () => { + const ghPage = await fetch("https://github.com/stack-auth/stack"); + const ghPageText = await ghPage.text(); + const regex = /\ match[1]); + console.log("Creating contributor image", { contributors }); + + const imagesPerRow = 9; + const rows = Math.ceil(contributors.length / imagesPerRow); + const profilePictureSize = 96; + const gapSize = Math.round(profilePictureSize * 0.1); + const cellSize = profilePictureSize + gapSize; + + const contributorsImageUrls = contributors.map((contributor) => `https://github.com/${encodeURIComponent(contributor)}.png`); + const contributorsImagesArrayBuffers = await Promise.all(contributorsImageUrls.map(async (url) => { + const image = await fetch(url); + return await image.arrayBuffer(); + })); + const contributorsImagesBuffersRounded = await Promise.all(contributorsImagesArrayBuffers.map(async (buffer) => { + const filledCircle = ``; + return await sharp(buffer) + .resize(profilePictureSize, profilePictureSize) + .composite([ + { + input: Buffer.from(filledCircle), + blend: "dest-in", + }, + ]) + .png() + .toBuffer(); + })); + + const circleOutline = ``; + let mergedImage = sharp({ + create: { + width: cellSize * (imagesPerRow - 1) + profilePictureSize + 2, + height: cellSize * (rows - 1) + profilePictureSize + 2, + channels: 3, + background: { alpha: 0, r: 255, g: 255, b: 255 }, + }, + }).composite(contributorsImagesBuffersRounded.flatMap((buffer, index) => [ + { + input: buffer, + top: Math.floor(index / imagesPerRow) * cellSize + 1, + left: (index % imagesPerRow) * cellSize + 1, + }, + { + input: Buffer.from(circleOutline), + top: Math.floor(index / imagesPerRow) * cellSize, + left: (index % imagesPerRow) * cellSize + } + ])); + + return await mergedImage.png().toBuffer(); + })(); + pngImagePromise.catch((error) => { + captureError("contributors-image", error); + if (process.env.NODE_ENV === "development") { + pngImagePromise = undefined; + } + }); + } + + return { + statusCode: 200, + bodyType: "binary", + headers: { + "Content-Type": ["image/png"], + }, + body: await pngImagePromise, + } as const; + }, +});