From 72a660821a3c0deb658be4dfe39a13909e3c2b28 Mon Sep 17 00:00:00 2001 From: creations Date: Sun, 20 Apr 2025 12:48:07 -0400 Subject: [PATCH 01/21] try to fix http return on https --- src/helpers/badges.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/helpers/badges.ts b/src/helpers/badges.ts index 68d5ce4..ad5406b 100644 --- a/src/helpers/badges.ts +++ b/src/helpers/badges.ts @@ -2,6 +2,14 @@ import { discordBadgeDetails, discordBadges } from "@config/discordBadges"; import { badgeServices, botToken, redisTtl } from "@config/environment"; import { fetch, redis } from "bun"; +function getRequestOrigin(request: Request): string { + const headers = request.headers; + const forwardedProto = headers.get("X-Forwarded-Proto") || "http"; + const host = headers.get("Host") || new URL(request.url).host; + return `${forwardedProto}://${host}`; +} + + export async function fetchBadges( userId: string, services: string[], @@ -141,11 +149,12 @@ export async function fetchBadges( if (!res.ok) break; const data = await res.json(); + const origin = request ? getRequestOrigin(request) : ""; if (data.avatar.startsWith("a_")) { result.push({ tooltip: "Discord Nitro", - badge: `${request ? new URL(request.url).origin : ""}/public/badges/discord/NITRO.svg`, + badge: `${origin}/public/badges/discord/NITRO.svg`, }); } @@ -155,7 +164,7 @@ export async function fetchBadges( discordBadgeDetails[flag as keyof typeof discordBadgeDetails]; result.push({ tooltip: badge.tooltip, - badge: `${request ? new URL(request.url).origin : ""}${badge.icon}`, + badge: `${origin}${badge.icon}`, }); } } From 881d4a08696dd24c5b011f617f1372a3055ffdd4 Mon Sep 17 00:00:00 2001 From: creations Date: Sun, 20 Apr 2025 15:52:56 -0400 Subject: [PATCH 02/21] Fix lint --- src/helpers/badges.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/helpers/badges.ts b/src/helpers/badges.ts index ad5406b..ba8b272 100644 --- a/src/helpers/badges.ts +++ b/src/helpers/badges.ts @@ -9,7 +9,6 @@ function getRequestOrigin(request: Request): string { return `${forwardedProto}://${host}`; } - export async function fetchBadges( userId: string, services: string[], From dd4a96cea40997ae7e461945318cb094344a25dc Mon Sep 17 00:00:00 2001 From: creations Date: Tue, 22 Apr 2025 20:16:51 -0400 Subject: [PATCH 03/21] move to logger on npm --- package.json | 3 +- src/helpers/char.ts | 7 -- src/helpers/logger.ts | 205 ------------------------------------------ src/index.ts | 2 +- src/server.ts | 6 +- src/websocket.ts | 2 +- 6 files changed, 8 insertions(+), 217 deletions(-) delete mode 100644 src/helpers/logger.ts diff --git a/package.json b/package.json index db5e8a9..0e80369 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "dev": "bun run --hot src/index.ts --dev", "lint": "bunx biome check", "lint:fix": "bunx biome check --fix", - "cleanup": "rm -rf logs node_modules bun.lockdb" + "cleanup": "rm -rf logs node_modules bun.lock" }, "devDependencies": { "@types/bun": "^1.2.9", @@ -19,6 +19,7 @@ "typescript": "^5.8.3" }, "dependencies": { + "@creations.works/logger": "^1.0.3", "ejs": "^3.1.10" } } diff --git a/src/helpers/char.ts b/src/helpers/char.ts index c885429..74868aa 100644 --- a/src/helpers/char.ts +++ b/src/helpers/char.ts @@ -1,10 +1,3 @@ -export function timestampToReadable(timestamp?: number): string { - const date: Date = - timestamp && !Number.isNaN(timestamp) ? new Date(timestamp) : new Date(); - if (Number.isNaN(date.getTime())) return "Invalid Date"; - return date.toISOString().replace("T", " ").replace("Z", ""); -} - export function validateID(id: string): boolean { if (!id) return false; diff --git a/src/helpers/logger.ts b/src/helpers/logger.ts deleted file mode 100644 index 4cbb12b..0000000 --- a/src/helpers/logger.ts +++ /dev/null @@ -1,205 +0,0 @@ -import type { Stats } from "node:fs"; -import { - type WriteStream, - createWriteStream, - existsSync, - mkdirSync, - statSync, -} from "node:fs"; -import { EOL } from "node:os"; -import { basename, join } from "node:path"; -import { environment } from "@config/environment"; -import { timestampToReadable } from "@helpers/char"; - -class Logger { - private static instance: Logger; - private static log: string = join(__dirname, "../../logs"); - - public static getInstance(): Logger { - if (!Logger.instance) { - Logger.instance = new Logger(); - } - - return Logger.instance; - } - - private writeToLog(logMessage: string): void { - if (environment.development) return; - - const date: Date = new Date(); - const logDir: string = Logger.log; - const logFile: string = join( - logDir, - `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}.log`, - ); - - if (!existsSync(logDir)) { - mkdirSync(logDir, { recursive: true }); - } - - let addSeparator = false; - - if (existsSync(logFile)) { - const fileStats: Stats = statSync(logFile); - if (fileStats.size > 0) { - const lastModified: Date = new Date(fileStats.mtime); - if ( - lastModified.getFullYear() === date.getFullYear() && - lastModified.getMonth() === date.getMonth() && - lastModified.getDate() === date.getDate() && - lastModified.getHours() !== date.getHours() - ) { - addSeparator = true; - } - } - } - - const stream: WriteStream = createWriteStream(logFile, { flags: "a" }); - - if (addSeparator) { - stream.write(`${EOL}${date.toISOString()}${EOL}`); - } - - stream.write(`${logMessage}${EOL}`); - stream.close(); - } - - private extractFileName(stack: string): string { - const stackLines: string[] = stack.split("\n"); - let callerFile = ""; - - for (let i = 2; i < stackLines.length; i++) { - const line: string = stackLines[i].trim(); - if (line && !line.includes("Logger.") && line.includes("(")) { - callerFile = line.split("(")[1]?.split(")")[0] || ""; - break; - } - } - - return basename(callerFile); - } - - private getCallerInfo(stack: unknown): { - filename: string; - timestamp: string; - } { - const filename: string = - typeof stack === "string" ? this.extractFileName(stack) : "unknown"; - - const readableTimestamp: string = timestampToReadable(); - - return { filename, timestamp: readableTimestamp }; - } - - public info(message: string | string[], breakLine = false): void { - const stack: string = new Error().stack || ""; - const { filename, timestamp } = this.getCallerInfo(stack); - - const joinedMessage: string = Array.isArray(message) - ? message.join(" ") - : message; - - const logMessageParts: ILogMessageParts = { - readableTimestamp: { value: timestamp, color: "90" }, - level: { value: "[INFO]", color: "32" }, - filename: { value: `(${filename})`, color: "36" }, - message: { value: joinedMessage, color: "0" }, - }; - - this.writeToLog(`${timestamp} [INFO] (${filename}) ${joinedMessage}`); - this.writeConsoleMessageColored(logMessageParts, breakLine); - } - - public warn(message: string | string[], breakLine = false): void { - const stack: string = new Error().stack || ""; - const { filename, timestamp } = this.getCallerInfo(stack); - - const joinedMessage: string = Array.isArray(message) - ? message.join(" ") - : message; - - const logMessageParts: ILogMessageParts = { - readableTimestamp: { value: timestamp, color: "90" }, - level: { value: "[WARN]", color: "33" }, - filename: { value: `(${filename})`, color: "36" }, - message: { value: joinedMessage, color: "0" }, - }; - - this.writeToLog(`${timestamp} [WARN] (${filename}) ${joinedMessage}`); - this.writeConsoleMessageColored(logMessageParts, breakLine); - } - - public error( - message: string | Error | (string | Error)[], - breakLine = false, - ): void { - const stack: string = new Error().stack || ""; - const { filename, timestamp } = this.getCallerInfo(stack); - - const messages: (string | Error)[] = Array.isArray(message) - ? message - : [message]; - const joinedMessage: string = messages - .map((msg: string | Error): string => - typeof msg === "string" ? msg : msg.message, - ) - .join(" "); - - const logMessageParts: ILogMessageParts = { - readableTimestamp: { value: timestamp, color: "90" }, - level: { value: "[ERROR]", color: "31" }, - filename: { value: `(${filename})`, color: "36" }, - message: { value: joinedMessage, color: "0" }, - }; - - this.writeToLog(`${timestamp} [ERROR] (${filename}) ${joinedMessage}`); - this.writeConsoleMessageColored(logMessageParts, breakLine); - } - - public custom( - bracketMessage: string, - bracketMessage2: string, - message: string | string[], - color: string, - breakLine = false, - ): void { - const stack: string = new Error().stack || ""; - const { timestamp } = this.getCallerInfo(stack); - - const joinedMessage: string = Array.isArray(message) - ? message.join(" ") - : message; - - const logMessageParts: ILogMessageParts = { - readableTimestamp: { value: timestamp, color: "90" }, - level: { value: bracketMessage, color }, - filename: { value: `${bracketMessage2}`, color: "36" }, - message: { value: joinedMessage, color: "0" }, - }; - - this.writeToLog( - `${timestamp} ${bracketMessage} (${bracketMessage2}) ${joinedMessage}`, - ); - this.writeConsoleMessageColored(logMessageParts, breakLine); - } - - public space(): void { - console.log(); - } - - private writeConsoleMessageColored( - logMessageParts: ILogMessageParts, - breakLine = false, - ): void { - const logMessage: string = Object.keys(logMessageParts) - .map((key: string) => { - const part: ILogMessagePart = logMessageParts[key]; - return `\x1b[${part.color}m${part.value}\x1b[0m`; - }) - .join(" "); - console.log(logMessage + (breakLine ? EOL : "")); - } -} - -const logger: Logger = Logger.getInstance(); -export { logger }; diff --git a/src/index.ts b/src/index.ts index 60606d4..c4148b4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -import { logger } from "@helpers/logger"; +import { logger } from "@creations.works/logger"; import { serverHandler } from "@/server"; diff --git a/src/server.ts b/src/server.ts index 909813c..ee79859 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,6 +1,6 @@ import { resolve } from "node:path"; import { environment } from "@config/environment"; -import { logger } from "@helpers/logger"; +import { logger } from "@creations.works/logger"; import { type BunFile, FileSystemRouter, @@ -39,7 +39,9 @@ class ServerHandler { logger.info( `Server running at http://${server.hostname}:${server.port}`, - true, + { + breakLine: true, + } ); this.logRoutes(); diff --git a/src/websocket.ts b/src/websocket.ts index 99686e8..7b65476 100644 --- a/src/websocket.ts +++ b/src/websocket.ts @@ -1,4 +1,4 @@ -import { logger } from "@helpers/logger"; +import { logger } from "@creations.works/logger"; import type { ServerWebSocket } from "bun"; class WebSocketHandler { From 45d9053aea356b8dcbc62d83ab727fd687408e08 Mon Sep 17 00:00:00 2001 From: creations Date: Tue, 22 Apr 2025 20:19:22 -0400 Subject: [PATCH 04/21] add stupid env var --- src/index.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/index.ts b/src/index.ts index c4148b4..73eaed7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,3 +10,7 @@ main().catch((error: Error) => { logger.error(["Error initializing the server:", error]); process.exit(1); }); + +if (process.env.Inpterodactyl === "true") { + console.log("Server Started"); +} From db53308044cb32d72efa16602470640c0f73e4b8 Mon Sep 17 00:00:00 2001 From: creations Date: Tue, 22 Apr 2025 20:20:39 -0400 Subject: [PATCH 05/21] renmame env --- src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 73eaed7..11b1e84 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,6 +11,6 @@ main().catch((error: Error) => { process.exit(1); }); -if (process.env.Inpterodactyl === "true") { +if (process.env.IN_PTERODACTYL === "true") { console.log("Server Started"); } From a1dae32f80bcdffc8d82f0ff6fe06431fa85131a Mon Sep 17 00:00:00 2001 From: creations Date: Wed, 23 Apr 2025 07:30:12 -0400 Subject: [PATCH 06/21] Fix lint --- src/server.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/server.ts b/src/server.ts index ee79859..5646636 100644 --- a/src/server.ts +++ b/src/server.ts @@ -37,12 +37,9 @@ class ServerHandler { }, }); - logger.info( - `Server running at http://${server.hostname}:${server.port}`, - { - breakLine: true, - } - ); + logger.info(`Server running at http://${server.hostname}:${server.port}`, { + breakLine: true, + }); this.logRoutes(); } From 891d61b2ef66453759e53f23113ed372bc85f77b Mon Sep 17 00:00:00 2001 From: serstars Date: Wed, 23 Apr 2025 17:20:32 +0200 Subject: [PATCH 07/21] Fix Discord badges math Following https://discord.com/developers/docs/resources/user#user-object-user-flags --- config/discordBadges.ts | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/config/discordBadges.ts b/config/discordBadges.ts index 7e0e258..ea7f8a2 100644 --- a/config/discordBadges.ts +++ b/config/discordBadges.ts @@ -1,25 +1,25 @@ export const discordBadges = { // User badges - HYPESQUAD: 2 << 2, - HYPESQUAD_ONLINE_HOUSE_1: 2 << 6, - HYPESQUAD_ONLINE_HOUSE_2: 2 << 7, - HYPESQUAD_ONLINE_HOUSE_3: 2 << 8, + HYPESQUAD: 1 << 2, + HYPESQUAD_ONLINE_HOUSE_1: 1 << 6, + HYPESQUAD_ONLINE_HOUSE_2: 1 << 7, + HYPESQUAD_ONLINE_HOUSE_3: 1 << 8, - STAFF: 2 << 0, - PARTNER: 2 << 1, - CERTIFIED_MODERATOR: 2 << 18, + STAFF: 1 << 0, + PARTNER: 1 << 1, + CERTIFIED_MODERATOR: 1 << 18, - VERIFIED_DEVELOPER: 2 << 17, - ACTIVE_DEVELOPER: 2 << 22, + VERIFIED_DEVELOPER: 1 << 17, + ACTIVE_DEVELOPER: 1 << 22, - PREMIUM_EARLY_SUPPORTER: 2 << 9, + PREMIUM_EARLY_SUPPORTER: 1 << 9, - BUG_HUNTER_LEVEL_1: 2 << 3, - BUG_HUNTER_LEVEL_2: 2 << 14, + BUG_HUNTER_LEVEL_1: 1 << 3, + BUG_HUNTER_LEVEL_2: 1 << 14, // Bot badges - SUPPORTS_COMMANDS: 2 << 23, - USES_AUTOMOD: 2 << 24, + SUPPORTS_COMMANDS: 1 << 23, + USES_AUTOMOD: 1 << 24, }; export const discordBadgeDetails = { From 9d7bd605b73e975607b796469a65f9af693acea2 Mon Sep 17 00:00:00 2001 From: creations Date: Fri, 25 Apr 2025 23:24:13 -0400 Subject: [PATCH 08/21] add more discord badges --- config/discordBadges.ts | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/config/discordBadges.ts b/config/discordBadges.ts index ea7f8a2..0e9bc66 100644 --- a/config/discordBadges.ts +++ b/config/discordBadges.ts @@ -1,23 +1,24 @@ export const discordBadges = { // User badges + STAFF: 1 << 0, + PARTNER: 1 << 1, HYPESQUAD: 1 << 2, + BUG_HUNTER_LEVEL_1: 1 << 3, HYPESQUAD_ONLINE_HOUSE_1: 1 << 6, HYPESQUAD_ONLINE_HOUSE_2: 1 << 7, HYPESQUAD_ONLINE_HOUSE_3: 1 << 8, - - STAFF: 1 << 0, - PARTNER: 1 << 1, - CERTIFIED_MODERATOR: 1 << 18, - + PREMIUM_EARLY_SUPPORTER: 1 << 9, + TEAM_USER: 1 << 10, + SYSTEM: 1 << 12, + BUG_HUNTER_LEVEL_2: 1 << 14, VERIFIED_DEVELOPER: 1 << 17, + CERTIFIED_MODERATOR: 1 << 18, + SPAMMER: 1 << 20, ACTIVE_DEVELOPER: 1 << 22, - PREMIUM_EARLY_SUPPORTER: 1 << 9, - - BUG_HUNTER_LEVEL_1: 1 << 3, - BUG_HUNTER_LEVEL_2: 1 << 14, - // Bot badges + VERIFIED_BOT: 1 << 16, + BOT_HTTP_INTERACTIONS: 1 << 19, SUPPORTS_COMMANDS: 1 << 23, USES_AUTOMOD: 1 << 24, }; From 50c5d5d55146e550bf405b90059e7eeb0466ef35 Mon Sep 17 00:00:00 2001 From: creations Date: Mon, 28 Apr 2025 18:04:55 -0400 Subject: [PATCH 09/21] change to bsd -3 --- LICENSE | 41 ++++++++++++++++++++++++----------------- README.md | 2 +- 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/LICENSE b/LICENSE index fb5f6af..d93a942 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,28 @@ -MIT License +BSD 3-Clause License -Copyright (c) 2025 [fullname] +Copyright (c) 2025, creations.works -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md index 5084653..28e4b3d 100644 --- a/README.md +++ b/README.md @@ -91,4 +91,4 @@ bun run start ``` ## License -[MIT](LICENSE) +[BSD 3](LICENSE) From 49ab7d6f197458822d6ae2d75ce7a9d095c24d26 Mon Sep 17 00:00:00 2001 From: creations Date: Fri, 2 May 2025 19:09:13 +0200 Subject: [PATCH 10/21] Update README.md --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 28e4b3d..2d4474c 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,9 @@ A fast Discord badge aggregation API built with [Bun](https://bun.sh) and Redis caching. +# Preview +https://badges.creations.works + ## Features - Aggregates custom badge data from multiple sources (e.g. Vencord, Nekocord, Equicord, etc.) From 53a1bb7d6be2d166ca7b7bdaeaf31ec0f523abb5 Mon Sep 17 00:00:00 2001 From: creations Date: Fri, 2 May 2025 22:59:17 +0200 Subject: [PATCH 11/21] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 2d4474c..4641b5d 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,7 @@ GET /:userId - Equicord - Nekocord - ReviewDb +- Nekocord ### Example From 4ff0577906c8119357d979420cc409c563b5ada2 Mon Sep 17 00:00:00 2001 From: creations Date: Fri, 2 May 2025 23:00:21 +0200 Subject: [PATCH 12/21] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4641b5d..e90c298 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,8 @@ GET /:userId - Equicord - Nekocord - ReviewDb -- Nekocord +- Enmity +- Discord ( some ) ### Example From 0f36203c1c8527357cdb98c8e2b7c3d21f8ed395 Mon Sep 17 00:00:00 2001 From: creations Date: Fri, 2 May 2025 23:04:43 +0200 Subject: [PATCH 13/21] Update README.md --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index e90c298..4168fd9 100644 --- a/README.md +++ b/README.md @@ -59,11 +59,11 @@ GET /:userId ### Query Parameters -| Name | Description | -|--------------|--------------------------------------------------------------------------| -| `services` | A comma or space separated list of services to fetch badges from | -| `cache` | Set to `true` or `false` (default: `true`). `false` bypasses Redis | -| `seperated` | Set to `true` to return results grouped by service, else merged array | +| Name | Description | +|--------------|---------------------------------------------------------------------------------------------------| +| `services` | A comma or space separated list of services to fetch badges from, if this is empty it fetches all | +| `cache` | Set to `true` or `false` (default: `true`). `false` bypasses Redis | +| `seperated` | Set to `true` to return results grouped by service, else merged array | ### Supported Services From 0ba0181e2b54ad86e2b6bb3a58818f91219ad815 Mon Sep 17 00:00:00 2001 From: creations Date: Fri, 2 May 2025 23:05:40 +0200 Subject: [PATCH 14/21] Update README.md last time trust --- README.md | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/README.md b/README.md index 4168fd9..a4783ec 100644 --- a/README.md +++ b/README.md @@ -80,18 +80,10 @@ GET /:userId GET /209830981060788225?seperated=true&cache=true&services=equicord ``` -## Development - -Run formatting and linting with BiomeJS: - -```bash -bun run lint -bun run lint:fix -``` - ## Start the Server ```bash +bun i bun run start ``` From 8cfa75ec5716d89a022026cae4f243ebe7d0f5c8 Mon Sep 17 00:00:00 2001 From: creations Date: Fri, 30 May 2025 20:12:53 -0400 Subject: [PATCH 15/21] move to @atums/echo logger, --- .gitignore | 1 + logger.json | 39 ++++++++++++++ package.json | 2 +- src/index.ts | 7 ++- src/server.ts | 132 +++++++++++++++++++++++++++++++++-------------- src/websocket.ts | 18 ++++--- 6 files changed, 151 insertions(+), 48 deletions(-) create mode 100644 logger.json diff --git a/.gitignore b/.gitignore index 97ce421..c7c8dc9 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ bun.lock .env .vscode/settings.json +logs diff --git a/logger.json b/logger.json new file mode 100644 index 0000000..521b3bc --- /dev/null +++ b/logger.json @@ -0,0 +1,39 @@ +{ + "directory": "logs", + "level": "debug", + "disableFile": false, + + "rotate": true, + "maxFiles": 3, + + "console": true, + "consoleColor": true, + + "dateFormat": "yyyy-MM-dd HH:mm:ss.SSS", + "timezone": "local", + + "silent": false, + + "pattern": "{color:gray}{pretty-timestamp}{reset} {color:levelColor}[{level-name}]{reset} {color:gray}({reset}{file-name}:{color:blue}{line}{reset}:{color:blue}{column}{color:gray}){reset} {data}", + "levelColor": { + "debug": "blue", + "info": "green", + "warn": "yellow", + "error": "red", + "fatal": "red" + }, + + "customPattern": "{color:gray}{pretty-timestamp}{reset} {color:tagColor}[{tag}]{reset} {color:contextColor}({context}){reset} {data}", + "customColors": { + "GET": "green", + "POST": "blue", + "PUT": "yellow", + "DELETE": "red", + "PATCH": "cyan", + "HEAD": "magenta", + "OPTIONS": "white", + "TRACE": "gray" + }, + + "prettyPrint": true +} diff --git a/package.json b/package.json index 0e80369..dc9b552 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "typescript": "^5.8.3" }, "dependencies": { - "@creations.works/logger": "^1.0.3", + "@atums/echo": "^1.0.3", "ejs": "^3.1.10" } } diff --git a/src/index.ts b/src/index.ts index 11b1e84..74320a0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -import { logger } from "@creations.works/logger"; +import { echo } from "@atums/echo"; import { serverHandler } from "@/server"; @@ -7,7 +7,10 @@ async function main(): Promise { } main().catch((error: Error) => { - logger.error(["Error initializing the server:", error]); + echo.error({ + message: "Error initializing the server", + error: error.message, + }); process.exit(1); }); diff --git a/src/server.ts b/src/server.ts index 5646636..e851a58 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,6 +1,6 @@ import { resolve } from "node:path"; +import { echo } from "@atums/echo"; import { environment } from "@config/environment"; -import { logger } from "@creations.works/logger"; import { type BunFile, FileSystemRouter, @@ -37,15 +37,20 @@ class ServerHandler { }, }); - logger.info(`Server running at http://${server.hostname}:${server.port}`, { - breakLine: true, - }); + const accessUrls: string[] = [ + `http://${server.hostname}:${server.port}`, + `http://localhost:${server.port}`, + `http://127.0.0.1:${server.port}`, + ]; + + echo.info(`Server running at ${accessUrls[0]}`); + echo.info(`Access via: ${accessUrls[1]} or ${accessUrls[2]}`); this.logRoutes(); } private logRoutes(): void { - logger.info("Available routes:"); + echo.info("Available routes:"); const sortedRoutes: [string, string][] = Object.entries( this.router.routes, @@ -54,14 +59,19 @@ class ServerHandler { ); for (const [path, filePath] of sortedRoutes) { - logger.info(`Route: ${path}, File: ${filePath}`); + echo.info(`Route: ${path}, File: ${filePath}`); } } - private async serveStaticFile(pathname: string): Promise { - try { - let filePath: string; + private async serveStaticFile( + request: ExtendedRequest, + pathname: string, + ip: string, + ): Promise { + let filePath: string; + let response: Response; + try { if (pathname === "/favicon.ico") { filePath = resolve("public", "assets", "favicon.ico"); } else { @@ -74,16 +84,47 @@ class ServerHandler { const fileContent: ArrayBuffer = await file.arrayBuffer(); const contentType: string = file.type || "application/octet-stream"; - return new Response(fileContent, { + response = new Response(fileContent, { headers: { "Content-Type": contentType }, }); + } else { + echo.warn(`File not found: ${filePath}`); + response = new Response("Not Found", { status: 404 }); } - logger.warn(`File not found: ${filePath}`); - return new Response("Not Found", { status: 404 }); } catch (error) { - logger.error([`Error serving static file: ${pathname}`, error as Error]); - return new Response("Internal Server Error", { status: 500 }); + echo.error({ + message: `Error serving static file: ${pathname}`, + error: error as Error, + }); + response = new Response("Internal Server Error", { status: 500 }); } + + this.logRequest(request, response, ip); + return response; + } + + private logRequest( + request: ExtendedRequest, + response: Response, + ip: string | undefined, + ): void { + const pathname = new URL(request.url).pathname; + + const ignoredStartsWith: string[] = ["/public"]; + const ignoredPaths: string[] = ["/favicon.ico"]; + + if ( + ignoredStartsWith.some((prefix) => pathname.startsWith(prefix)) || + ignoredPaths.includes(pathname) + ) { + return; + } + + echo.custom(`${request.method}`, `${response.status}`, [ + request.url, + `${(performance.now() - request.startPerf).toFixed(2)}ms`, + ip || "unknown", + ]); } private async handleRequest( @@ -93,14 +134,44 @@ class ServerHandler { const extendedRequest: ExtendedRequest = request as ExtendedRequest; extendedRequest.startPerf = performance.now(); + const headers = request.headers; + let ip = server.requestIP(request)?.address; + let response: Response; + + if (!ip || ip.startsWith("172.") || ip === "127.0.0.1") { + ip = + headers.get("CF-Connecting-IP")?.trim() || + headers.get("X-Real-IP")?.trim() || + headers.get("X-Forwarded-For")?.split(",")[0].trim() || + "unknown"; + } + const pathname: string = new URL(request.url).pathname; + + const baseDir = resolve("public/custom"); + const customPath = resolve(baseDir, pathname.slice(1)); + + if (!customPath.startsWith(baseDir)) { + return new Response("Forbidden", { status: 403 }); + } + + const customFile = Bun.file(customPath); + if (await customFile.exists()) { + const content = await customFile.arrayBuffer(); + const type = customFile.type || "application/octet-stream"; + response = new Response(content, { + headers: { "Content-Type": type }, + }); + this.logRequest(extendedRequest, response, ip); + return response; + } + if (pathname.startsWith("/public") || pathname === "/favicon.ico") { - return await this.serveStaticFile(pathname); + return await this.serveStaticFile(extendedRequest, pathname, ip); } const match: MatchedRoute | null = this.router.match(request); let requestBody: unknown = {}; - let response: Response; if (match) { const { filePath, params, query } = match; @@ -198,7 +269,10 @@ class ServerHandler { } } } catch (error: unknown) { - logger.error([`Error handling route ${request.url}:`, error as Error]); + echo.error({ + message: `Error handling route ${request.url}`, + error: error, + }); response = Response.json( { @@ -220,31 +294,11 @@ class ServerHandler { ); } - const headers = request.headers; - let ip = server.requestIP(request)?.address; - - if (!ip || ip.startsWith("172.") || ip === "127.0.0.1") { - ip = - headers.get("CF-Connecting-IP")?.trim() || - headers.get("X-Real-IP")?.trim() || - headers.get("X-Forwarded-For")?.split(",")[0].trim() || - "unknown"; - } - - logger.custom( - `[${request.method}]`, - `(${response.status})`, - [ - request.url, - `${(performance.now() - extendedRequest.startPerf).toFixed(2)}ms`, - ip || "unknown", - ], - "90", - ); - + this.logRequest(extendedRequest, response, ip); return response; } } + const serverHandler: ServerHandler = new ServerHandler( environment.port, environment.host, diff --git a/src/websocket.ts b/src/websocket.ts index 7b65476..1877f6b 100644 --- a/src/websocket.ts +++ b/src/websocket.ts @@ -1,27 +1,33 @@ -import { logger } from "@creations.works/logger"; +import { echo } from "@atums/echo"; import type { ServerWebSocket } from "bun"; class WebSocketHandler { public handleMessage(ws: ServerWebSocket, message: string): void { - logger.info(`WebSocket received: ${message}`); + echo.info(`WebSocket received: ${message}`); try { ws.send(`You said: ${message}`); } catch (error) { - logger.error(["WebSocket send error", error as Error]); + echo.error({ + message: "WebSocket send error", + error: (error as Error).message, + }); } } public handleOpen(ws: ServerWebSocket): void { - logger.info("WebSocket connection opened."); + echo.info("WebSocket connection opened."); try { ws.send("Welcome to the WebSocket server!"); } catch (error) { - logger.error(["WebSocket send error", error as Error]); + echo.error({ + message: "WebSocket send error", + error: (error as Error).message, + }); } } public handleClose(ws: ServerWebSocket, code: number, reason: string): void { - logger.warn(`WebSocket closed with code ${code}, reason: ${reason}`); + echo.info(`WebSocket closed with code ${code}, reason: ${reason}`); } } From 75d3dab85e59870b1481d6d93147fc361eca5de9 Mon Sep 17 00:00:00 2001 From: creations Date: Wed, 4 Jun 2025 15:47:51 -0400 Subject: [PATCH 16/21] add index route info, make it fetch per hour instead of every user, add health route, update to latest biome config aswell as logger --- biome.json | 24 +- config/{discordBadges.ts => constants.ts} | 58 ++++- config/environment.ts | 43 ---- config/index.ts | 45 ++++ logger.json | 2 +- package.json | 10 +- src/helpers/badges.ts | 192 ---------------- src/index.ts | 22 +- src/lib/badgeCache.ts | 233 +++++++++++++++++++ src/lib/badges.ts | 260 ++++++++++++++++++++++ src/{helpers => lib}/char.ts | 6 +- src/routes/[id].ts | 89 ++++---- src/routes/health.ts | 89 ++++++++ src/routes/index.ts | 57 ++++- src/server.ts | 44 ++-- src/websocket.ts | 2 +- tsconfig.json | 24 +- types/badge.d.ts | 67 ++++++ types/bun.d.ts | 14 +- types/health.d.ts | 17 ++ types/logger.d.ts | 9 - 21 files changed, 943 insertions(+), 364 deletions(-) rename config/{discordBadges.ts => constants.ts} (58%) delete mode 100644 config/environment.ts create mode 100644 config/index.ts delete mode 100644 src/helpers/badges.ts create mode 100644 src/lib/badgeCache.ts create mode 100644 src/lib/badges.ts rename src/{helpers => lib}/char.ts (50%) create mode 100644 src/routes/health.ts create mode 100644 types/health.d.ts delete mode 100644 types/logger.d.ts diff --git a/biome.json b/biome.json index 921a7a5..3a44cc4 100644 --- a/biome.json +++ b/biome.json @@ -7,7 +7,7 @@ }, "files": { "ignoreUnknown": true, - "ignore": [] + "ignore": ["dist"] }, "formatter": { "enabled": true, @@ -17,11 +17,29 @@ "organizeImports": { "enabled": true }, + "css": { + "formatter": { + "indentStyle": "tab", + "lineEnding": "lf" + } + }, "linter": { "enabled": true, "rules": { - "recommended": true - } + "recommended": true, + "correctness": { + "noUnusedImports": "error", + "noUnusedVariables": "error" + }, + "suspicious": { + "noConsole": "error" + }, + "style": { + "useConst": "error", + "noVar": "error" + } + }, + "ignore": ["types"] }, "javascript": { "formatter": { diff --git a/config/discordBadges.ts b/config/constants.ts similarity index 58% rename from config/discordBadges.ts rename to config/constants.ts index 0e9bc66..37a8a5a 100644 --- a/config/discordBadges.ts +++ b/config/constants.ts @@ -1,4 +1,4 @@ -export const discordBadges = { +const discordBadges = { // User badges STAFF: 1 << 0, PARTNER: 1 << 1, @@ -23,7 +23,7 @@ export const discordBadges = { USES_AUTOMOD: 1 << 24, }; -export const discordBadgeDetails = { +const discordBadgeDetails = { HYPESQUAD: { tooltip: "HypeSquad Events", icon: "/public/badges/discord/HYPESQUAD.svg", @@ -86,3 +86,57 @@ export const discordBadgeDetails = { icon: "/public/badges/discord/USES_AUTOMOD.svg", }, }; + +const badgeServices: badgeURLMap[] = [ + { + service: "Vencord", + url: "https://badges.vencord.dev/badges.json", + }, + { + service: "Equicord", // Ekwekord ! WOOP + url: "https://raw.githubusercontent.com/Equicord/Equibored/refs/heads/main/badges.json", + }, + { + service: "Nekocord", + url: "https://nekocord.dev/assets/badges.json", + }, + { + service: "ReviewDb", + url: "https://manti.vendicated.dev/api/reviewdb/badges", + }, + { + service: "Enmity", + url: (userId: string) => ({ + user: `https://raw.githubusercontent.com/enmity-mod/badges/main/${userId}.json`, + badge: (id: string) => + `https://raw.githubusercontent.com/enmity-mod/badges/main/data/${id}.json`, + }), + }, + { + service: "Discord", + url: (userId: string) => `https://discord.com/api/v10/users/${userId}`, + }, +]; + +function getServiceDescription(service: string): string { + const descriptions: Record = { + Vencord: "Custom badges from Vencord Discord client", + Equicord: "Custom badges from Equicord Discord client", + Nekocord: "Custom badges from Nekocord Discord client", + ReviewDb: "Badges from ReviewDB service", + Enmity: "Custom badges from Enmity mobile Discord client", + Discord: "Official Discord badges (staff, partner, hypesquad, etc.)", + }; + + return descriptions[service] || "Custom badge service"; +} + +const gitUrl = "https://git.creations.works/creations/badgeAPI"; + +export { + badgeServices, + discordBadges, + discordBadgeDetails, + getServiceDescription, + gitUrl, +}; diff --git a/config/environment.ts b/config/environment.ts deleted file mode 100644 index 6b056d6..0000000 --- a/config/environment.ts +++ /dev/null @@ -1,43 +0,0 @@ -export const environment: Environment = { - port: Number.parseInt(process.env.PORT || "8080", 10), - host: process.env.HOST || "0.0.0.0", - development: - process.env.NODE_ENV === "development" || process.argv.includes("--dev"), -}; - -export const redisTtl: number = process.env.REDIS_TTL - ? Number.parseInt(process.env.REDIS_TTL, 10) - : 60 * 60 * 1; // 1 hour - -export const badgeServices: badgeURLMap[] = [ - { - service: "Vencord", - url: "https://badges.vencord.dev/badges.json", - }, - { - service: "Equicord", // Ekwekord ! WOOP - url: "https://raw.githubusercontent.com/Equicord/Equibored/refs/heads/main/badges.json", - }, - { - service: "Nekocord", - url: "https://nekocord.dev/assets/badges.json", - }, - { - service: "ReviewDb", - url: "https://manti.vendicated.dev/api/reviewdb/badges", - }, - { - service: "Enmity", - url: (userId: string) => ({ - user: `https://raw.githubusercontent.com/enmity-mod/badges/main/${userId}.json`, - badge: (id: string) => - `https://raw.githubusercontent.com/enmity-mod/badges/main/data/${id}.json`, - }), - }, - { - service: "Discord", - url: (userId: string) => `https://discord.com/api/v10/users/${userId}`, - }, -]; - -export const botToken: string | undefined = process.env.DISCORD_TOKEN; diff --git a/config/index.ts b/config/index.ts new file mode 100644 index 0000000..5f7ad84 --- /dev/null +++ b/config/index.ts @@ -0,0 +1,45 @@ +import { echo } from "@atums/echo"; + +const environment: Environment = { + port: Number.parseInt(process.env.PORT || "8080", 10), + host: process.env.HOST || "0.0.0.0", + development: + process.env.NODE_ENV === "development" || process.argv.includes("--dev"), +}; + +const redisTtl: number = process.env.REDIS_TTL + ? Number.parseInt(process.env.REDIS_TTL, 10) + : 60 * 60 * 1; // 1 hour + +const badgeFetchInterval: number = process.env.BADGE_FETCH_INTERVAL + ? Number.parseInt(process.env.BADGE_FETCH_INTERVAL, 10) + : 60 * 60 * 1000; // 1 hour + +const botToken: string | undefined = process.env.DISCORD_TOKEN; + +function verifyRequiredVariables(): void { + const requiredVariables = ["HOST", "PORT", "DISCORD_TOKEN"]; + + let hasError = false; + + for (const key of requiredVariables) { + const value = process.env[key]; + if (value === undefined || value.trim() === "") { + echo.error(`Missing or empty environment variable: ${key}`); + hasError = true; + } + } + + if (hasError) { + process.exit(1); + } +} + +export * from "@config/constants"; +export { + environment, + redisTtl, + badgeFetchInterval, + botToken, + verifyRequiredVariables, +}; diff --git a/logger.json b/logger.json index 521b3bc..cc10cab 100644 --- a/logger.json +++ b/logger.json @@ -1,6 +1,6 @@ { "directory": "logs", - "level": "debug", + "level": "info", "disableFile": false, "rotate": true, diff --git a/package.json b/package.json index dc9b552..1a420b5 100644 --- a/package.json +++ b/package.json @@ -10,16 +10,10 @@ "cleanup": "rm -rf logs node_modules bun.lock" }, "devDependencies": { - "@types/bun": "^1.2.9", - "@types/ejs": "^3.1.5", - "globals": "^16.0.0", + "@types/bun": "latest", "@biomejs/biome": "^1.9.4" }, - "peerDependencies": { - "typescript": "^5.8.3" - }, "dependencies": { - "@atums/echo": "^1.0.3", - "ejs": "^3.1.10" + "@atums/echo": "latest" } } diff --git a/src/helpers/badges.ts b/src/helpers/badges.ts deleted file mode 100644 index ba8b272..0000000 --- a/src/helpers/badges.ts +++ /dev/null @@ -1,192 +0,0 @@ -import { discordBadgeDetails, discordBadges } from "@config/discordBadges"; -import { badgeServices, botToken, redisTtl } from "@config/environment"; -import { fetch, redis } from "bun"; - -function getRequestOrigin(request: Request): string { - const headers = request.headers; - const forwardedProto = headers.get("X-Forwarded-Proto") || "http"; - const host = headers.get("Host") || new URL(request.url).host; - return `${forwardedProto}://${host}`; -} - -export async function fetchBadges( - userId: string, - services: string[], - options?: FetchBadgesOptions, - request?: Request, -): Promise { - const { nocache = false, separated = false } = options ?? {}; - const results: Record = {}; - - await Promise.all( - services.map(async (service) => { - const entry = badgeServices.find( - (s) => s.service.toLowerCase() === service.toLowerCase(), - ); - if (!entry) return; - - const serviceKey = service.toLowerCase(); - const cacheKey = `badges:${serviceKey}:${userId}`; - - if (!nocache) { - const cached = await redis.get(cacheKey); - if (cached) { - try { - const parsed: Badge[] = JSON.parse(cached); - results[serviceKey] = parsed; - return; - } catch { - // corrupted cache, proceed with fetch :p - } - } - } - - const result: Badge[] = []; - - try { - let url: string | { user: string; badge: (id: string) => string }; - if (typeof entry.url === "function") { - url = entry.url(userId); - } else { - url = entry.url; - } - - switch (serviceKey) { - case "vencord": - case "equicord": { - const res = await fetch(url as string); - if (!res.ok) break; - - const data = await res.json(); - const userBadges = data[userId]; - if (Array.isArray(userBadges)) { - for (const b of userBadges) { - result.push({ - tooltip: b.tooltip, - badge: b.badge, - }); - } - } - break; - } - - case "nekocord": { - const res = await fetch(url as string); - if (!res.ok) break; - - const data = await res.json(); - const userBadgeIds = data.users?.[userId]?.badges; - if (Array.isArray(userBadgeIds)) { - for (const id of userBadgeIds) { - const badgeInfo = data.badges?.[id]; - if (badgeInfo) { - result.push({ - tooltip: badgeInfo.name, - badge: badgeInfo.image, - }); - } - } - } - break; - } - - case "reviewdb": { - const res = await fetch(url as string); - if (!res.ok) break; - - const data = await res.json(); - for (const b of data) { - if (b.discordID === userId) { - result.push({ - tooltip: b.name, - badge: b.icon, - }); - } - } - break; - } - - case "enmity": { - if ( - typeof url !== "object" || - typeof url.user !== "string" || - typeof url.badge !== "function" - ) - break; - - const userRes = await fetch(url.user); - if (!userRes.ok) break; - - const badgeIds: string[] = await userRes.json(); - if (!Array.isArray(badgeIds)) break; - - await Promise.all( - badgeIds.map(async (id) => { - const badgeRes = await fetch(url.badge(id)); - if (!badgeRes.ok) return; - - const badge = await badgeRes.json(); - if (!badge?.name || !badge?.url?.dark) return; - - result.push({ - tooltip: badge.name, - badge: badge.url.dark, - }); - }), - ); - break; - } - - case "discord": { - if (!botToken) break; - - const res = await fetch(url as string, { - headers: { - Authorization: `Bot ${botToken}`, - }, - }); - if (!res.ok) break; - - const data = await res.json(); - const origin = request ? getRequestOrigin(request) : ""; - - if (data.avatar.startsWith("a_")) { - result.push({ - tooltip: "Discord Nitro", - badge: `${origin}/public/badges/discord/NITRO.svg`, - }); - } - - for (const [flag, bitwise] of Object.entries(discordBadges)) { - if (data.flags & bitwise) { - const badge = - discordBadgeDetails[flag as keyof typeof discordBadgeDetails]; - result.push({ - tooltip: badge.tooltip, - badge: `${origin}${badge.icon}`, - }); - } - } - break; - } - } - - if (result.length > 0) { - results[serviceKey] = result; - if (!nocache) { - await redis.set(cacheKey, JSON.stringify(result)); - await redis.expire(cacheKey, redisTtl); - } - } - } catch (_) {} - }), - ); - - if (separated) return results; - - const combined: Badge[] = []; - for (const group of Object.values(results)) { - combined.push(...group); - } - return combined; -} diff --git a/src/index.ts b/src/index.ts index 74320a0..fbdbfe9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,25 @@ import { echo } from "@atums/echo"; - -import { serverHandler } from "@/server"; +import { verifyRequiredVariables } from "@config"; +import { badgeCacheManager } from "@lib/badgeCache"; +import { serverHandler } from "@server"; async function main(): Promise { + verifyRequiredVariables(); + + await badgeCacheManager.initialize(); + + process.on("SIGINT", async () => { + echo.debug("Received SIGINT, shutting down gracefully..."); + await badgeCacheManager.shutdown(); + process.exit(0); + }); + + process.on("SIGTERM", async () => { + echo.debug("Received SIGTERM, shutting down gracefully..."); + await badgeCacheManager.shutdown(); + process.exit(0); + }); + serverHandler.initialize(); } @@ -15,5 +32,6 @@ main().catch((error: Error) => { }); if (process.env.IN_PTERODACTYL === "true") { + // biome-ignore lint/suspicious/noConsole: Needed for Pterodactyl to actually know the server started console.log("Server Started"); } diff --git a/src/lib/badgeCache.ts b/src/lib/badgeCache.ts new file mode 100644 index 0000000..682f6c9 --- /dev/null +++ b/src/lib/badgeCache.ts @@ -0,0 +1,233 @@ +import { echo } from "@atums/echo"; +import { badgeFetchInterval, badgeServices, gitUrl, redisTtl } from "@config"; +import { redis } from "bun"; + +class BadgeCacheManager { + private updateInterval: Timer | null = null; + private readonly CACHE_PREFIX = "badge_service_data:"; + private readonly CACHE_TIMESTAMP_PREFIX = "badge_cache_timestamp:"; + + async initialize(): Promise { + echo.debug("Initializing badge cache manager..."); + + const needsUpdate = await this.checkIfUpdateNeeded(); + if (needsUpdate) { + await this.updateAllServiceData(); + } else { + echo.debug("Badge cache is still valid, skipping initial update"); + } + + this.updateInterval = setInterval( + () => this.updateAllServiceData(), + badgeFetchInterval, + ); + + echo.debug("Badge cache manager initialized with 1-hour update interval"); + } + + async shutdown(): Promise { + if (this.updateInterval) { + clearInterval(this.updateInterval); + this.updateInterval = null; + } + echo.debug("Badge cache manager shut down"); + } + + private async checkIfUpdateNeeded(): Promise { + try { + const staticServices = ["vencord", "equicord", "nekocord", "reviewdb"]; + const now = Date.now(); + + for (const serviceName of staticServices) { + const timestampKey = `${this.CACHE_TIMESTAMP_PREFIX}${serviceName}`; + const cacheKey = `${this.CACHE_PREFIX}${serviceName}`; + + const [timestamp, data] = await Promise.all([ + redis.get(timestampKey), + redis.get(cacheKey), + ]); + + if (!data || !timestamp) { + echo.debug(`Cache missing for service: ${serviceName}`); + return true; + } + + const lastUpdate = Number.parseInt(timestamp, 10); + if (now - lastUpdate > badgeFetchInterval) { + echo.debug(`Cache expired for service: ${serviceName}`); + return true; + } + } + + echo.debug("All service caches are valid"); + return false; + } catch (error) { + echo.warn({ + message: "Failed to check cache validity, forcing update", + error: error instanceof Error ? error.message : String(error), + }); + return true; + } + } + + private async updateAllServiceData(): Promise { + echo.debug("Updating badge service data..."); + + const updatePromises = badgeServices.map(async (service: BadgeService) => { + try { + await this.updateServiceData(service); + } catch (error) { + echo.error({ + message: `Failed to update service data for ${service.service}`, + error: error instanceof Error ? error.message : String(error), + }); + } + }); + + await Promise.allSettled(updatePromises); + echo.debug("Badge service data update completed"); + } + + private async updateServiceData(service: BadgeService): Promise { + const serviceKey = service.service.toLowerCase(); + const cacheKey = `${this.CACHE_PREFIX}${serviceKey}`; + const timestampKey = `${this.CACHE_TIMESTAMP_PREFIX}${serviceKey}`; + + try { + let data: BadgeServiceData | null = null; + + switch (serviceKey) { + case "vencord": + case "equicord": { + if (typeof service.url === "string") { + const res = await fetch(service.url, { + headers: { + "User-Agent": `BadgeAPI/1.0 ${gitUrl}`, + }, + }); + + if (res.ok) { + data = (await res.json()) as VencordEquicordData; + } + } + break; + } + + case "nekocord": { + if (typeof service.url === "string") { + const res = await fetch(service.url, { + headers: { + "User-Agent": `BadgeAPI/1.0 ${gitUrl}`, + }, + }); + + if (res.ok) { + data = (await res.json()) as NekocordData; + } + } + break; + } + + case "reviewdb": { + if (typeof service.url === "string") { + const res = await fetch(service.url, { + headers: { + "User-Agent": `BadgeAPI/1.0 ${gitUrl}`, + }, + }); + + if (res.ok) { + data = (await res.json()) as ReviewDbData; + } + } + break; + } + + case "discord": + case "enmity": + return; + + default: + echo.warn(`Unknown service type: ${serviceKey}`); + return; + } + + if (data) { + const now = Date.now(); + await Promise.all([ + redis.set(cacheKey, JSON.stringify(data)), + redis.set(timestampKey, now.toString()), + redis.expire(cacheKey, redisTtl * 2), + redis.expire(timestampKey, redisTtl * 2), + ]); + + echo.debug(`Updated cache for service: ${service.service}`); + } + } catch (error) { + echo.warn({ + message: `Failed to fetch data for service: ${service.service}`, + error: error instanceof Error ? error.message : String(error), + }); + } + } + + async getServiceData(serviceKey: string): Promise { + const cacheKey = `${this.CACHE_PREFIX}${serviceKey}`; + + try { + const cached = await redis.get(cacheKey); + if (cached) { + return JSON.parse(cached) as BadgeServiceData; + } + } catch (error) { + echo.warn({ + message: `Failed to get cached data for service: ${serviceKey}`, + error: error instanceof Error ? error.message : String(error), + }); + } + + return null; + } + + async getVencordEquicordData( + serviceKey: string, + ): Promise { + const data = await this.getServiceData(serviceKey); + if (data && (serviceKey === "vencord" || serviceKey === "equicord")) { + return data as VencordEquicordData; + } + return null; + } + + async getNekocordData(): Promise { + const data = await this.getServiceData("nekocord"); + if (data) { + return data as NekocordData; + } + return null; + } + + async getReviewDbData(): Promise { + const data = await this.getServiceData("reviewdb"); + if (data) { + return data as ReviewDbData; + } + return null; + } + + async forceUpdateService(serviceName: string): Promise { + const service = badgeServices.find( + (s: BadgeService) => + s.service.toLowerCase() === serviceName.toLowerCase(), + ); + + if (service) { + await this.updateServiceData(service); + echo.info(`Force updated service: ${serviceName}`); + } else { + throw new Error(`Service not found: ${serviceName}`); + } + } +} + +export const badgeCacheManager = new BadgeCacheManager(); diff --git a/src/lib/badges.ts b/src/lib/badges.ts new file mode 100644 index 0000000..284cdcc --- /dev/null +++ b/src/lib/badges.ts @@ -0,0 +1,260 @@ +import { echo } from "@atums/echo"; +import { discordBadgeDetails, discordBadges } from "@config"; +import { badgeServices, botToken, redisTtl } from "@config"; +import { badgeCacheManager } from "@lib/badgeCache"; +import { redis } from "bun"; + +function getRequestOrigin(request: Request): string { + const headers = request.headers; + const forwardedProto = headers.get("X-Forwarded-Proto") || "http"; + const host = headers.get("Host") || new URL(request.url).host; + return `${forwardedProto}://${host}`; +} + +export async function fetchBadges( + userId: string | undefined, + services: string[], + options?: FetchBadgesOptions, + request?: Request, +): Promise { + const { nocache = false, separated = false } = options ?? {}; + const results: Record = {}; + + if (!userId || !Array.isArray(services) || services.length === 0) { + return separated ? results : []; + } + + const userCachePromises = services.map(async (service) => { + const serviceKey = service.toLowerCase(); + const userCacheKey = `user_badges:${serviceKey}:${userId}`; + + if (!nocache) { + try { + const cached = await redis.get(userCacheKey); + if (cached) { + const parsed: Badge[] = JSON.parse(cached); + results[serviceKey] = parsed; + return true; + } + } catch {} + } + return false; + }); + + const cacheHits = await Promise.all(userCachePromises); + + const servicesToFetch = services.filter((_, index) => !cacheHits[index]); + + await Promise.all( + servicesToFetch.map(async (service) => { + const entry = badgeServices.find( + (s) => s.service.toLowerCase() === service.toLowerCase(), + ); + if (!entry) return; + + const serviceKey = service.toLowerCase(); + const result: Badge[] = []; + + try { + switch (serviceKey) { + case "vencord": + case "equicord": { + const serviceData = + await badgeCacheManager.getVencordEquicordData(serviceKey); + if (!serviceData) { + echo.warn(`No cached data for service: ${serviceKey}`); + break; + } + + const userBadges = serviceData[userId]; + if (Array.isArray(userBadges)) { + for (const badgeItem of userBadges) { + result.push({ + tooltip: badgeItem.tooltip, + badge: badgeItem.badge, + }); + } + } + break; + } + + case "nekocord": { + const serviceData = await badgeCacheManager.getNekocordData(); + if (!serviceData) { + echo.warn(`No cached data for service: ${serviceKey}`); + break; + } + + const userBadgeIds = serviceData.users?.[userId]?.badges; + if (Array.isArray(userBadgeIds)) { + for (const id of userBadgeIds) { + const badgeInfo = serviceData.badges?.[id]; + if (badgeInfo) { + result.push({ + tooltip: badgeInfo.name, + badge: badgeInfo.image, + }); + } + } + } + break; + } + + case "reviewdb": { + const serviceData = await badgeCacheManager.getReviewDbData(); + if (!serviceData) { + echo.warn(`No cached data for service: ${serviceKey}`); + break; + } + + for (const badgeItem of serviceData) { + if (badgeItem.discordID === userId) { + result.push({ + tooltip: badgeItem.name, + badge: badgeItem.icon, + }); + } + } + break; + } + + case "enmity": { + if (typeof entry.url !== "function") { + break; + } + + const urlResult = entry.url(userId); + + if ( + typeof urlResult !== "object" || + typeof urlResult.user !== "string" || + typeof urlResult.badge !== "function" + ) { + break; + } + + const userRes = await fetch(urlResult.user); + if (!userRes.ok) break; + + const badgeIds = await userRes.json(); + if (!Array.isArray(badgeIds)) break; + + await Promise.all( + badgeIds.map(async (id: string) => { + try { + const badgeRes = await fetch(urlResult.badge(id)); + if (!badgeRes.ok) return; + + const badge: EnmityBadgeItem = await badgeRes.json(); + if (!badge?.name || !badge?.url?.dark) return; + + result.push({ + tooltip: badge.name, + badge: badge.url.dark, + }); + } catch (error) { + echo.warn({ + message: `Failed to fetch Enmity badge ${id}`, + error: + error instanceof Error ? error.message : String(error), + }); + } + }), + ); + break; + } + + case "discord": { + if (!botToken) { + echo.warn("Discord bot token not configured"); + break; + } + + if (typeof entry.url !== "function") { + echo.warn("Discord service URL should be a function"); + break; + } + + const url = entry.url(userId); + if (typeof url !== "string") { + echo.warn("Discord URL function should return a string"); + break; + } + + const res = await fetch(url, { + headers: { + Authorization: `Bot ${botToken}`, + }, + }); + + if (!res.ok) { + echo.warn( + `Discord API request failed with status: ${res.status}`, + ); + break; + } + + const data: DiscordUserData = await res.json(); + const origin = request ? getRequestOrigin(request) : ""; + + if (data.avatar?.startsWith("a_")) { + result.push({ + tooltip: "Discord Nitro", + badge: `${origin}/public/badges/discord/NITRO.svg`, + }); + } + + if (typeof data.flags === "number") { + for (const [flag, bitwise] of Object.entries(discordBadges)) { + if (data.flags & bitwise) { + const badge = + discordBadgeDetails[ + flag as keyof typeof discordBadgeDetails + ]; + if (badge) { + result.push({ + tooltip: badge.tooltip, + badge: `${origin}${badge.icon}`, + }); + } + } + } + } + break; + } + + default: + echo.warn(`Unknown service: ${serviceKey}`); + break; + } + + if ( + result.length > 0 || + serviceKey === "discord" || + serviceKey === "enmity" + ) { + results[serviceKey] = result; + if (!nocache) { + const userCacheKey = `user_badges:${serviceKey}:${userId}`; + await redis.set(userCacheKey, JSON.stringify(result)); + await redis.expire(userCacheKey, Math.min(redisTtl, 900)); + } + } + } catch (error) { + echo.warn({ + message: `Failed to fetch badges for service ${serviceKey}`, + error: error instanceof Error ? error.message : String(error), + userId, + }); + } + }), + ); + + if (separated) return results; + + const combined: Badge[] = []; + for (const group of Object.values(results)) { + combined.push(...group); + } + return combined; +} diff --git a/src/helpers/char.ts b/src/lib/char.ts similarity index 50% rename from src/helpers/char.ts rename to src/lib/char.ts index 74868aa..f6038db 100644 --- a/src/helpers/char.ts +++ b/src/lib/char.ts @@ -1,12 +1,14 @@ -export function validateID(id: string): boolean { +function validateID(id: string | undefined): boolean { if (!id) return false; return /^\d{17,20}$/.test(id.trim()); } -export function parseServices(input: string): string[] { +function parseServices(input: string): string[] { return input .split(/[\s,]+/) .map((s) => s.trim()) .filter(Boolean); } + +export { validateID, parseServices }; diff --git a/src/routes/[id].ts b/src/routes/[id].ts index 4b8421e..1f291cf 100644 --- a/src/routes/[id].ts +++ b/src/routes/[id].ts @@ -1,6 +1,6 @@ -import { badgeServices } from "@config/environment"; -import { fetchBadges } from "@helpers/badges"; -import { parseServices, validateID } from "@helpers/char"; +import { badgeServices } from "@config"; +import { fetchBadges } from "@lib/badges"; +import { parseServices, validateID } from "@lib/char"; function isValidServices(services: string[]): boolean { if (!Array.isArray(services)) return false; @@ -18,47 +18,52 @@ const routeDef: RouteDef = { async function handler(request: ExtendedRequest): Promise { const { id: userId } = request.params; - const { services, cache, seperated } = request.query; - - let validServices: string[]; + const { services, cache = "true", seperated = "false" } = request.query; if (!validateID(userId)) { return Response.json( { status: 400, - error: "Invalid Discord User ID", - }, - { - status: 400, + error: "Invalid Discord User ID. Must be 17-20 digits.", }, + { status: 400 }, ); } + let validServices: string[]; + const availableServices = badgeServices.map((b) => b.service); + if (services) { const parsed = parseServices(services); - - if (parsed.length > 0) { - if (!isValidServices(parsed)) { - return Response.json( - { - status: 400, - error: "Invalid Services", - }, - { - status: 400, - }, - ); - } - - validServices = parsed; - } else { - validServices = badgeServices.map((b) => b.service); + if (parsed.length === 0) { + return Response.json( + { + status: 400, + error: "No valid services provided", + availableServices, + }, + { status: 400 }, + ); } + + if (!isValidServices(parsed)) { + return Response.json( + { + status: 400, + error: "Invalid service(s) provided", + availableServices, + provided: parsed, + }, + { status: 400 }, + ); + } + + validServices = parsed; } else { - validServices = badgeServices.map((b) => b.service); + validServices = availableServices; } - const badges: BadgeResult = await fetchBadges( + const badges = await fetchBadges( userId, validServices, { @@ -68,27 +73,18 @@ async function handler(request: ExtendedRequest): Promise { request, ); - if (badges instanceof Error) { - return Response.json( - { - status: 500, - error: badges.message, - }, - { - status: 500, - }, - ); - } + const isEmpty = Array.isArray(badges) + ? badges.length === 0 + : Object.keys(badges).length === 0; - if (badges.length === 0) { + if (isEmpty) { return Response.json( { status: 404, - error: "No Badges Found", - }, - { - status: 404, + error: "No badges found for this user", + services: validServices, }, + { status: 404 }, ); } @@ -105,9 +101,6 @@ async function handler(request: ExtendedRequest): Promise { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, OPTIONS", "Access-Control-Allow-Headers": "Content-Type", - "Access-Control-Max-Age": "86400", - "Access-Control-Allow-Credentials": "true", - "Access-Control-Expose-Headers": "Content-Type", }, }, ); diff --git a/src/routes/health.ts b/src/routes/health.ts new file mode 100644 index 0000000..12c0dc9 --- /dev/null +++ b/src/routes/health.ts @@ -0,0 +1,89 @@ +import { redis } from "bun"; + +const routeDef: RouteDef = { + method: "GET", + accepts: "*/*", + returns: "application/json", +}; + +async function handler(): Promise { + const health: HealthResponse = { + status: "ok", + timestamp: new Date().toISOString(), + uptime: process.uptime(), + services: { + redis: "unknown", + }, + cache: { + lastFetched: {}, + nextUpdate: null, + }, + }; + + try { + await redis.connect(); + health.services.redis = "ok"; + } catch { + health.services.redis = "error"; + health.status = "degraded"; + } + + if (health.services.redis === "ok") { + const services = ["vencord", "equicord", "nekocord", "reviewdb"]; + const timestampPrefix = "badge_cache_timestamp:"; + + try { + const timestamps = await Promise.all( + services.map(async (service) => { + const timestamp = await redis.get(`${timestampPrefix}${service}`); + return { + service, + timestamp: timestamp ? Number.parseInt(timestamp, 10) : null, + }; + }), + ); + + const lastFetched: Record = {}; + let oldestTimestamp: number | null = null; + + for (const { service, timestamp } of timestamps) { + if (timestamp) { + const date = new Date(timestamp); + lastFetched[service] = { + timestamp: date.toISOString(), + age: `${Math.floor((Date.now() - timestamp) / 1000)}s ago`, + }; + + if (!oldestTimestamp || timestamp < oldestTimestamp) { + oldestTimestamp = timestamp; + } + } else { + lastFetched[service] = { + timestamp: null, + age: "never", + }; + } + } + + health.cache.lastFetched = lastFetched; + + if (oldestTimestamp) { + const nextUpdate = new Date(oldestTimestamp + 60 * 60 * 1000); + health.cache.nextUpdate = nextUpdate.toISOString(); + } + } catch { + health.cache.lastFetched = { error: "Failed to fetch cache timestamps" }; + } + } + + const status = health.status === "ok" ? 200 : 503; + + return Response.json(health, { + status, + headers: { + "Cache-Control": "no-cache", + }, + }); +} + +export { handler, routeDef }; diff --git a/src/routes/index.ts b/src/routes/index.ts index 76e5c37..bbfacb5 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -1,3 +1,5 @@ +import { badgeServices, getServiceDescription, gitUrl } from "@config"; + const routeDef: RouteDef = { method: "GET", accepts: "*/*", @@ -8,15 +10,56 @@ async function handler(request: ExtendedRequest): Promise { const endPerf: number = Date.now(); const perf: number = endPerf - request.startPerf; - const { query, params } = request; - - const response: Record = { - perf, - query, - params, + const response = { + name: "Badge Aggregator API", + description: + "A fast Discord badge aggregation API built with Bun and Redis caching", + version: "1.0.0", + author: "creations.works", + repository: gitUrl, + performance: { + responseTime: `${perf}ms`, + uptime: `${process.uptime()}s`, + }, + routes: { + "GET /": "API information and available routes", + "GET /:userId": "Get badges for a Discord user", + "GET /health": "Health check endpoint", + }, + endpoints: { + badges: { + path: "/:userId", + method: "GET", + description: "Fetch badges for a Discord user", + parameters: { + path: { + userId: "Discord User ID (17-20 digits)", + }, + query: { + services: "Comma/space separated list of services (optional)", + cache: "Enable/disable caching (true/false, default: true)", + seperated: + "Return results grouped by service (true/false, default: false)", + }, + }, + example: "/:userId?services=discord,vencord&seperated=true&cache=true", + }, + }, + supportedServices: badgeServices.map((service) => ({ + name: service.service, + description: getServiceDescription(service.service), + })), + ratelimit: { + window: "60 seconds", + requests: 60, + }, }; - return Response.json(response); + return Response.json(response, { + headers: { + "Cache-Control": "public, max-age=300", + }, + }); } export { handler, routeDef }; diff --git a/src/server.ts b/src/server.ts index e851a58..d24b025 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,14 +1,14 @@ import { resolve } from "node:path"; -import { echo } from "@atums/echo"; -import { environment } from "@config/environment"; +import { Echo, echo } from "@atums/echo"; +import { environment } from "@config"; import { type BunFile, FileSystemRouter, type MatchedRoute, - type Serve, + type Server, } from "bun"; -import { webSocketHandler } from "@/websocket"; +import { webSocketHandler } from "@websocket"; class ServerHandler { private router: FileSystemRouter; @@ -19,14 +19,14 @@ class ServerHandler { ) { this.router = new FileSystemRouter({ style: "nextjs", - dir: "./src/routes", + dir: resolve("src", "routes"), fileExtensions: [".ts"], origin: `http://${this.host}:${this.port}`, }); } public initialize(): void { - const server: Serve = Bun.serve({ + const server: Server = Bun.serve({ port: this.port, hostname: this.host, fetch: this.handleRequest.bind(this), @@ -37,19 +37,15 @@ class ServerHandler { }, }); - const accessUrls: string[] = [ - `http://${server.hostname}:${server.port}`, - `http://localhost:${server.port}`, - `http://127.0.0.1:${server.port}`, - ]; + const echoChild = new Echo({ disableFile: true }); - echo.info(`Server running at ${accessUrls[0]}`); - echo.info(`Access via: ${accessUrls[1]} or ${accessUrls[2]}`); - - this.logRoutes(); + echoChild.info( + `Server running at http://${server.hostname}:${server.port}`, + ); + this.logRoutes(echoChild); } - private logRoutes(): void { + private logRoutes(echo: Echo): void { echo.info("Available routes:"); const sortedRoutes: [string, string][] = Object.entries( @@ -82,7 +78,7 @@ class ServerHandler { if (await file.exists()) { const fileContent: ArrayBuffer = await file.arrayBuffer(); - const contentType: string = file.type || "application/octet-stream"; + const contentType: string = file.type ?? "application/octet-stream"; response = new Response(fileContent, { headers: { "Content-Type": contentType }, @@ -129,7 +125,7 @@ class ServerHandler { private async handleRequest( request: Request, - server: BunServer, + server: Server, ): Promise { const extendedRequest: ExtendedRequest = request as ExtendedRequest; extendedRequest.startPerf = performance.now(); @@ -142,23 +138,25 @@ class ServerHandler { ip = headers.get("CF-Connecting-IP")?.trim() || headers.get("X-Real-IP")?.trim() || - headers.get("X-Forwarded-For")?.split(",")[0].trim() || + headers.get("X-Forwarded-For")?.split(",")[0]?.trim() || "unknown"; } const pathname: string = new URL(request.url).pathname; - const baseDir = resolve("public/custom"); + const baseDir = resolve("public", "custom"); const customPath = resolve(baseDir, pathname.slice(1)); if (!customPath.startsWith(baseDir)) { - return new Response("Forbidden", { status: 403 }); + response = new Response("Forbidden", { status: 403 }); + this.logRequest(extendedRequest, response, ip); + return response; } const customFile = Bun.file(customPath); if (await customFile.exists()) { const content = await customFile.arrayBuffer(); - const type = customFile.type || "application/octet-stream"; + const type: string = customFile.type ?? "application/octet-stream"; response = new Response(content, { headers: { "Content-Type": type }, }); @@ -180,7 +178,7 @@ class ServerHandler { const routeModule: RouteModule = await import(filePath); const contentType: string | null = request.headers.get("Content-Type"); const actualContentType: string | null = contentType - ? contentType.split(";")[0].trim() + ? (contentType.split(";")[0]?.trim() ?? null) : null; if ( diff --git a/src/websocket.ts b/src/websocket.ts index 1877f6b..6be34fd 100644 --- a/src/websocket.ts +++ b/src/websocket.ts @@ -26,7 +26,7 @@ class WebSocketHandler { } } - public handleClose(ws: ServerWebSocket, code: number, reason: string): void { + public handleClose(_ws: ServerWebSocket, code: number, reason: string): void { echo.info(`WebSocket closed with code ${code}, reason: ${reason}`); } } diff --git a/tsconfig.json b/tsconfig.json index 68a5a97..ec4e48d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,32 +2,30 @@ "compilerOptions": { "baseUrl": "./", "paths": { - "@/*": ["src/*"], + "@*": ["src/*"], + "@config": ["config/index.ts"], "@config/*": ["config/*"], "@types/*": ["types/*"], - "@helpers/*": ["src/helpers/*"] + "@lib/*": ["src/lib/*"] }, - "typeRoots": ["./src/types", "./node_modules/@types"], - // Enable latest features + "typeRoots": ["./types", "./node_modules/@types"], "lib": ["ESNext", "DOM"], "target": "ESNext", "module": "ESNext", "moduleDetection": "force", - "jsx": "react-jsx", - "allowJs": true, - // Bundler mode + "allowJs": false, "moduleResolution": "bundler", - "allowImportingTsExtensions": true, + "allowImportingTsExtensions": false, "verbatimModuleSyntax": true, "noEmit": true, - // Best practices "strict": true, "skipLibCheck": true, "noFallthroughCasesInSwitch": true, - // Some stricter flags (disabled by default) - "noUnusedLocals": false, - "noUnusedParameters": false, + "noUnusedLocals": true, + "noUnusedParameters": true, + "exactOptionalPropertyTypes": true, + "noUncheckedIndexedAccess": true, "noPropertyAccessFromIndexSignature": false }, - "include": ["src", "types", "config"] + "include": ["src", "types"] } diff --git a/types/badge.d.ts b/types/badge.d.ts index 0731370..7d17c08 100644 --- a/types/badge.d.ts +++ b/types/badge.d.ts @@ -20,3 +20,70 @@ type badgeURLMap = { badge: (id: string) => string; }); }; + +interface VencordEquicordData { + [userId: string]: Array<{ + tooltip: string; + badge: string; + }>; +} + +interface NekocordData { + users: { + [userId: string]: { + badges: string[]; + }; + }; + badges: { + [badgeId: string]: { + name: string; + image: string; + }; + }; +} + +interface ReviewDbData + extends Array<{ + discordID: string; + name: string; + icon: string; + }> {} + +type BadgeServiceData = VencordEquicordData | NekocordData | ReviewDbData; + +interface BadgeService { + service: string; + url: + | string + | (( + userId: string, + ) => string | { user: string; badge: (id: string) => string }); +} + +interface VencordBadgeItem { + tooltip: string; + badge: string; +} + +interface NekocordBadgeInfo { + name: string; + image: string; +} + +interface ReviewDbBadgeItem { + discordID: string; + name: string; + icon: string; +} + +interface EnmityBadgeItem { + name: string; + url: { + dark: string; + }; +} + +interface DiscordUserData { + avatar: string; + flags: number; +} diff --git a/types/bun.d.ts b/types/bun.d.ts index 018bf35..9afe286 100644 --- a/types/bun.d.ts +++ b/types/bun.d.ts @@ -1,14 +1,8 @@ -import type { Server } from "bun"; - type Query = Record; type Params = Record; -declare global { - type BunServer = Server; - - interface ExtendedRequest extends Request { - startPerf: number; - query: Query; - params: Params; - } +interface ExtendedRequest extends Request { + startPerf: number; + query: Query; + params: Params; } diff --git a/types/health.d.ts b/types/health.d.ts new file mode 100644 index 0000000..786acc1 --- /dev/null +++ b/types/health.d.ts @@ -0,0 +1,17 @@ +interface CacheInfo { + timestamp: string | null; + age: string; +} + +interface HealthResponse { + status: "ok" | "degraded"; + timestamp: string; + uptime: number; + services: { + redis: "ok" | "error" | "unknown"; + }; + cache: { + lastFetched: Record | { error: string }; + nextUpdate: string | null; + }; +} diff --git a/types/logger.d.ts b/types/logger.d.ts deleted file mode 100644 index ff6a601..0000000 --- a/types/logger.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -type ILogMessagePart = { value: string; color: string }; - -type ILogMessageParts = { - level: ILogMessagePart; - filename: ILogMessagePart; - readableTimestamp: ILogMessagePart; - message: ILogMessagePart; - [key: string]: ILogMessagePart; -}; From d300f20b4911d9386f87b1c6ca9c1135b294d019 Mon Sep 17 00:00:00 2001 From: creations Date: Fri, 6 Jun 2025 00:39:36 +0200 Subject: [PATCH 17/21] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a4783ec..d6c2938 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ A fast Discord badge aggregation API built with [Bun](https://bun.sh) and Redis caching. # Preview -https://badges.creations.works +https://badges.atums.world ## Features From 269b858e8848b5f8d14cc355096ef0313e4d0911 Mon Sep 17 00:00:00 2001 From: creations Date: Thu, 5 Jun 2025 19:49:23 -0400 Subject: [PATCH 18/21] add vencord and equicord contributor to fetching --- config/constants.ts | 6 +++- src/lib/badgeCache.ts | 74 ++++++++++++++++++++++++++++++++++++++++++- types/badge.d.ts | 32 +++++++++++++------ 3 files changed, 100 insertions(+), 12 deletions(-) diff --git a/config/constants.ts b/config/constants.ts index 37a8a5a..ac3a5c6 100644 --- a/config/constants.ts +++ b/config/constants.ts @@ -87,7 +87,7 @@ const discordBadgeDetails = { }, }; -const badgeServices: badgeURLMap[] = [ +const badgeServices: BadgeService[] = [ { service: "Vencord", url: "https://badges.vencord.dev/badges.json", @@ -118,6 +118,9 @@ const badgeServices: badgeURLMap[] = [ }, ]; +const vencordEquicordContributorUrl = + "https://raw.githubusercontent.com/Equicord/Equibored/refs/heads/main/plugins.json"; + function getServiceDescription(service: string): string { const descriptions: Record = { Vencord: "Custom badges from Vencord Discord client", @@ -137,6 +140,7 @@ export { badgeServices, discordBadges, discordBadgeDetails, + vencordEquicordContributorUrl, getServiceDescription, gitUrl, }; diff --git a/src/lib/badgeCache.ts b/src/lib/badgeCache.ts index 682f6c9..8711187 100644 --- a/src/lib/badgeCache.ts +++ b/src/lib/badgeCache.ts @@ -1,5 +1,11 @@ import { echo } from "@atums/echo"; -import { badgeFetchInterval, badgeServices, gitUrl, redisTtl } from "@config"; +import { + badgeFetchInterval, + badgeServices, + gitUrl, + redisTtl, + vencordEquicordContributorUrl, +} from "@config"; import { redis } from "bun"; class BadgeCacheManager { @@ -110,6 +116,72 @@ class BadgeCacheManager { data = (await res.json()) as VencordEquicordData; } } + + if (typeof vencordEquicordContributorUrl === "string") { + const contributorRes = await fetch(vencordEquicordContributorUrl, { + headers: { + "User-Agent": `BadgeAPI/1.0 ${gitUrl}`, + }, + }); + + if (contributorRes.ok) { + const pluginData = await contributorRes.json(); + + if (Array.isArray(pluginData)) { + if (!data) { + data = {} as VencordEquicordData; + } + + const contributors = new Set(); + + for (const plugin of pluginData) { + if (plugin.authors && Array.isArray(plugin.authors)) { + const isEquicordPlugin = + plugin.filePath && + typeof plugin.filePath === "string" && + plugin.filePath.includes("equicordplugins/"); + + const shouldInclude = + (serviceKey === "equicord" && isEquicordPlugin) || + (serviceKey === "vencord" && !isEquicordPlugin); + + if (shouldInclude) { + for (const author of plugin.authors) { + if (author.id) { + contributors.add(author.id); + } + } + } + } + } + + const badgeDetails = + serviceKey === "vencord" + ? { + tooltip: "Vencord Contributor", + badge: "https://vencord.dev/assets/favicon.png", + } + : { + tooltip: "Equicord Contributor", + badge: "https://i.imgur.com/57ATLZu.png", + }; + + for (const authorId of contributors) { + if (!data[authorId]) { + data[authorId] = []; + } + + const hasContributorBadge = data[authorId].some( + (badge) => badge.tooltip === badgeDetails.tooltip, + ); + + if (!hasContributorBadge) { + data[authorId].push(badgeDetails); + } + } + } + } + } break; } diff --git a/types/badge.d.ts b/types/badge.d.ts index 7d17c08..08043b9 100644 --- a/types/badge.d.ts +++ b/types/badge.d.ts @@ -10,7 +10,7 @@ interface FetchBadgesOptions { separated?: boolean; } -type badgeURLMap = { +type BadgeService = { service: string; url: | string @@ -51,15 +51,6 @@ interface ReviewDbData type BadgeServiceData = VencordEquicordData | NekocordData | ReviewDbData; -interface BadgeService { - service: string; - url: - | string - | (( - userId: string, - ) => string | { user: string; badge: (id: string) => string }); -} - interface VencordBadgeItem { tooltip: string; badge: string; @@ -87,3 +78,24 @@ interface DiscordUserData { avatar: string; flags: number; } + +interface PluginData { + hasPatches: boolean; + hasCommands: boolean; + enabledByDefault: boolean; + required: boolean; + tags: string[]; + name: string; + description: string; + authors: Array<{ + name: string; + id: string; + }>; + filePath: string; + commands?: Array<{ + name: string; + description: string; + }>; + dependencies?: string[]; + target?: string; +} From 2f9b38ace8921c3cb243b4d67736714759212639 Mon Sep 17 00:00:00 2001 From: creations Date: Sat, 7 Jun 2025 21:08:10 -0400 Subject: [PATCH 19/21] fix required bot token, fix equicord and vencord contrib badges, move equicord to svg --- config/constants.ts | 24 +++++++++++++--- config/index.ts | 2 +- public/badges/equicord.svg | 5 ++++ public/badges/vencord.png | Bin 0 -> 900 bytes src/lib/badgeCache.ts | 33 ++++++++-------------- src/lib/badges.ts | 56 ++++++++++++++++++++++--------------- types/badge.d.ts | 1 + 7 files changed, 72 insertions(+), 49 deletions(-) create mode 100644 public/badges/equicord.svg create mode 100644 public/badges/vencord.png diff --git a/config/constants.ts b/config/constants.ts index ac3a5c6..b6ec990 100644 --- a/config/constants.ts +++ b/config/constants.ts @@ -85,16 +85,36 @@ const discordBadgeDetails = { tooltip: "Uses AutoMod", icon: "/public/badges/discord/USES_AUTOMOD.svg", }, + + // Custom + + VENCORD_CONTRIBUTOR: { + tooltip: "Vencord Contributor", + icon: "/public/badges/vencord.png", + }, + EQUICORD_CONTRIBUTOR: { + tooltip: "Equicord Contributor", + icon: "/public/badges/equicord.svg", + }, + + DISCORD_NITRO: { + tooltip: "Discord Nitro", + icon: "/public/badges/discord/NITRO.svg", + }, }; const badgeServices: BadgeService[] = [ { service: "Vencord", url: "https://badges.vencord.dev/badges.json", + pluginsUrl: + "https://raw.githubusercontent.com/Vencord/builds/main/plugins.json", }, { service: "Equicord", // Ekwekord ! WOOP url: "https://raw.githubusercontent.com/Equicord/Equibored/refs/heads/main/badges.json", + pluginsUrl: + "https://raw.githubusercontent.com/Equicord/Equibored/refs/heads/main/plugins.json", }, { service: "Nekocord", @@ -118,9 +138,6 @@ const badgeServices: BadgeService[] = [ }, ]; -const vencordEquicordContributorUrl = - "https://raw.githubusercontent.com/Equicord/Equibored/refs/heads/main/plugins.json"; - function getServiceDescription(service: string): string { const descriptions: Record = { Vencord: "Custom badges from Vencord Discord client", @@ -140,7 +157,6 @@ export { badgeServices, discordBadges, discordBadgeDetails, - vencordEquicordContributorUrl, getServiceDescription, gitUrl, }; diff --git a/config/index.ts b/config/index.ts index 5f7ad84..cd99868 100644 --- a/config/index.ts +++ b/config/index.ts @@ -18,7 +18,7 @@ const badgeFetchInterval: number = process.env.BADGE_FETCH_INTERVAL const botToken: string | undefined = process.env.DISCORD_TOKEN; function verifyRequiredVariables(): void { - const requiredVariables = ["HOST", "PORT", "DISCORD_TOKEN"]; + const requiredVariables = ["HOST", "PORT"]; let hasError = false; diff --git a/public/badges/equicord.svg b/public/badges/equicord.svg new file mode 100644 index 0000000..d60e5ce --- /dev/null +++ b/public/badges/equicord.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/badges/vencord.png b/public/badges/vencord.png new file mode 100644 index 0000000000000000000000000000000000000000..7f4300462f3bb2efd87a5337f08aaa0a9e48e563 GIT binary patch literal 900 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD3?#3*wSy#B(j9#r85lP9bN@+X1@aY=J%W50 z7^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+10qA0*8#1%*b!TXAXZkd-%)w!(T2O{(AB7_Zx?Q+&=vE^5Jh+4u89P_}lfv-)|oNbmH)*yCb1o#_R%lCdPnFPOpM*^M+1C&}C0g(;1@CI!ghEbxddW?lFz$+-9S#>;I@3IYrX?9sXRzwf>GUo?M- zim_B<*F)QL{g<3SDfU>stSr3}czi!shtuM}r}aNG_ploreite7k|mS<{D%7A10{c+ zr5{k3XU=_~_|HSh7wcOE6_z)@i;-l!IPIurReQr4#`kHb8$4D$V%&IPTSLKmX8*+uv*@*D zyDz^iYZYd>=;rU3!uad&!2}DL-lhbbcYBr04Q8(3G;c6*()n1jN-c&_L-6_2oMmbv z^^9wn=X2H_sM!~xGkx{dDp{c?P0VXcw(>0zPOQo^o3S8x#msfr8dVf874JUE!n)=1 zRVxQh# { const serviceKey = service.toLowerCase(); + + if (!USER_CACHE_SERVICES.includes(serviceKey) || nocache) { + return false; + } + const userCacheKey = `user_badges:${serviceKey}:${userId}`; - if (!nocache) { - try { - const cached = await redis.get(userCacheKey); - if (cached) { - const parsed: Badge[] = JSON.parse(cached); - results[serviceKey] = parsed; - return true; - } - } catch {} - } + try { + const cached = await redis.get(userCacheKey); + if (cached) { + const parsed: Badge[] = JSON.parse(cached); + results[serviceKey] = parsed; + return true; + } + } catch {} + return false; }); const cacheHits = await Promise.all(userCachePromises); - const servicesToFetch = services.filter((_, index) => !cacheHits[index]); await Promise.all( @@ -68,10 +73,16 @@ export async function fetchBadges( const userBadges = serviceData[userId]; if (Array.isArray(userBadges)) { + const origin = request ? getRequestOrigin(request) : ""; + for (const badgeItem of userBadges) { + const badgeUrl = badgeItem.badge.startsWith("/") + ? `${origin}${badgeItem.badge}` + : badgeItem.badge; + result.push({ tooltip: badgeItem.tooltip, - badge: badgeItem.badge, + badge: badgeUrl, }); } } @@ -199,8 +210,8 @@ export async function fetchBadges( if (data.avatar?.startsWith("a_")) { result.push({ - tooltip: "Discord Nitro", - badge: `${origin}/public/badges/discord/NITRO.svg`, + tooltip: discordBadgeDetails.DISCORD_NITRO.tooltip, + badge: `${origin}${discordBadgeDetails.DISCORD_NITRO.icon}`, }); } @@ -228,17 +239,16 @@ export async function fetchBadges( break; } + results[serviceKey] = result; + if ( - result.length > 0 || - serviceKey === "discord" || - serviceKey === "enmity" + USER_CACHE_SERVICES.includes(serviceKey) && + !nocache && + result.length > 0 ) { - results[serviceKey] = result; - if (!nocache) { - const userCacheKey = `user_badges:${serviceKey}:${userId}`; - await redis.set(userCacheKey, JSON.stringify(result)); - await redis.expire(userCacheKey, Math.min(redisTtl, 900)); - } + const userCacheKey = `user_badges:${serviceKey}:${userId}`; + await redis.set(userCacheKey, JSON.stringify(result)); + await redis.expire(userCacheKey, Math.min(redisTtl, 900)); } } catch (error) { echo.warn({ diff --git a/types/badge.d.ts b/types/badge.d.ts index 08043b9..5f235a7 100644 --- a/types/badge.d.ts +++ b/types/badge.d.ts @@ -19,6 +19,7 @@ type BadgeService = { user: string; badge: (id: string) => string; }); + pluginsUrl?: string; }; interface VencordEquicordData { From 3aca639660e670cb5addd89599c814c0e2c5630b Mon Sep 17 00:00:00 2001 From: creations Date: Mon, 9 Jun 2025 18:12:48 -0400 Subject: [PATCH 20/21] add compose logic --- .dockerignore | 14 ++++++++++++++ .gitignore | 2 +- Dockerfile | 30 ++++++++++++++++++++++++++++++ bun.lock | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ compose.yml | 32 ++++++++++++++++++++++++++++++++ 5 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 bun.lock create mode 100644 compose.yml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..ef2db44 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,14 @@ +node_modules +.env +.env.example +logs +dist +.vscode +.git +.gitignore +Dockerfile +.dockerignore +README.md +LICENSE +.forgejo +.editorconfig diff --git a/.gitignore b/.gitignore index c7c8dc9..3290e48 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ /node_modules -bun.lock .env .vscode/settings.json logs +dragonfly-data diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..9aeb334 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,30 @@ +FROM oven/bun:latest AS base +WORKDIR /usr/src/app + +FROM base AS install +RUN mkdir -p /temp/dev +COPY package.json bun.lock /temp/dev/ +RUN cd /temp/dev && bun install --frozen-lockfile + +RUN mkdir -p /temp/prod +COPY package.json bun.lock /temp/prod/ +RUN cd /temp/prod && bun install --production + +FROM base AS prerelease +COPY --from=install /temp/dev/node_modules node_modules +COPY . . + +FROM base AS release +COPY --from=install /temp/prod/node_modules node_modules +COPY --from=prerelease /usr/src/app/src ./src +COPY --from=prerelease /usr/src/app/public ./public +COPY --from=prerelease /usr/src/app/package.json . +COPY --from=prerelease /usr/src/app/tsconfig.json . +COPY --from=prerelease /usr/src/app/config ./config +COPY --from=prerelease /usr/src/app/types ./types +COPY --from=prerelease /usr/src/app/logger.json . + +RUN mkdir -p /usr/src/app/logs && chown bun:bun /usr/src/app/logs + +USER bun +ENTRYPOINT [ "bun", "run", "start" ] diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..a04a9ab --- /dev/null +++ b/bun.lock @@ -0,0 +1,48 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "bun_frontend_template", + "dependencies": { + "@atums/echo": "latest", + }, + "devDependencies": { + "@biomejs/biome": "^1.9.4", + "@types/bun": "latest", + }, + }, + }, + "packages": { + "@atums/echo": ["@atums/echo@1.0.3", "", { "dependencies": { "date-fns-tz": "^3.2.0" } }, "sha512-WQ2d4oWTaE+6VeLIu2FepmZipdwUrM+SiiO5moHhSsP4P+MaQCjq5qp34nwB/vOHv2jd9UcBzy27iUziTffCjg=="], + + "@biomejs/biome": ["@biomejs/biome@1.9.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "1.9.4", "@biomejs/cli-darwin-x64": "1.9.4", "@biomejs/cli-linux-arm64": "1.9.4", "@biomejs/cli-linux-arm64-musl": "1.9.4", "@biomejs/cli-linux-x64": "1.9.4", "@biomejs/cli-linux-x64-musl": "1.9.4", "@biomejs/cli-win32-arm64": "1.9.4", "@biomejs/cli-win32-x64": "1.9.4" }, "bin": { "biome": "bin/biome" } }, "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog=="], + + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@1.9.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw=="], + + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@1.9.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg=="], + + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g=="], + + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA=="], + + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg=="], + + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg=="], + + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@1.9.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg=="], + + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@1.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA=="], + + "@types/bun": ["@types/bun@1.2.15", "", { "dependencies": { "bun-types": "1.2.15" } }, "sha512-U1ljPdBEphF0nw1MIk0hI7kPg7dFdPyM7EenHsp6W5loNHl7zqy6JQf/RKCgnUn2KDzUpkBwHPnEJEjII594bA=="], + + "@types/node": ["@types/node@22.14.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw=="], + + "bun-types": ["bun-types@1.2.15", "", { "dependencies": { "@types/node": "*" } }, "sha512-NarRIaS+iOaQU1JPfyKhZm4AsUOrwUOqRNHY0XxI8GI8jYxiLXLcdjYMG9UKS+fwWasc1uw1htV9AX24dD+p4w=="], + + "date-fns": ["date-fns@4.1.0", "", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="], + + "date-fns-tz": ["date-fns-tz@3.2.0", "", { "peerDependencies": { "date-fns": "^3.0.0 || ^4.0.0" } }, "sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ=="], + + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + } +} diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..f8cf88b --- /dev/null +++ b/compose.yml @@ -0,0 +1,32 @@ +services: + badge-api: + container_name: badge-api + pull_policy: build + build: + context: . + restart: unless-stopped + ports: + - "${PORT:-8080}:${PORT:-8080}" + env_file: + - .env + environment: + - REDIS_URL=redis://dragonfly:6379 + depends_on: + dragonfly: + condition: service_started + networks: + - badge-api-network + + dragonfly: + image: docker.dragonflydb.io/dragonflydb/dragonfly + restart: unless-stopped + ulimits: + memlock: -1 + volumes: + - ./dragonfly-data:/data + networks: + - badge-api-network + +networks: + badge-api-network: + driver: bridge From 868c5b6dbe393f7e063bfaeb5b71d0428e0a1c89 Mon Sep 17 00:00:00 2001 From: creations Date: Mon, 9 Jun 2025 18:31:00 -0400 Subject: [PATCH 21/21] dont force env or compose --- compose.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/compose.yml b/compose.yml index f8cf88b..4e38729 100644 --- a/compose.yml +++ b/compose.yml @@ -6,9 +6,7 @@ services: context: . restart: unless-stopped ports: - - "${PORT:-8080}:${PORT:-8080}" - env_file: - - .env + - "8080:8080" environment: - REDIS_URL=redis://dragonfly:6379 depends_on: