diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index ef2db44..0000000 --- a/.dockerignore +++ /dev/null @@ -1,14 +0,0 @@ -node_modules -.env -.env.example -logs -dist -.vscode -.git -.gitignore -Dockerfile -.dockerignore -README.md -LICENSE -.forgejo -.editorconfig diff --git a/.gitignore b/.gitignore index 3290e48..97ce421 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ /node_modules +bun.lock .env .vscode/settings.json -logs -dragonfly-data diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 9aeb334..0000000 --- a/Dockerfile +++ /dev/null @@ -1,30 +0,0 @@ -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/LICENSE b/LICENSE index d93a942..fb5f6af 100644 --- a/LICENSE +++ b/LICENSE @@ -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 -modification, are permitted provided that the following conditions are met: +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: -1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of 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. +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. diff --git a/README.md b/README.md index d6c2938..5084653 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,6 @@ A fast Discord badge aggregation API built with [Bun](https://bun.sh) and Redis caching. -# Preview -https://badges.atums.world - ## Features - Aggregates custom badge data from multiple sources (e.g. Vencord, Nekocord, Equicord, etc.) @@ -59,11 +56,11 @@ GET /:userId ### Query Parameters -| 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 | +| 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 | ### Supported Services @@ -71,8 +68,6 @@ GET /:userId - Equicord - Nekocord - ReviewDb -- Enmity -- Discord ( some ) ### Example @@ -80,12 +75,20 @@ 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 ``` ## License -[BSD 3](LICENSE) +[MIT](LICENSE) diff --git a/biome.json b/biome.json index 3a44cc4..921a7a5 100644 --- a/biome.json +++ b/biome.json @@ -7,7 +7,7 @@ }, "files": { "ignoreUnknown": true, - "ignore": ["dist"] + "ignore": [] }, "formatter": { "enabled": true, @@ -17,29 +17,11 @@ "organizeImports": { "enabled": true }, - "css": { - "formatter": { - "indentStyle": "tab", - "lineEnding": "lf" - } - }, "linter": { "enabled": true, "rules": { - "recommended": true, - "correctness": { - "noUnusedImports": "error", - "noUnusedVariables": "error" - }, - "suspicious": { - "noConsole": "error" - }, - "style": { - "useConst": "error", - "noVar": "error" - } - }, - "ignore": ["types"] + "recommended": true + } }, "javascript": { "formatter": { diff --git a/bun.lock b/bun.lock deleted file mode 100644 index a04a9ab..0000000 --- a/bun.lock +++ /dev/null @@ -1,48 +0,0 @@ -{ - "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 deleted file mode 100644 index 4e38729..0000000 --- a/compose.yml +++ /dev/null @@ -1,30 +0,0 @@ -services: - badge-api: - container_name: badge-api - pull_policy: build - build: - context: . - restart: unless-stopped - ports: - - "8080:8080" - 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 diff --git a/config/constants.ts b/config/constants.ts deleted file mode 100644 index b6ec990..0000000 --- a/config/constants.ts +++ /dev/null @@ -1,162 +0,0 @@ -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, - 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, - - // Bot badges - VERIFIED_BOT: 1 << 16, - BOT_HTTP_INTERACTIONS: 1 << 19, - SUPPORTS_COMMANDS: 1 << 23, - USES_AUTOMOD: 1 << 24, -}; - -const discordBadgeDetails = { - HYPESQUAD: { - tooltip: "HypeSquad Events", - icon: "/public/badges/discord/HYPESQUAD.svg", - }, - HYPESQUAD_ONLINE_HOUSE_1: { - tooltip: "HypeSquad Bravery", - icon: "/public/badges/discord/HYPESQUAD_ONLINE_HOUSE_1.svg", - }, - HYPESQUAD_ONLINE_HOUSE_2: { - tooltip: "HypeSquad Brilliance", - icon: "/public/badges/discord/HYPESQUAD_ONLINE_HOUSE_2.svg", - }, - HYPESQUAD_ONLINE_HOUSE_3: { - tooltip: "HypeSquad Balance", - icon: "/public/badges/discord/HYPESQUAD_ONLINE_HOUSE_3.svg", - }, - - STAFF: { - tooltip: "Discord Staff", - icon: "/public/badges/discord/STAFF.svg", - }, - PARTNER: { - tooltip: "Discord Partner", - icon: "/public/badges/discord/PARTNER.svg", - }, - CERTIFIED_MODERATOR: { - tooltip: "Certified Moderator", - icon: "/public/badges/discord/CERTIFIED_MODERATOR.svg", - }, - - VERIFIED_DEVELOPER: { - tooltip: "Verified Bot Developer", - icon: "/public/badges/discord/VERIFIED_DEVELOPER.svg", - }, - ACTIVE_DEVELOPER: { - tooltip: "Active Developer", - icon: "/public/badges/discord/ACTIVE_DEVELOPER.svg", - }, - - PREMIUM_EARLY_SUPPORTER: { - tooltip: "Premium Early Supporter", - icon: "/public/badges/discord/PREMIUM_EARLY_SUPPORTER.svg", - }, - - BUG_HUNTER_LEVEL_1: { - tooltip: "Bug Hunter (Level 1)", - icon: "/public/badges/discord/BUG_HUNTER_LEVEL_1.svg", - }, - BUG_HUNTER_LEVEL_2: { - tooltip: "Bug Hunter (Level 2)", - icon: "/public/badges/discord/BUG_HUNTER_LEVEL_2.svg", - }, - - SUPPORTS_COMMANDS: { - tooltip: "Supports Commands", - icon: "/public/badges/discord/SUPPORTS_COMMANDS.svg", - }, - USES_AUTOMOD: { - 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", - 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/discordBadges.ts b/config/discordBadges.ts new file mode 100644 index 0000000..7e0e258 --- /dev/null +++ b/config/discordBadges.ts @@ -0,0 +1,87 @@ +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, + + STAFF: 2 << 0, + PARTNER: 2 << 1, + CERTIFIED_MODERATOR: 2 << 18, + + VERIFIED_DEVELOPER: 2 << 17, + ACTIVE_DEVELOPER: 2 << 22, + + PREMIUM_EARLY_SUPPORTER: 2 << 9, + + BUG_HUNTER_LEVEL_1: 2 << 3, + BUG_HUNTER_LEVEL_2: 2 << 14, + + // Bot badges + SUPPORTS_COMMANDS: 2 << 23, + USES_AUTOMOD: 2 << 24, +}; + +export const discordBadgeDetails = { + HYPESQUAD: { + tooltip: "HypeSquad Events", + icon: "/public/badges/discord/HYPESQUAD.svg", + }, + HYPESQUAD_ONLINE_HOUSE_1: { + tooltip: "HypeSquad Bravery", + icon: "/public/badges/discord/HYPESQUAD_ONLINE_HOUSE_1.svg", + }, + HYPESQUAD_ONLINE_HOUSE_2: { + tooltip: "HypeSquad Brilliance", + icon: "/public/badges/discord/HYPESQUAD_ONLINE_HOUSE_2.svg", + }, + HYPESQUAD_ONLINE_HOUSE_3: { + tooltip: "HypeSquad Balance", + icon: "/public/badges/discord/HYPESQUAD_ONLINE_HOUSE_3.svg", + }, + + STAFF: { + tooltip: "Discord Staff", + icon: "/public/badges/discord/STAFF.svg", + }, + PARTNER: { + tooltip: "Discord Partner", + icon: "/public/badges/discord/PARTNER.svg", + }, + CERTIFIED_MODERATOR: { + tooltip: "Certified Moderator", + icon: "/public/badges/discord/CERTIFIED_MODERATOR.svg", + }, + + VERIFIED_DEVELOPER: { + tooltip: "Verified Bot Developer", + icon: "/public/badges/discord/VERIFIED_DEVELOPER.svg", + }, + ACTIVE_DEVELOPER: { + tooltip: "Active Developer", + icon: "/public/badges/discord/ACTIVE_DEVELOPER.svg", + }, + + PREMIUM_EARLY_SUPPORTER: { + tooltip: "Premium Early Supporter", + icon: "/public/badges/discord/PREMIUM_EARLY_SUPPORTER.svg", + }, + + BUG_HUNTER_LEVEL_1: { + tooltip: "Bug Hunter (Level 1)", + icon: "/public/badges/discord/BUG_HUNTER_LEVEL_1.svg", + }, + BUG_HUNTER_LEVEL_2: { + tooltip: "Bug Hunter (Level 2)", + icon: "/public/badges/discord/BUG_HUNTER_LEVEL_2.svg", + }, + + SUPPORTS_COMMANDS: { + tooltip: "Supports Commands", + icon: "/public/badges/discord/SUPPORTS_COMMANDS.svg", + }, + USES_AUTOMOD: { + tooltip: "Uses AutoMod", + icon: "/public/badges/discord/USES_AUTOMOD.svg", + }, +}; diff --git a/config/environment.ts b/config/environment.ts new file mode 100644 index 0000000..6b056d6 --- /dev/null +++ b/config/environment.ts @@ -0,0 +1,43 @@ +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 deleted file mode 100644 index cd99868..0000000 --- a/config/index.ts +++ /dev/null @@ -1,45 +0,0 @@ -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"]; - - 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 deleted file mode 100644 index cc10cab..0000000 --- a/logger.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "directory": "logs", - "level": "info", - "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 1a420b5..db5e8a9 100644 --- a/package.json +++ b/package.json @@ -7,13 +7,18 @@ "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.lock" + "cleanup": "rm -rf logs node_modules bun.lockdb" }, "devDependencies": { - "@types/bun": "latest", + "@types/bun": "^1.2.9", + "@types/ejs": "^3.1.5", + "globals": "^16.0.0", "@biomejs/biome": "^1.9.4" }, + "peerDependencies": { + "typescript": "^5.8.3" + }, "dependencies": { - "@atums/echo": "latest" + "ejs": "^3.1.10" } } diff --git a/public/badges/equicord.svg b/public/badges/equicord.svg deleted file mode 100644 index d60e5ce..0000000 --- a/public/badges/equicord.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/public/badges/vencord.png b/public/badges/vencord.png deleted file mode 100644 index 7f43004..0000000 Binary files a/public/badges/vencord.png and /dev/null differ diff --git a/src/helpers/badges.ts b/src/helpers/badges.ts new file mode 100644 index 0000000..68d5ce4 --- /dev/null +++ b/src/helpers/badges.ts @@ -0,0 +1,184 @@ +import { discordBadgeDetails, discordBadges } from "@config/discordBadges"; +import { badgeServices, botToken, redisTtl } from "@config/environment"; +import { fetch, redis } from "bun"; + +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(); + + if (data.avatar.startsWith("a_")) { + result.push({ + tooltip: "Discord Nitro", + badge: `${request ? new URL(request.url).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: `${request ? new URL(request.url).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/helpers/char.ts b/src/helpers/char.ts new file mode 100644 index 0000000..c885429 --- /dev/null +++ b/src/helpers/char.ts @@ -0,0 +1,19 @@ +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; + + return /^\d{17,20}$/.test(id.trim()); +} + +export function parseServices(input: string): string[] { + return input + .split(/[\s,]+/) + .map((s) => s.trim()) + .filter(Boolean); +} diff --git a/src/helpers/logger.ts b/src/helpers/logger.ts new file mode 100644 index 0000000..4cbb12b --- /dev/null +++ b/src/helpers/logger.ts @@ -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 }; diff --git a/src/index.ts b/src/index.ts index fbdbfe9..60606d4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,37 +1,12 @@ -import { echo } from "@atums/echo"; -import { verifyRequiredVariables } from "@config"; -import { badgeCacheManager } from "@lib/badgeCache"; -import { serverHandler } from "@server"; +import { logger } from "@helpers/logger"; + +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(); } main().catch((error: Error) => { - echo.error({ - message: "Error initializing the server", - error: error.message, - }); + logger.error(["Error initializing the server:", error]); process.exit(1); }); - -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 deleted file mode 100644 index 5821e09..0000000 --- a/src/lib/badgeCache.ts +++ /dev/null @@ -1,296 +0,0 @@ -import { echo } from "@atums/echo"; -import { - badgeFetchInterval, - badgeServices, - discordBadgeDetails, - 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; - } - } - - if (typeof service.pluginsUrl === "string") { - const contributorRes = await fetch(service.pluginsUrl, { - 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)) { - for (const author of plugin.authors) { - if (author.id) { - contributors.add(author.id); - } - } - } - } - - const badgeDetails = - serviceKey === "vencord" - ? { - tooltip: - discordBadgeDetails.VENCORD_CONTRIBUTOR.tooltip, - badge: discordBadgeDetails.VENCORD_CONTRIBUTOR.icon, - } - : { - tooltip: - discordBadgeDetails.EQUICORD_CONTRIBUTOR.tooltip, - badge: discordBadgeDetails.EQUICORD_CONTRIBUTOR.icon, - }; - - 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; - } - - 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 deleted file mode 100644 index f92789e..0000000 --- a/src/lib/badges.ts +++ /dev/null @@ -1,270 +0,0 @@ -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}`; -} - -const USER_CACHE_SERVICES = ["discord", "enmity"]; - -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(); - - if (!USER_CACHE_SERVICES.includes(serviceKey) || nocache) { - return false; - } - - const userCacheKey = `user_badges:${serviceKey}:${userId}`; - - 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)) { - 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: badgeUrl, - }); - } - } - 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: discordBadgeDetails.DISCORD_NITRO.tooltip, - badge: `${origin}${discordBadgeDetails.DISCORD_NITRO.icon}`, - }); - } - - 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; - } - - results[serviceKey] = result; - - if ( - USER_CACHE_SERVICES.includes(serviceKey) && - !nocache && - result.length > 0 - ) { - 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/lib/char.ts b/src/lib/char.ts deleted file mode 100644 index f6038db..0000000 --- a/src/lib/char.ts +++ /dev/null @@ -1,14 +0,0 @@ -function validateID(id: string | undefined): boolean { - if (!id) return false; - - return /^\d{17,20}$/.test(id.trim()); -} - -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 1f291cf..4b8421e 100644 --- a/src/routes/[id].ts +++ b/src/routes/[id].ts @@ -1,6 +1,6 @@ -import { badgeServices } from "@config"; -import { fetchBadges } from "@lib/badges"; -import { parseServices, validateID } from "@lib/char"; +import { badgeServices } from "@config/environment"; +import { fetchBadges } from "@helpers/badges"; +import { parseServices, validateID } from "@helpers/char"; function isValidServices(services: string[]): boolean { if (!Array.isArray(services)) return false; @@ -18,52 +18,47 @@ const routeDef: RouteDef = { async function handler(request: ExtendedRequest): Promise { const { id: userId } = request.params; - const { services, cache = "true", seperated = "false" } = request.query; + const { services, cache, seperated } = request.query; + + let validServices: string[]; if (!validateID(userId)) { return Response.json( { status: 400, - error: "Invalid Discord User ID. Must be 17-20 digits.", + error: "Invalid Discord User ID", + }, + { + status: 400, }, - { status: 400 }, ); } - let validServices: string[]; - const availableServices = badgeServices.map((b) => b.service); - if (services) { const parsed = parseServices(services); - 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 }, - ); - } + if (parsed.length > 0) { + if (!isValidServices(parsed)) { + return Response.json( + { + status: 400, + error: "Invalid Services", + }, + { + status: 400, + }, + ); + } - validServices = parsed; + validServices = parsed; + } else { + validServices = badgeServices.map((b) => b.service); + } } else { - validServices = availableServices; + validServices = badgeServices.map((b) => b.service); } - const badges = await fetchBadges( + const badges: BadgeResult = await fetchBadges( userId, validServices, { @@ -73,18 +68,27 @@ async function handler(request: ExtendedRequest): Promise { request, ); - const isEmpty = Array.isArray(badges) - ? badges.length === 0 - : Object.keys(badges).length === 0; + if (badges instanceof Error) { + return Response.json( + { + status: 500, + error: badges.message, + }, + { + status: 500, + }, + ); + } - if (isEmpty) { + if (badges.length === 0) { return Response.json( { status: 404, - error: "No badges found for this user", - services: validServices, + error: "No Badges Found", + }, + { + status: 404, }, - { status: 404 }, ); } @@ -101,6 +105,9 @@ 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 deleted file mode 100644 index 12c0dc9..0000000 --- a/src/routes/health.ts +++ /dev/null @@ -1,89 +0,0 @@ -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 bbfacb5..76e5c37 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -1,5 +1,3 @@ -import { badgeServices, getServiceDescription, gitUrl } from "@config"; - const routeDef: RouteDef = { method: "GET", accepts: "*/*", @@ -10,56 +8,15 @@ async function handler(request: ExtendedRequest): Promise { const endPerf: number = Date.now(); const perf: number = endPerf - request.startPerf; - 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, - }, + const { query, params } = request; + + const response: Record = { + perf, + query, + params, }; - return Response.json(response, { - headers: { - "Cache-Control": "public, max-age=300", - }, - }); + return Response.json(response); } export { handler, routeDef }; diff --git a/src/server.ts b/src/server.ts index d24b025..909813c 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,14 +1,14 @@ import { resolve } from "node:path"; -import { Echo, echo } from "@atums/echo"; -import { environment } from "@config"; +import { environment } from "@config/environment"; +import { logger } from "@helpers/logger"; import { type BunFile, FileSystemRouter, type MatchedRoute, - type Server, + type Serve, } 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: resolve("src", "routes"), + dir: "./src/routes", fileExtensions: [".ts"], origin: `http://${this.host}:${this.port}`, }); } public initialize(): void { - const server: Server = Bun.serve({ + const server: Serve = Bun.serve({ port: this.port, hostname: this.host, fetch: this.handleRequest.bind(this), @@ -37,16 +37,16 @@ class ServerHandler { }, }); - const echoChild = new Echo({ disableFile: true }); - - echoChild.info( + logger.info( `Server running at http://${server.hostname}:${server.port}`, + true, ); - this.logRoutes(echoChild); + + this.logRoutes(); } - private logRoutes(echo: Echo): void { - echo.info("Available routes:"); + private logRoutes(): void { + logger.info("Available routes:"); const sortedRoutes: [string, string][] = Object.entries( this.router.routes, @@ -55,19 +55,14 @@ class ServerHandler { ); for (const [path, filePath] of sortedRoutes) { - echo.info(`Route: ${path}, File: ${filePath}`); + logger.info(`Route: ${path}, File: ${filePath}`); } } - private async serveStaticFile( - request: ExtendedRequest, - pathname: string, - ip: string, - ): Promise { - let filePath: string; - let response: Response; - + private async serveStaticFile(pathname: string): Promise { try { + let filePath: string; + if (pathname === "/favicon.ico") { filePath = resolve("public", "assets", "favicon.ico"); } else { @@ -78,98 +73,35 @@ 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, { + return 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) { - echo.error({ - message: `Error serving static file: ${pathname}`, - error: error as Error, - }); - response = new Response("Internal Server Error", { status: 500 }); + logger.error([`Error serving static file: ${pathname}`, error as Error]); + return 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( request: Request, - server: Server, + server: BunServer, ): Promise { 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)) { - 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: string = 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(extendedRequest, pathname, ip); + return await this.serveStaticFile(pathname); } const match: MatchedRoute | null = this.router.match(request); let requestBody: unknown = {}; + let response: Response; if (match) { const { filePath, params, query } = match; @@ -178,7 +110,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() ?? null) + ? contentType.split(";")[0].trim() : null; if ( @@ -267,10 +199,7 @@ class ServerHandler { } } } catch (error: unknown) { - echo.error({ - message: `Error handling route ${request.url}`, - error: error, - }); + logger.error([`Error handling route ${request.url}:`, error as Error]); response = Response.json( { @@ -292,11 +221,31 @@ class ServerHandler { ); } - this.logRequest(extendedRequest, response, ip); + 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", + ); + return response; } } - const serverHandler: ServerHandler = new ServerHandler( environment.port, environment.host, diff --git a/src/websocket.ts b/src/websocket.ts index 6be34fd..99686e8 100644 --- a/src/websocket.ts +++ b/src/websocket.ts @@ -1,33 +1,27 @@ -import { echo } from "@atums/echo"; +import { logger } from "@helpers/logger"; import type { ServerWebSocket } from "bun"; class WebSocketHandler { public handleMessage(ws: ServerWebSocket, message: string): void { - echo.info(`WebSocket received: ${message}`); + logger.info(`WebSocket received: ${message}`); try { ws.send(`You said: ${message}`); } catch (error) { - echo.error({ - message: "WebSocket send error", - error: (error as Error).message, - }); + logger.error(["WebSocket send error", error as Error]); } } public handleOpen(ws: ServerWebSocket): void { - echo.info("WebSocket connection opened."); + logger.info("WebSocket connection opened."); try { ws.send("Welcome to the WebSocket server!"); } catch (error) { - echo.error({ - message: "WebSocket send error", - error: (error as Error).message, - }); + logger.error(["WebSocket send error", error as Error]); } } - public handleClose(_ws: ServerWebSocket, code: number, reason: string): void { - echo.info(`WebSocket closed with code ${code}, reason: ${reason}`); + public handleClose(ws: ServerWebSocket, code: number, reason: string): void { + logger.warn(`WebSocket closed with code ${code}, reason: ${reason}`); } } diff --git a/tsconfig.json b/tsconfig.json index ec4e48d..68a5a97 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,30 +2,32 @@ "compilerOptions": { "baseUrl": "./", "paths": { - "@*": ["src/*"], - "@config": ["config/index.ts"], + "@/*": ["src/*"], "@config/*": ["config/*"], "@types/*": ["types/*"], - "@lib/*": ["src/lib/*"] + "@helpers/*": ["src/helpers/*"] }, - "typeRoots": ["./types", "./node_modules/@types"], + "typeRoots": ["./src/types", "./node_modules/@types"], + // Enable latest features "lib": ["ESNext", "DOM"], "target": "ESNext", "module": "ESNext", "moduleDetection": "force", - "allowJs": false, + "jsx": "react-jsx", + "allowJs": true, + // Bundler mode "moduleResolution": "bundler", - "allowImportingTsExtensions": false, + "allowImportingTsExtensions": true, "verbatimModuleSyntax": true, "noEmit": true, + // Best practices "strict": true, "skipLibCheck": true, "noFallthroughCasesInSwitch": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "exactOptionalPropertyTypes": true, - "noUncheckedIndexedAccess": true, + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, "noPropertyAccessFromIndexSignature": false }, - "include": ["src", "types"] + "include": ["src", "types", "config"] } diff --git a/types/badge.d.ts b/types/badge.d.ts index 5f235a7..0731370 100644 --- a/types/badge.d.ts +++ b/types/badge.d.ts @@ -10,7 +10,7 @@ interface FetchBadgesOptions { separated?: boolean; } -type BadgeService = { +type badgeURLMap = { service: string; url: | string @@ -19,84 +19,4 @@ type BadgeService = { user: string; badge: (id: string) => string; }); - pluginsUrl?: 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 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; -} - -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; -} diff --git a/types/bun.d.ts b/types/bun.d.ts index 9afe286..018bf35 100644 --- a/types/bun.d.ts +++ b/types/bun.d.ts @@ -1,8 +1,14 @@ +import type { Server } from "bun"; + type Query = Record; type Params = Record; -interface ExtendedRequest extends Request { - startPerf: number; - query: Query; - params: Params; +declare global { + type BunServer = Server; + + interface ExtendedRequest extends Request { + startPerf: number; + query: Query; + params: Params; + } } diff --git a/types/health.d.ts b/types/health.d.ts deleted file mode 100644 index 786acc1..0000000 --- a/types/health.d.ts +++ /dev/null @@ -1,17 +0,0 @@ -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 new file mode 100644 index 0000000..ff6a601 --- /dev/null +++ b/types/logger.d.ts @@ -0,0 +1,9 @@ +type ILogMessagePart = { value: string; color: string }; + +type ILogMessageParts = { + level: ILogMessagePart; + filename: ILogMessagePart; + readableTimestamp: ILogMessagePart; + message: ILogMessagePart; + [key: string]: ILogMessagePart; +};