add index route info, make it fetch per hour instead of every user, add health route, update to latest biome config aswell as logger
All checks were successful
Code quality checks / biome (push) Successful in 15s
All checks were successful
Code quality checks / biome (push) Successful in 15s
This commit is contained in:
parent
8cfa75ec57
commit
75d3dab85e
21 changed files with 943 additions and 364 deletions
24
biome.json
24
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": {
|
||||
|
|
|
@ -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<string, string> = {
|
||||
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,
|
||||
};
|
|
@ -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;
|
45
config/index.ts
Normal file
45
config/index.ts
Normal file
|
@ -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,
|
||||
};
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"directory": "logs",
|
||||
"level": "debug",
|
||||
"level": "info",
|
||||
"disableFile": false,
|
||||
|
||||
"rotate": true,
|
||||
|
|
10
package.json
10
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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<BadgeResult> {
|
||||
const { nocache = false, separated = false } = options ?? {};
|
||||
const results: Record<string, Badge[]> = {};
|
||||
|
||||
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;
|
||||
}
|
22
src/index.ts
22
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<void> {
|
||||
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");
|
||||
}
|
||||
|
|
233
src/lib/badgeCache.ts
Normal file
233
src/lib/badgeCache.ts
Normal file
|
@ -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<void> {
|
||||
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<void> {
|
||||
if (this.updateInterval) {
|
||||
clearInterval(this.updateInterval);
|
||||
this.updateInterval = null;
|
||||
}
|
||||
echo.debug("Badge cache manager shut down");
|
||||
}
|
||||
|
||||
private async checkIfUpdateNeeded(): Promise<boolean> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<BadgeServiceData | null> {
|
||||
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<VencordEquicordData | null> {
|
||||
const data = await this.getServiceData(serviceKey);
|
||||
if (data && (serviceKey === "vencord" || serviceKey === "equicord")) {
|
||||
return data as VencordEquicordData;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async getNekocordData(): Promise<NekocordData | null> {
|
||||
const data = await this.getServiceData("nekocord");
|
||||
if (data) {
|
||||
return data as NekocordData;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async getReviewDbData(): Promise<ReviewDbData | null> {
|
||||
const data = await this.getServiceData("reviewdb");
|
||||
if (data) {
|
||||
return data as ReviewDbData;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async forceUpdateService(serviceName: string): Promise<void> {
|
||||
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();
|
260
src/lib/badges.ts
Normal file
260
src/lib/badges.ts
Normal file
|
@ -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<BadgeResult> {
|
||||
const { nocache = false, separated = false } = options ?? {};
|
||||
const results: Record<string, Badge[]> = {};
|
||||
|
||||
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;
|
||||
}
|
|
@ -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 };
|
|
@ -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<Response> {
|
||||
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<Response> {
|
|||
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<Response> {
|
|||
"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",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
|
89
src/routes/health.ts
Normal file
89
src/routes/health.ts
Normal file
|
@ -0,0 +1,89 @@
|
|||
import { redis } from "bun";
|
||||
|
||||
const routeDef: RouteDef = {
|
||||
method: "GET",
|
||||
accepts: "*/*",
|
||||
returns: "application/json",
|
||||
};
|
||||
|
||||
async function handler(): Promise<Response> {
|
||||
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<string, CacheInfo> = {};
|
||||
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 };
|
|
@ -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<Response> {
|
|||
const endPerf: number = Date.now();
|
||||
const perf: number = endPerf - request.startPerf;
|
||||
|
||||
const { query, params } = request;
|
||||
|
||||
const response: Record<string, unknown> = {
|
||||
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 };
|
||||
|
|
|
@ -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<Response> {
|
||||
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 (
|
||||
|
|
|
@ -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}`);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"]
|
||||
}
|
||||
|
|
67
types/badge.d.ts
vendored
67
types/badge.d.ts
vendored
|
@ -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;
|
||||
}
|
||||
|
|
14
types/bun.d.ts
vendored
14
types/bun.d.ts
vendored
|
@ -1,14 +1,8 @@
|
|||
import type { Server } from "bun";
|
||||
|
||||
type Query = Record<string, string>;
|
||||
type Params = Record<string, string>;
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
17
types/health.d.ts
vendored
Normal file
17
types/health.d.ts
vendored
Normal file
|
@ -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<string, CacheInfo> | { error: string };
|
||||
nextUpdate: string | null;
|
||||
};
|
||||
}
|
9
types/logger.d.ts
vendored
9
types/logger.d.ts
vendored
|
@ -1,9 +0,0 @@
|
|||
type ILogMessagePart = { value: string; color: string };
|
||||
|
||||
type ILogMessageParts = {
|
||||
level: ILogMessagePart;
|
||||
filename: ILogMessagePart;
|
||||
readableTimestamp: ILogMessagePart;
|
||||
message: ILogMessagePart;
|
||||
[key: string]: ILogMessagePart;
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue