(AppModule);
7 | app.useBodyParser('json', { limit: '100mb' });
8 | app.enableCors()
9 | await app.listen(process.env.APP_PORT);
10 | console.log(`Application is running on: ${await app.getUrl()}`)
11 | }
12 | bootstrap();
13 |
--------------------------------------------------------------------------------
/src/readme/Renderer.ts:
--------------------------------------------------------------------------------
1 | import { RequestService } from "src/request/request.service"
2 | import { ChessService } from "src/games/chess/chess.service"
3 | import { GameboyService } from "src/games/gameboy/gameboy.service"
4 | import { GbaService } from "src/games/gba/gba.service"
5 | import { MinesweeperService } from "src/games/minesweeper/minesweeper.service"
6 | import { WordleService } from "src/games/wordle/wordle.service"
7 | import * as fs from 'fs'
8 | import Config from "src/declarations/config.interface"
9 |
10 | class Module {
11 | value: string
12 |
13 | constructor(
14 | private parent: Renderer,
15 | public flat: any
16 | ) {}
17 |
18 | render() {
19 | const {config} = this.parent
20 | if(this.value) return this.value
21 |
22 | const {id, data, options} = this.flat
23 | console.log(id)
24 | if(id === "static/element") this.value = `<${data.element}${options?.align ? ` align="${options?.align}"` : `` }>${data.content}${data.element}>\n`
25 | else if(id === "static/raw") this.value = data.content + ((data.content as string).endsWith('\n') ? "" : "\n")
26 | else if(id === "static/greeting") this.value = `I'm ${config.datas.perso.firstname} ${config.datas.perso.lastname} !
\n`
27 | else if(id === "3rdParty/profileViews") this.value = `\n
\n
\n`
28 | else if(id === "static/lines") {
29 | const path = data.field.split('.')
30 | let lines: any = config.datas;
31 |
32 | for (let i = 0; i < path.length; i++) {
33 | lines = lines[path[i]];
34 | }
35 |
36 | if(!data.range.includes('-')) lines = [lines[+data.range - 1]]
37 | else {
38 | const [start, end] = data.range.split('-').map((r: string) => +r)
39 | lines = lines.slice(start - 1, end)
40 | }
41 |
42 | this.value = lines.map(l => `${l}
\n`).join('')
43 | }
44 | else if(id === "static/list") {
45 | const path = data.field.split('.')
46 | let list: any = config.datas;
47 |
48 | for (let i = 0; i < path.length; i++) {
49 | list = list[path[i]];
50 | }
51 |
52 | const {title, content} = list
53 |
54 | this.value = `${title ? `${title}
\n` : ''}\n${content.map(l => ` - ${l}
\n`).join('')}
\n`
55 | }
56 | else if(id === "static/socials") {
57 | let readMeString = ''
58 | readMeString += `Reach Me
\n`;
59 | readMeString += `\n`;
60 | readMeString += config.datas.perso.socials.map((social) => {
61 | return ` \n
\n \n`;
62 | }).join('');
63 | readMeString += `
\n`;
64 |
65 | this.value = readMeString
66 | }
67 | else if(id === "static/skills") {
68 | let skills = config.skills;
69 | let readMeString = ''
70 |
71 | readMeString += `Technical skills
\n`;
72 |
73 | if(config.skills.learning) {
74 | readMeString += `Currently learning:\n`;
75 | readMeString += skills.learning.map((skill) => {
76 | return ` \n
\n \n`;
77 | }).join('');
78 | readMeString += `
\n`;
79 | }
80 |
81 | // Front
82 | readMeString += `Front-end technologies
\n\n`;
83 | readMeString += skills.front.map((skill) => {
84 | return ` \n
\n \n`;
85 | }).join('');
86 | readMeString += `
\n`;
87 |
88 | // Back
89 | readMeString += `Back-end technologies
\n\n`;
90 | readMeString += skills.back.map((skill) => {
91 | return ` \n
\n \n`;
92 | }).join('');
93 | readMeString += `
\n`;
94 |
95 | // Notions
96 | readMeString += `Other technologies where I have notions
\n\n`;
97 | readMeString += skills.notions.map((skill) => {
98 | return ` \n
\n \n`;
99 | }).join('');
100 | readMeString += `
\n`;
101 |
102 | // Tools
103 | readMeString += `Tools
\n\n`;
104 | readMeString += skills.tools.map((skill) => {
105 | return ` \n
\n \n`;
106 | }).join('');
107 | readMeString += `
\n`;
108 |
109 | this.value = readMeString
110 | }
111 | else if(id === "static/trigger") {
112 | let readMeString = ''
113 | readMeString += `Work in progress
\n`;
114 | readMeString += `Other features are in progress, feel free to follow me to discover them.
\n`;
115 | readMeString += `To understand how it works, take a look here
\n`;
116 | readMeString += `\n
\n
\n`;
117 | readMeString += `\n See ya <3\n
\n`;
118 | this.value = readMeString
119 | }
120 |
121 | return this.value
122 | }
123 | }
124 |
125 | class AsyncModule {
126 |
127 | constructor(private parent: Renderer, public flat: any) {}
128 |
129 | async render() {
130 |
131 | const {id, data, options} = this.flat
132 |
133 | if(id === "dynamic/followers") {
134 |
135 | const followers = (this.parent.services.get('request')! as RequestService).lastFollowers
136 |
137 | let returnString = '';
138 | returnString += `\n \n \n Last Followers | \n
\n \n \n`;
139 | returnString += JSON.parse(JSON.stringify(followers.lastFollowers)).reverse().map((follower, index) => {
140 | return ` \n ${followers.followerCount - (followers.lastFollowers.length - index - 1)} | \n \n \n \n \n | \n \n ${follower.login}\n | \n
\n`;
141 | }).join('');
142 | returnString += ` \n ${followers.followerCount + 1} | \n Maybe You ? (can take a few minutes to update) | \n
`;
143 | returnString += `\n \n
\n`;
144 |
145 | return returnString
146 | }
147 | else if(id.startsWith('games/')) {
148 | return await (this.parent.services.get(id.split('/')[1])! as ChessService | GameboyService | GbaService | MinesweeperService | WordleService).toMd(this.parent.BASE_URL, data, options)
149 | }
150 | else if(id === "dynamic/generated") {
151 | let currentDate = new Date();
152 | const days = [ "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
153 | const months = [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep" , "Oct", "Nov", "Dec"]
154 | return `Generated in ${(Date.now() - this.parent.startRenderDate) / 1000}s on ${days[currentDate.getDay()]} ${months[currentDate.getMonth()]} ${currentDate.getDate()} at ${currentDate.getHours()}:${currentDate.getMinutes().toString().padStart(2, '0')}
\n`;
155 | }
156 | }
157 | }
158 |
159 | class Renderer {
160 | BASE_URL = `${process.env.APP_PROTOCOL}://${process.env.APP_SUB_DOMAIN}.${process.env.APP_DOMAIN}`
161 | services = new Map()
162 | structure: (Module | AsyncModule)[]
163 | startRenderDate: number
164 | config: Config
165 | constructor() {
166 | this.config = JSON.parse(fs.readFileSync('./config.json').toString())
167 | this.createStructure()
168 | }
169 |
170 | createStructure() {
171 | const {config} = this
172 | this.structure = config.structure.filter(m => !m?.disabled).map(flatModule => {
173 | if(
174 | flatModule.id === "trigger"
175 | || flatModule.id.startsWith("static")
176 | || flatModule.id.startsWith("3rdParty")
177 | ) return new Module(this, flatModule)
178 | return new AsyncModule(this, flatModule)
179 | })
180 | }
181 |
182 | async render() {
183 |
184 | this.startRenderDate = Date.now()
185 |
186 | const parts = await Promise.all(this.structure.filter(m => m.flat.id !== "dynamic/generated").map(m => m.render()))
187 | if(this.structure.at(-1).flat.id === "dynamic/generated") {
188 | let currentDate = new Date();
189 | const days = [ "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
190 | const months = [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep" , "Oct", "Nov", "Dec"]
191 | parts.push(`Generated in ${(Date.now() - this.startRenderDate) / 1000}s on ${days[currentDate.getDay()]} ${months[currentDate.getMonth()]} ${currentDate.getDate()} at ${currentDate.getHours()}:${currentDate.getMinutes().toString().padStart(2, '0')}
\n`);
192 | }
193 |
194 | return parts.join('')
195 | }
196 | }
197 |
198 | export default new Renderer()
--------------------------------------------------------------------------------
/src/readme/readme.module.ts:
--------------------------------------------------------------------------------
1 | import { Module, forwardRef } from '@nestjs/common';
2 | import { ReadmeService } from './readme.service';
3 | import { GamesModule } from 'src/games/games.module';
4 | import { RequestModule } from 'src/request/request.module';
5 | import { ConfigService } from 'src/config/config.service';
6 |
7 | @Module({
8 | imports: [forwardRef(() => GamesModule), RequestModule],
9 | providers: [ReadmeService, ConfigService],
10 | exports: [ReadmeService]
11 | })
12 | export class ReadmeModule {}
13 |
--------------------------------------------------------------------------------
/src/readme/readme.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, OnModuleInit } from '@nestjs/common';
2 | import { Octokit } from 'octokit';
3 | import { MinesweeperService } from 'src/games/minesweeper/minesweeper.service';
4 | import { ChessService } from 'src/games/chess/chess.service';
5 | import { RequestService } from 'src/request/request.service';
6 | import { WordleService } from 'src/games/wordle/wordle.service';
7 | import { GbaService } from 'src/games/gba/gba.service';
8 | import renderer from './Renderer';
9 | import { ConfigService } from 'src/config/config.service';
10 |
11 | @Injectable()
12 | export class ReadmeService {
13 | currentContentSha: string | null;
14 | startDateRender: number;
15 |
16 | constructor(
17 | private configService: ConfigService,
18 | private requestService: RequestService,
19 | private minesweeperService: MinesweeperService,
20 | private chessService: ChessService,
21 | private wordleService: WordleService,
22 | private gbaService: GbaService,
23 | ) {
24 | renderer.services.set("request", this.requestService)
25 | renderer.services.set("minesweeper", this.minesweeperService)
26 | renderer.services.set("chess", this.chessService)
27 | renderer.services.set("wordle", this.wordleService)
28 | renderer.services.set("gba", this.gbaService)
29 | this.currentContentSha = null;
30 | }
31 |
32 | async push(octokit: Octokit, message: string, content: string, sha: string): Promise {
33 | const {config} = this.configService
34 | return (await octokit.request(
35 | `PUT /repos/${config.datas.repo.owner}/${config.datas.repo.name}/contents/${config.datas.repo.readme.path}`,
36 | {
37 | owner: config.datas.repo.owner,
38 | repo: config.datas.repo.name,
39 | path: config.datas.repo.readme.path,
40 | message,
41 | committer: {
42 | name: process.env.OCTO_COMMITTER_NAME,
43 | email: process.env.OCTO_COMMITTER_EMAIL,
44 | },
45 | content,
46 | sha,
47 | },
48 | )).data.content.sha
49 | }
50 |
51 | async commit(commitMessage: string) {
52 | const {config} = this.configService
53 | this.startDateRender = Date.now();
54 | const octokit = new Octokit({ auth: process.env.GH_TOKEN });
55 |
56 | let sha: string | any = this.currentContentSha
57 | if(!this.currentContentSha) {
58 | sha = (await octokit.request(
59 | `GET /repos/${config.datas.repo.owner}/${config.datas.repo.name}/contents/${config.datas.repo.readme.path}`,
60 | )).data.sha;
61 | }
62 |
63 | const readmeContent = await renderer.render()
64 |
65 | const buffer = Buffer.from(readmeContent);
66 | const base64 = buffer.toString('base64');
67 | if(process.env.NO_COMMIT === "true") {
68 | console.log(readmeContent)
69 | return
70 | }
71 | let pushRespSha: string
72 | try {
73 | pushRespSha = await this.push(octokit, commitMessage, base64, sha)
74 | console.log(readmeContent)
75 | } catch (e) {
76 | this.currentContentSha = (await octokit.request(`GET /repos/${config.datas.repo.owner}/${config.datas.repo.name}/contents/${config.datas.repo.readme.path}`)).data.sha
77 | pushRespSha = await this.push(octokit, commitMessage, base64, this.currentContentSha)
78 | }
79 | this.currentContentSha = pushRespSha;
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/redis/redis.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { RedisService } from './redis.service';
3 |
4 | @Module({
5 | providers: [RedisService],
6 | exports: [RedisService]
7 | })
8 | export class RedisModule {}
9 |
--------------------------------------------------------------------------------
/src/redis/redis.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, Logger } from '@nestjs/common';
2 | import { RedisClientType, createClient } from 'redis';
3 |
4 | @Injectable()
5 | export class RedisService {
6 | public client: RedisClientType;
7 | private readonly logger = new Logger('DB')
8 | constructor() {
9 | this.client = createClient({
10 | url: process.env.REDIS_URL
11 | });
12 | (async () => {
13 | await this.client.connect();
14 | if(await this.client.ping() === 'PONG') this.logger.log('Connection established to the database')
15 | })()
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/request/request.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { RequestService } from './request.service';
3 | import { ConfigService } from 'src/config/config.service';
4 |
5 | @Module({
6 | providers: [RequestService, ConfigService],
7 | exports: [RequestService]
8 | })
9 | export class RequestModule {}
10 |
--------------------------------------------------------------------------------
/src/request/request.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import axios from 'axios';
3 | import { ConfigService } from 'src/config/config.service';
4 |
5 | @Injectable()
6 | export class RequestService {
7 | public lastFollowers: {followerCount: number, lastFollowers: {login: string, avatarUrl: string}[]} = {followerCount: 0, lastFollowers: []}
8 |
9 | constructor(
10 | private configService: ConfigService
11 | ) {
12 | this.getFollowers(3).then((followers) => {
13 | this.lastFollowers = followers
14 | })
15 | }
16 |
17 | async getFollowers(limit: number): Promise<{followerCount: number, lastFollowers: {login: string, avatarUrl: string}[]}> {
18 | const {config} = this.configService
19 | try {
20 | const query = `
21 | query {
22 | user(login: "${config.datas.repo.owner}") {
23 | totalCount: followers {
24 | totalCount
25 | }
26 | followers(first: ${limit}) {
27 | nodes {
28 | login
29 | avatarUrl
30 | }
31 | }
32 | }
33 | }`;
34 | const response = await axios.post('https://api.github.com/graphql', { query }, {
35 | headers: {
36 | Authorization: `Bearer ${process.env.GH_TOKEN}`,
37 | },
38 | });
39 | return {
40 | followerCount: response.data.data.user.totalCount.totalCount,
41 | lastFollowers: response.data.data.user.followers.nodes
42 | }
43 | } catch (error) {
44 | console.error(error)
45 | return {followerCount: 0, lastFollowers: []}
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/trigger/trigger.controller.ts:
--------------------------------------------------------------------------------
1 | import { join } from 'path';
2 | import { Controller, Get, Res } from '@nestjs/common';
3 | import { Response } from 'express';
4 | import { ReadmeService } from 'src/readme/readme.service';
5 | import { RequestService } from 'src/request/request.service';
6 |
7 | @Controller('trigger')
8 | export class TriggerController {
9 | constructor(
10 | private requestService: RequestService,
11 | private readMeService: ReadmeService,
12 | ) {}
13 |
14 | @Get()
15 | trigger(@Res() res: Response) {
16 | // return as soon as possible
17 | this.requestService.getFollowers(3).then((followers) => {
18 | if(JSON.stringify(followers) !== JSON.stringify(this.requestService.lastFollowers)) {
19 | this.requestService.lastFollowers = followers
20 | this.readMeService.commit(':alarm_clock: Update followers table')
21 | }
22 | })
23 | return res.sendFile(join(process.cwd(), 'public/trigger.webp'))
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/trigger/trigger.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { TriggerController } from './trigger.controller';
3 | import { ReadmeModule } from 'src/readme/readme.module';
4 | import { RequestModule } from 'src/request/request.module';
5 |
6 | @Module({
7 | imports: [ReadmeModule, RequestModule],
8 | controllers: [TriggerController],
9 | })
10 | export class TriggerModule {}
11 |
--------------------------------------------------------------------------------
/src/users/users.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { UsersService } from './users.service';
3 |
4 | @Module({
5 | providers: [UsersService]
6 | })
7 | export class UsersModule {}
8 |
--------------------------------------------------------------------------------
/src/users/users.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { UsersService } from './users.service';
3 |
4 | describe('UsersService', () => {
5 | let service: UsersService;
6 |
7 | beforeEach(async () => {
8 | const module: TestingModule = await Test.createTestingModule({
9 | providers: [UsersService],
10 | }).compile();
11 |
12 | service = module.get(UsersService);
13 | });
14 |
15 | it('should be defined', () => {
16 | expect(service).toBeDefined();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/src/users/users.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 |
3 | @Injectable()
4 | export class UsersService {}
5 |
--------------------------------------------------------------------------------
/test/app.e2e-spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { INestApplication } from '@nestjs/common';
3 | import * as request from 'supertest';
4 | import { AppModule } from './../src/app.module';
5 |
6 | describe('AppController (e2e)', () => {
7 | let app: INestApplication;
8 |
9 | beforeEach(async () => {
10 | const moduleFixture: TestingModule = await Test.createTestingModule({
11 | imports: [AppModule],
12 | }).compile();
13 |
14 | app = moduleFixture.createNestApplication();
15 | await app.init();
16 | });
17 |
18 | it('/ (GET)', () => {
19 | return request(app.getHttpServer())
20 | .get('/')
21 | .expect(200)
22 | .expect('Hello World!');
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/test/jest-e2e.json:
--------------------------------------------------------------------------------
1 | {
2 | "moduleFileExtensions": ["js", "json", "ts"],
3 | "rootDir": ".",
4 | "testEnvironment": "node",
5 | "testRegex": ".e2e-spec.ts$",
6 | "transform": {
7 | "^.+\\.(t|j)s$": "ts-jest"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
4 | }
5 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "declaration": true,
5 | "removeComments": true,
6 | "emitDecoratorMetadata": true,
7 | "experimentalDecorators": true,
8 | "allowSyntheticDefaultImports": true,
9 | "target": "es2017",
10 | "sourceMap": true,
11 | "outDir": "./dist",
12 | "baseUrl": "./",
13 | "incremental": true,
14 | "skipLibCheck": true,
15 | "strictNullChecks": false,
16 | "noImplicitAny": false,
17 | "strictBindCallApply": false,
18 | "forceConsistentCasingInFileNames": false,
19 | "noFallthroughCasesInSwitch": false,
20 | "resolveJsonModule": true,
21 | "paths": {
22 | "~public/*": ["./public/*"]
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------