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
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;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue