Compare commits

..

No commits in common. "main" and "dev" have entirely different histories.
main ... dev

10 changed files with 272 additions and 77 deletions

41
LICENSE
View file

@ -1,28 +1,21 @@
BSD 3-Clause License MIT License
Copyright (c) 2025, creations.works Copyright (c) 2025 [fullname]
Redistribution and use in source and binary forms, with or without Permission is hereby granted, free of charge, to any person obtaining a copy
modification, are permitted provided that the following conditions are met: 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:
1. Redistributions of source code must retain the above copyright notice, this The above copyright notice and this permission notice shall be included in all
list of conditions and the following disclaimer. copies or substantial portions of the Software.
2. Redistributions in binary form must reproduce the above copyright notice, THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
this list of conditions and the following disclaimer in the documentation IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
and/or other materials provided with the distribution. FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
3. Neither the name of the copyright holder nor the names of its LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
contributors may be used to endorse or promote products derived from OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
this software without specific prior written permission. SOFTWARE.
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.

View file

@ -2,9 +2,6 @@
A fast Discord badge aggregation API built with [Bun](https://bun.sh) and Redis caching. A fast Discord badge aggregation API built with [Bun](https://bun.sh) and Redis caching.
# Preview
https://badges.creations.works
## Features ## Features
- Aggregates custom badge data from multiple sources (e.g. Vencord, Nekocord, Equicord, etc.) - Aggregates custom badge data from multiple sources (e.g. Vencord, Nekocord, Equicord, etc.)
@ -60,8 +57,8 @@ GET /:userId
### Query Parameters ### Query Parameters
| Name | Description | | Name | Description |
|--------------|---------------------------------------------------------------------------------------------------| |--------------|--------------------------------------------------------------------------|
| `services` | A comma or space separated list of services to fetch badges from, if this is empty it fetches all | | `services` | A comma or space separated list of services to fetch badges from |
| `cache` | Set to `true` or `false` (default: `true`). `false` bypasses Redis | | `cache` | Set to `true` or `false` (default: `true`). `false` bypasses Redis |
| `seperated` | Set to `true` to return results grouped by service, else merged array | | `seperated` | Set to `true` to return results grouped by service, else merged array |
@ -71,8 +68,6 @@ GET /:userId
- Equicord - Equicord
- Nekocord - Nekocord
- ReviewDb - ReviewDb
- Enmity
- Discord ( some )
### Example ### Example
@ -80,12 +75,20 @@ GET /:userId
GET /209830981060788225?seperated=true&cache=true&services=equicord 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 ## Start the Server
```bash ```bash
bun i
bun run start bun run start
``` ```
## License ## License
[BSD 3](LICENSE) [MIT](LICENSE)

View file

@ -1,26 +1,25 @@
export const discordBadges = { export const discordBadges = {
// User badges // User badges
STAFF: 1 << 0, HYPESQUAD: 2 << 2,
PARTNER: 1 << 1, HYPESQUAD_ONLINE_HOUSE_1: 2 << 6,
HYPESQUAD: 1 << 2, HYPESQUAD_ONLINE_HOUSE_2: 2 << 7,
BUG_HUNTER_LEVEL_1: 1 << 3, HYPESQUAD_ONLINE_HOUSE_3: 2 << 8,
HYPESQUAD_ONLINE_HOUSE_1: 1 << 6,
HYPESQUAD_ONLINE_HOUSE_2: 1 << 7, STAFF: 2 << 0,
HYPESQUAD_ONLINE_HOUSE_3: 1 << 8, PARTNER: 2 << 1,
PREMIUM_EARLY_SUPPORTER: 1 << 9, CERTIFIED_MODERATOR: 2 << 18,
TEAM_USER: 1 << 10,
SYSTEM: 1 << 12, VERIFIED_DEVELOPER: 2 << 17,
BUG_HUNTER_LEVEL_2: 1 << 14, ACTIVE_DEVELOPER: 2 << 22,
VERIFIED_DEVELOPER: 1 << 17,
CERTIFIED_MODERATOR: 1 << 18, PREMIUM_EARLY_SUPPORTER: 2 << 9,
SPAMMER: 1 << 20,
ACTIVE_DEVELOPER: 1 << 22, BUG_HUNTER_LEVEL_1: 2 << 3,
BUG_HUNTER_LEVEL_2: 2 << 14,
// Bot badges // Bot badges
VERIFIED_BOT: 1 << 16, SUPPORTS_COMMANDS: 2 << 23,
BOT_HTTP_INTERACTIONS: 1 << 19, USES_AUTOMOD: 2 << 24,
SUPPORTS_COMMANDS: 1 << 23,
USES_AUTOMOD: 1 << 24,
}; };
export const discordBadgeDetails = { export const discordBadgeDetails = {

View file

@ -7,7 +7,7 @@
"dev": "bun run --hot src/index.ts --dev", "dev": "bun run --hot src/index.ts --dev",
"lint": "bunx biome check", "lint": "bunx biome check",
"lint:fix": "bunx biome check --fix", "lint:fix": "bunx biome check --fix",
"cleanup": "rm -rf logs node_modules bun.lock" "cleanup": "rm -rf logs node_modules bun.lockdb"
}, },
"devDependencies": { "devDependencies": {
"@types/bun": "^1.2.9", "@types/bun": "^1.2.9",
@ -19,7 +19,6 @@
"typescript": "^5.8.3" "typescript": "^5.8.3"
}, },
"dependencies": { "dependencies": {
"@creations.works/logger": "^1.0.3",
"ejs": "^3.1.10" "ejs": "^3.1.10"
} }
} }

View file

@ -2,13 +2,6 @@ import { discordBadgeDetails, discordBadges } from "@config/discordBadges";
import { badgeServices, botToken, redisTtl } from "@config/environment"; import { badgeServices, botToken, redisTtl } from "@config/environment";
import { fetch, redis } from "bun"; 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( export async function fetchBadges(
userId: string, userId: string,
services: string[], services: string[],
@ -148,12 +141,11 @@ export async function fetchBadges(
if (!res.ok) break; if (!res.ok) break;
const data = await res.json(); const data = await res.json();
const origin = request ? getRequestOrigin(request) : "";
if (data.avatar.startsWith("a_")) { if (data.avatar.startsWith("a_")) {
result.push({ result.push({
tooltip: "Discord Nitro", tooltip: "Discord Nitro",
badge: `${origin}/public/badges/discord/NITRO.svg`, badge: `${request ? new URL(request.url).origin : ""}/public/badges/discord/NITRO.svg`,
}); });
} }
@ -163,7 +155,7 @@ export async function fetchBadges(
discordBadgeDetails[flag as keyof typeof discordBadgeDetails]; discordBadgeDetails[flag as keyof typeof discordBadgeDetails];
result.push({ result.push({
tooltip: badge.tooltip, tooltip: badge.tooltip,
badge: `${origin}${badge.icon}`, badge: `${request ? new URL(request.url).origin : ""}${badge.icon}`,
}); });
} }
} }

View file

@ -1,3 +1,10 @@
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 { export function validateID(id: string): boolean {
if (!id) return false; if (!id) return false;

205
src/helpers/logger.ts Normal file
View file

@ -0,0 +1,205 @@
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 };

View file

@ -1,4 +1,4 @@
import { logger } from "@creations.works/logger"; import { logger } from "@helpers/logger";
import { serverHandler } from "@/server"; import { serverHandler } from "@/server";
@ -10,7 +10,3 @@ main().catch((error: Error) => {
logger.error(["Error initializing the server:", error]); logger.error(["Error initializing the server:", error]);
process.exit(1); process.exit(1);
}); });
if (process.env.IN_PTERODACTYL === "true") {
console.log("Server Started");
}

View file

@ -1,6 +1,6 @@
import { resolve } from "node:path"; import { resolve } from "node:path";
import { environment } from "@config/environment"; import { environment } from "@config/environment";
import { logger } from "@creations.works/logger"; import { logger } from "@helpers/logger";
import { import {
type BunFile, type BunFile,
FileSystemRouter, FileSystemRouter,
@ -37,9 +37,10 @@ class ServerHandler {
}, },
}); });
logger.info(`Server running at http://${server.hostname}:${server.port}`, { logger.info(
breakLine: true, `Server running at http://${server.hostname}:${server.port}`,
}); true,
);
this.logRoutes(); this.logRoutes();
} }

View file

@ -1,4 +1,4 @@
import { logger } from "@creations.works/logger"; import { logger } from "@helpers/logger";
import type { ServerWebSocket } from "bun"; import type { ServerWebSocket } from "bun";
class WebSocketHandler { class WebSocketHandler {