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

Commit

Permalink
feat: adding utilities for cards local generation & testing (#34)
Browse files Browse the repository at this point in the history
* feat: getuserdata private

* fix: fix incorrect import

* feat: refractor & add local scripts

* feat: add local dev generation script

* last touches

* refractor: suggestions

* docs: add documentation for local dev script

* refractor: change output folder

* update local dev user command name
  • Loading branch information
nightknighto committed Apr 26, 2023
1 parent bb520af commit c5a5fec
Show file tree
Hide file tree
Showing 5 changed files with 100 additions and 39 deletions.
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",
"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();

0 comments on commit c5a5fec

Please sign in to comment.