diff --git a/old/fetchers/ProfileCardFetcher.ts b/old/fetchers/ProfileCardFetcher.ts deleted file mode 100644 index 0f01ec0..0000000 --- a/old/fetchers/ProfileCardFetcher.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { TopicContributionEndpoint } from "../utils/types/dtos.types"; - -export default async function ProfileCardDataFetcher (name: string): Promise<{ langs: string[], repos: string[], img: string }> { - const req = await fetch(`https://beta.gs-api.opensauced.pizza/v1/*/contributions?page=1&limit=1&range=30&contributor=${name}`); - const reqData = await req.json() as TopicContributionEndpoint; - - console.log(reqData); - const contributor = reqData.data[0]; - - // could be undefined if array is empty - if (!contributor) { - throw new Error(`User '${name}' Not Found`); - } - - const langs = contributor.langs ? contributor.langs.split(",") : []; - const repos = contributor.recent_repo_list ? contributor.recent_repo_list.split(",") : []; - - const imgReq = await fetch(`https://www.github.com/${name}.png?size=300`); - const imgReqBody = await imgReq.blob(); - - const img = URL.createObjectURL(imgReqBody); - - return { - langs, - repos, - img, - }; -} diff --git a/old/handlers/ProfileCardHandler.ts b/old/handlers/ProfileCardHandler.ts deleted file mode 100644 index fed467c..0000000 --- a/old/handlers/ProfileCardHandler.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { readFile } from "node:fs/promises"; - -import { default as satori } from "satori"; -import { Resvg } from "@resvg/resvg-js"; -import { Request, Response } from "express"; -import ProfileCardDataFetcher from "../fetchers/ProfileCardFetcher"; -import userLangs from "../../src/social-card/templates/user-langs"; -import userProfileRepos from "../../src/social-card/templates/user-profile-repos"; - -export default async function profileCardHandler (req: Request, res: Response) { - const { name } = req.params; - - - // based on the design of: User Profile Card - Linkedin Content Images - 1200x627 - let data: Awaited>; - - try { - data = await ProfileCardDataFetcher(name); - } catch (error) { - const { message } = error as Error; - - console.error(`Missing data: ${message}`); - res.writeHead(404, { "Content-Type": "text/plain" }); - res.end(message); - return; - } - - const { langs, repos, img } = data; - - res.writeHead(201, { "Content-Type": "image/png" }); - - const { html } = await import("satori-html"); - - const template = html(userProfileCard(name, img, userLangs(langs), userProfileRepos(repos))); - - const robotoArrayBuffer = await readFile("public/Roboto-Regular.ttf"); - const svg = await satori(template, { - width: 1200, - height: 627, - fonts: [ - { - name: "Roboto", - data: robotoArrayBuffer, - weight: 400, - style: "normal", - }, - ], - }); - - const resvg = new Resvg(svg, { background: "rgba(238, 235, 230, .9)" }); - - const pngData = resvg.render(); - const pngBuffer = pngData.asPng(); - - res.end(pngBuffer); -} diff --git a/old/utils/types/dtos.types.ts b/old/utils/types/dtos.types.ts deleted file mode 100644 index f54b6b2..0000000 --- a/old/utils/types/dtos.types.ts +++ /dev/null @@ -1,25 +0,0 @@ - - -export interface TopicContributionEndpoint { - readonly data: DbContribution[]; -} - -interface DbContribution { - readonly id: number; - readonly commits: string; - readonly commit_days: string; - readonly files_modified: string; - readonly first_commit_time: string; - readonly last_commit_time: string; - readonly email: string; - readonly name: string; - readonly host_login: string; - readonly langs: string; - readonly recent_repo_list: string; - readonly recent_pr_total: number; - readonly recent_contribution_count: number; - readonly recent_opened_prs: number; - readonly recent_pr_reviews: number; - readonly recent_pr_velocity: number; - readonly recent_merged_prs: number; -} diff --git a/src/app.controller.ts b/src/app.controller.ts deleted file mode 100644 index b11b78f..0000000 --- a/src/app.controller.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Controller, Get, Param } from '@nestjs/common'; -import { AppService } from './app.service'; -import { ApiNotFoundResponse, ApiOkResponse, ApiOperation, ApiTags } from "@nestjs/swagger"; - -@Controller("users") -@ApiTags("Users social cards") -export class AppController { - constructor(private readonly appService: AppService) {} - - @Get("/:username") - @ApiOperation({ - operationId: "generateUserSocialCard", - summary: "Gets latest cache aware social card link for :username or generates a new one", - }) - @ApiOkResponse({ type: String }) - @ApiNotFoundResponse({ description: "User not found" }) - async generateUserSocialCard ( - @Param("username") username: string, - ): Promise { - return this.appService.getUserCard(username); - } -} diff --git a/src/app.module.ts b/src/app.module.ts index 365e922..849c919 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -5,8 +5,7 @@ import { TerminusModule } from "@nestjs/terminus"; import { LoggerModule } from "nestjs-pino"; import { clc } from "@nestjs/common/utils/cli-colors.util"; -import { AppController } from './app.controller'; -import { AppService } from './app.service'; +import { SocialCardModule } from './social-card/social-card.module'; import ApiConfig from "./config/api.config"; @Module({ @@ -42,8 +41,9 @@ import ApiConfig from "./config/api.config"; }), TerminusModule, HttpModule, + SocialCardModule, ], - controllers: [AppController], - providers: [AppService], + controllers: [], + providers: [], }) export class AppModule {} diff --git a/src/app.service.ts b/src/app.service.ts deleted file mode 100644 index 1f090e5..0000000 --- a/src/app.service.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -@Injectable() -export class AppService { - getUserCard(username: string): string { - return `Hello ${username}!`; - } -} diff --git a/src/social-card/social-card.controller.ts b/src/social-card/social-card.controller.ts new file mode 100644 index 0000000..6aa0f24 --- /dev/null +++ b/src/social-card/social-card.controller.ts @@ -0,0 +1,32 @@ +import {Controller, Get, Header, Param, Req, Res, StreamableFile} from '@nestjs/common'; +import { ApiNotFoundResponse, ApiOkResponse, ApiOperation } from "@nestjs/swagger"; +import { createReadStream } from "node:fs"; +import { Readable } from 'stream'; + +import { SocialCardService } from "./social-card.service"; + +@Controller('users') +export class SocialCardController { + constructor( + private readonly socialCardService: SocialCardService + ) {} + + @Get("/:username") + @ApiOperation({ + operationId: "generateUserSocialCard", + summary: "Gets latest cache aware social card link for :username or generates a new one", + }) + @ApiOkResponse({ type: String }) + @ApiNotFoundResponse({ description: "User not found" }) + async generateUserSocialCard ( + @Param("username") username: string, + @Req() request: Request, + @Res() response: Response + ): Promise { + const image = await this.socialCardService.getUserCard(username); + + console.log(image); + + return new StreamableFile(image); + } +} diff --git a/src/social-card/social-card.module.ts b/src/social-card/social-card.module.ts new file mode 100644 index 0000000..fa48290 --- /dev/null +++ b/src/social-card/social-card.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { HttpModule } from "@nestjs/axios"; +import { SocialCardService } from './social-card.service'; +import { SocialCardController } from './social-card.controller'; + +@Module({ + imports: [HttpModule], + providers: [SocialCardService], + controllers: [SocialCardController] +}) +export class SocialCardModule {} diff --git a/src/social-card/social-card.service.ts b/src/social-card/social-card.service.ts new file mode 100644 index 0000000..044ca59 --- /dev/null +++ b/src/social-card/social-card.service.ts @@ -0,0 +1,96 @@ +import { Injectable } from '@nestjs/common'; +import { HttpService } from "@nestjs/axios"; +import { catchError, firstValueFrom, map, throwError } from "rxjs"; +import { readFile } from "node:fs/promises"; +import { Resvg } from "@resvg/resvg-js"; + +import userLangs from "./templates/user-langs"; +import userProfileRepos from "./templates/user-profile-repos"; +import userProfileCard from "./templates/user-profile-card"; + +@Injectable() +export class SocialCardService { + constructor( + private readonly httpService: HttpService, + ) {} + + async getUserData(username: string): Promise<{ + langs: string[], + repos: string[], + img: string, + }> { + const request= this.httpService + .get(`https://beta.gs-api.opensauced.pizza/v1/*/contributions?page=1&limit=1&range=30&contributor=${username}`) + .pipe( + map((res) => res.data), + ) + .pipe( + catchError((err) => { + console.log(err); + return throwError(err); + } + )); + + const { data } = await firstValueFrom(request); + + const contributor = data[0]; + + if (!contributor) { + throw new Error(`User '${username}' Not Found`); + } + + const langs = contributor.langs ? contributor.langs.split(",") : []; + const repos = contributor.recent_repo_list ? contributor.recent_repo_list.split(",") : []; + + const imgReq = this.httpService + .get(`https://www.github.com/${username}.png?size=300`, { + responseType: 'text', + responseEncoding: 'base64' + }) + .pipe( + catchError((err) => { + console.log(err); + return throwError(err); + } + )); + + const { data: img } = await firstValueFrom(imgReq); + + return { + langs, + repos, + img, + }; + } + + async getUserCard(username: string): Promise { + const { html } = await import("satori-html"); + const satori = (await import("satori")).default; + + const { img, repos, langs } = await this.getUserData(username); + + const template = html(userProfileCard(img, username, userLangs(langs), userProfileRepos(repos))); + + const robotoArrayBuffer = await readFile("public/Roboto-Regular.ttf"); + const svg = await satori(template, { + width: 1200, + height: 627, + fonts: [ + { + name: "Roboto", + data: robotoArrayBuffer, + weight: 400, + style: "normal", + }, + ], + }); + + const resvg = new Resvg(svg, { background: "rgba(238, 235, 230, .9)" }); + + const pngData = resvg.render(); + + const pngBuffer = pngData.asPng(); + + return pngBuffer; + } +} diff --git a/src/social-card/templates/user-langs.ts b/src/social-card/templates/user-langs.ts index 5d30e44..abb5850 100644 --- a/src/social-card/templates/user-langs.ts +++ b/src/social-card/templates/user-langs.ts @@ -7,7 +7,7 @@ const userLangs = (langs: string[], joinLiteral = "") => langs.map(lang => { return `
`; diff --git a/src/social-card/templates/user-profile-card.ts b/src/social-card/templates/user-profile-card.ts index 85f8601..d76cf9e 100644 --- a/src/social-card/templates/user-profile-card.ts +++ b/src/social-card/templates/user-profile-card.ts @@ -43,7 +43,7 @@ const userProfileCard = (img: string, name: string, langs: string, repos: string width: 1136px; height: 134px; "> -