Skip to content
This repository has been archived by the owner on Aug 13, 2024. It is now read-only.

feat: adding utilities for cards local generation & testing #34

Merged
merged 9 commits into from
Apr 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,18 @@ To start a local copy of the app on port `3001`:
npm run start:dev
```

### Local dev scripts

There are a few scripts that can be used to generate and test the social cards locally without having to deploy to the CDN. This is the way to go when developing & testing the interface for the social cards.

#### Generating user profile cards:

```shell
npm run local-dev:usercards
```

> Generates user cards for all users in the test array inside `test/local-dev/UserCards.ts` and outputs them in `dist/local-dev/` for testing.

### πŸ“ Environment variables

Some environment variables are required to run the application. You can find them in the `.env.example` file. While most of them are optional, some are required to run the application.
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"test:cov": "npm run test --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "npm run test --config test/jest-e2e.json",
"test:local:user": "npx ts-node test/local-dev/UserCards",
0-vortex marked this conversation as resolved.
Show resolved Hide resolved
"docs": "npx compodoc -p tsconfig.json --hideGenerator --disableDependencies -d ./dist/documentation ./src",
"docs:serve": "npm run docs -- --serve"
},
Expand Down
88 changes: 50 additions & 38 deletions src/social-card/social-card.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { ForbiddenException, Injectable, Logger, NotFoundException } from "@nest
import { HttpService } from "@nestjs/axios";
import { Resvg } from "@resvg/resvg-js";
import { Repository, Language, User } from "@octokit/graphql-schema";
import { readFile } from "node:fs/promises";
import fs from "node:fs/promises";


import { GithubService } from "../github/github.service";
import { S3FileStorageService } from "../s3-file-storage/s3-file-storage.service";
Expand All @@ -18,6 +19,18 @@ interface RequiresUpdateMeta {
lastModified: Date | null,
}

interface UserCardData {
id: User["databaseId"],
name: User["name"],
langs: (Language & {
size: number,
})[],
langTotal: number,
repos: Repository[],
avatarUrl: string,
}


@Injectable()
export class SocialCardService {
private readonly logger = new Logger(this.constructor.name);
Expand All @@ -28,16 +41,7 @@ export class SocialCardService {
private readonly s3FileStorageService: S3FileStorageService,
) {}

async getUserData (username: string): Promise<{
id: User["databaseId"],
name: User["name"],
langs: (Language & {
size: number,
})[],
langTotal: number,
repos: Repository[],
avatarUrl: string,
}> {
private async getUserData (username: string): Promise<UserCardData> {
const langs: Record<string, Language & {
size: number,
}> = {};
Expand Down Expand Up @@ -74,6 +78,38 @@ export class SocialCardService {
};
}

// public only to be used in local scripts. Not for controller direct use.
async generateCardBuffer (username: string, userData?: UserCardData) {
const { html } = await import("satori-html");
const satori = (await import("satori")).default;

const { avatarUrl, repos, langs, langTotal } = userData ? userData : await this.getUserData(username);

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

const interArrayBuffer = await fs.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 pngData = resvg.render();

return { png: pngData.asPng(), svg };
}

async checkRequiresUpdate (username: string): Promise<RequiresUpdateMeta> {
const hash = `users/${String(username)}.png`;
const fileUrl = `${this.s3FileStorageService.getCdnEndpoint()}${hash}`;
Expand Down Expand Up @@ -107,39 +143,15 @@ export class SocialCardService {
throw new ForbiddenException("Rate limit exceeded");
}

const { html } = await import("satori-html");
const satori = (await import("satori")).default;
const userData = await this.getUserData(username);

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 resvg = new Resvg(svg, { background: "rgba(238, 235, 230, .9)" });

const pngData = resvg.render();

const pngBuffer = pngData.asPng();
const { png } = await this.generateCardBuffer(username, userData);

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

this.logger.debug(`User ${username} did not exist in S3, generated image and uploaded to S3, redirecting`);

Expand Down
2 changes: 1 addition & 1 deletion test/app.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import request from 'supertest';
import { AppModule } from './../src/app.module';

describe('AppController (e2e)', () => {
Expand Down
36 changes: 36 additions & 0 deletions test/local-dev/UserCards.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Test, TestingModule } from "@nestjs/testing";
import { AppModule } from "../../src/app.module";
import { SocialCardService } from "../../src/social-card/social-card.service";
import { existsSync } from "node:fs";
import { mkdir, writeFile } from "fs/promises";


const testUsernames = [
"bdougie", "deadreyo", "defunkt", "0-vortex",
];

const folderPath = "dist";

async function testUserCards () {
const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [AppModule] }).compile();

const app = moduleFixture.createNestApplication();

await app.init();

const instance = app.get(SocialCardService);

const promises = testUsernames.map(async username => {
const { svg } = await instance.generateCardBuffer(username);

if (!existsSync(folderPath)) {
await mkdir(folderPath);
}
await writeFile(`${folderPath}/${username}.svg`, svg);
});

// generating sequential: 10.5 seconds, parallel: 4.5 seconds
await Promise.all(promises);
}

testUserCards();