Skip to content

Commit

Permalink
feat: optimize image loading and return errors
Browse files Browse the repository at this point in the history
closes #17
  • Loading branch information
0-vortex committed Apr 10, 2023
1 parent 1d782e9 commit 7c6f199
Show file tree
Hide file tree
Showing 2 changed files with 45 additions and 43 deletions.
10 changes: 9 additions & 1 deletion src/social-card/social-card.controller.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Controller, Get, Head, Header, HttpStatus, Param, Redirect, Res, StreamableFile } from "@nestjs/common";
import {
ApiForbiddenResponse,
ApiNoContentResponse,
ApiNotFoundResponse,
ApiOkResponse,
Expand Down Expand Up @@ -44,13 +45,20 @@ export class SocialCardController {
@Header("Content-Type", "image/png")
@ApiOkResponse({ type: StreamableFile, description: "Social card image" })
@ApiNotFoundResponse({ description: "User not found" })
@ApiForbiddenResponse({ description: "Rate limit exceeded" })
@Redirect()
async generateUserSocialCard (
@Param("username") username: string,
@Res() res: FastifyReply,
): Promise<void> {
const { fileUrl, hasFile, needsUpdate } = await this.socialCardService.checkRequiresUpdate(username);

if (hasFile && !needsUpdate) {
return res.status(HttpStatus.FOUND).redirect(fileUrl);
}

const url = await this.socialCardService.getUserCard(username);

return res.status(302).redirect(url);
return res.status(HttpStatus.FOUND).redirect(url);
}
}
78 changes: 36 additions & 42 deletions src/social-card/social-card.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Injectable, Logger } from "@nestjs/common";
import { ForbiddenException, Injectable, Logger, NotFoundException } from "@nestjs/common";
import { HttpService } from "@nestjs/axios";
import { Resvg } from "@resvg/resvg-js";
import { Repository, Language, User } from "@octokit/graphql-schema";
Expand Down Expand Up @@ -78,7 +78,7 @@ export class SocialCardService {
const hash = `users/${String(username)}.png`;
const fileUrl = `${this.s3FileStorageService.getCdnEndpoint()}${hash}`;
const hasFile = await this.s3FileStorageService.fileExists(hash);
const today3daysAgo = new Date((new Date).setDate((new Date).getDate() - 0.001));
const today3daysAgo = new Date((new Date).setDate((new Date).getDate() - 3));
const returnVal: RequiresUpdateMeta = {
fileUrl,
hasFile,
Expand All @@ -93,7 +93,7 @@ export class SocialCardService {

if (lastModified && lastModified > today3daysAgo) {
this.logger.debug(`User ${username} exists in S3 with lastModified: ${lastModified.toISOString()} less than 3 days ago, redirecting to ${fileUrl}`);
returnVal.needsUpdate = true;
returnVal.needsUpdate = false;
}
}

Expand All @@ -104,56 +104,50 @@ export class SocialCardService {
const { remaining } = await this.githubService.rateLimit();

if (remaining < 1000) {
throw new Error("Rate limit exceeded");
throw new ForbiddenException("Rate limit exceeded");
}

const { html } = await import("satori-html");
const satori = (await import("satori")).default;

const { id, avatarUrl, repos, langs, langTotal } = await this.getUserData(username);
const hash = `users/${String(id)}.png`;
const fileUrl = `${this.s3FileStorageService.getCdnEndpoint()}${hash}`;
const hasFile = await this.s3FileStorageService.fileExists(hash);
const today3daysAgo = new Date((new Date).setDate((new Date).getDate() - 3));

if (hasFile) {
// route to s3
const lastModified = await this.s3FileStorageService.getFileLastModified(hash);

if (lastModified && lastModified > today3daysAgo) {
this.logger.debug(`User ${username} exists in S3 with lastModified: ${lastModified.toISOString()} less than 3 days ago, redirecting`);
return fileUrl;
}
}
try {
const { id, avatarUrl, repos, langs, langTotal } = await this.getUserData(username);
const hash = `users/${String(username)}.png`;
const fileUrl = `${this.s3FileStorageService.getCdnEndpoint()}${hash}`;

const template = html(userProfileCard(avatarUrl, username, userLangs(langs, langTotal), userProfileRepos(repos)));

const interArrayBuffer = await readFile("node_modules/@fontsource/inter/files/inter-all-400-normal.woff");

const svg = await satori(template, {
width: 1200,
height: 627,
fonts: [
{
name: "Inter",
data: interArrayBuffer,
weight: 400,
style: "normal",
},
],
tailwindConfig,
});

const template = html(userProfileCard(avatarUrl, username, userLangs(langs, langTotal), userProfileRepos(repos)));

const interArrayBuffer = await readFile("node_modules/@fontsource/inter/files/inter-all-400-normal.woff");

const svg = await satori(template, {
width: 1200,
height: 627,
fonts: [
{
name: "Inter",
data: interArrayBuffer,
weight: 400,
style: "normal",
},
],
tailwindConfig,
});
const resvg = new Resvg(svg, { background: "rgba(238, 235, 230, .9)" });

const resvg = new Resvg(svg, { background: "rgba(238, 235, 230, .9)" });
const pngData = resvg.render();

const pngData = resvg.render();
const pngBuffer = pngData.asPng();

const pngBuffer = pngData.asPng();
await this.s3FileStorageService.uploadFile(pngBuffer, hash, "image/png", { "x-amz-meta-user-id": String(id) });

await this.s3FileStorageService.uploadFile(pngBuffer, hash, "image/png", { "x-amz-meta-user-id": String(id) });
this.logger.debug(`User ${username} did not exist in S3, generated image and uploaded to S3, redirecting`);

this.logger.debug(`User ${username} did not exist in S3, generated image and uploaded to S3, redirecting`);
return fileUrl;
} catch (e) {
this.logger.error(`Error generating user card for ${username}`, e);

return fileUrl;
throw (new NotFoundException);
}
}
}

0 comments on commit 7c6f199

Please sign in to comment.