From ff9408fb8c268cc838e5171ae03887159797b49d Mon Sep 17 00:00:00 2001 From: Liam Date: Wed, 16 Oct 2024 07:31:52 +0100 Subject: [PATCH] re-add per page/leaderboard embed colors --- .gitea/kubernetes/backend/sealed-secret.yaml | 5 +- bun.lockb | Bin 321016 -> 333488 bytes projects/backend/Dockerfile | 15 +++++- projects/backend/package.json | 2 + projects/backend/src/common/config.ts | 1 + .../src/controller/image.controller.ts | 10 ++++ projects/backend/src/index.ts | 9 +++- .../backend/src/service/image.service.tsx | 47 ++++++++++++++++++ projects/backend/src/service/score.service.ts | 36 ++++++++++++++ .../(pages)/leaderboard/[...slug]/page.tsx | 8 +-- .../src/app/(pages)/player/[...slug]/page.tsx | 8 +-- projects/website/src/common/image-utils.ts | 12 +++-- 12 files changed, 131 insertions(+), 22 deletions(-) create mode 100644 projects/backend/src/service/score.service.ts diff --git a/.gitea/kubernetes/backend/sealed-secret.yaml b/.gitea/kubernetes/backend/sealed-secret.yaml index 3b51b19..5a642e6 100644 --- a/.gitea/kubernetes/backend/sealed-secret.yaml +++ b/.gitea/kubernetes/backend/sealed-secret.yaml @@ -7,8 +7,9 @@ metadata: namespace: public-services spec: encryptedData: - MONGO_URI: AgBZ4poRziaXwQLAspWQkgxQIKTYVq2w1fPBSbOBugso2kk5S6UpYo2MXD4F/ERDZM82p41yoNbwA5D8qPkAXlm7rdhiJI9PA410m/96eSFtE1UHNX0KXqrK/ay+ck9G2gjXv2osvrKphAgMy2k/PKbDDUE8p4iHVqDrzpTb5O1JvPg9Lcdv3kHZQL/wFWQDI1hzjL3W8ZkdeyNM81y20aG/is66rIcRukPPcg3rPfhLIcXmNlw6ows3SZqgN4QNWrUpT3pwaxySj4v1UbusH/HQGpdAcR0AjpTfIdG7o0+wBQPCeUMDMowvIyXQn/3rfeVG0mPSwEceJPhJ527YOcd91E693/c2QZKwG0slXnDSfKcrkiVeM7Mirjz6HHm5GQnFOUz7/i0Dttb8U56HGepqLJ8xcYFE5VWy/vzfmqD9bADHw9Ivk5lfXaKgrBWYQS7GNy8h9XISIszcQdkfz6cDhErl9iU6POYX2YnU1mxl3mSOPPM3efxJ/bm0PAZA8Ezyo6ww1P96QCbUCfZt/Ju1OYVvkwBGrJfo0bXx6x5yzd9Y22EKGoo3hTcoFbRzangVjf0/Vvu9EibC3UNEqeB/NwD5Xo8FvSbovr/wrmH12DUWVJYzKyWLPJObF8rIpn9OI1dsHk53jpfJyfToguy6ZQwsDU18OTqXPKyz86X/h2+jSUwuTGa+ktTIm78Ff8KrQ6sFSeqtskwdvLte5pclErdiRTPSCGxUu8jeqQM9q/ytsf2flWEXqLxoTuHWe9w+kylbimm3nQclViY3dX6ib7H6TYZkQE/GfFg9C+B5PfN6MaE83hSbQW8= - TRACKED_PLAYERS_WEBHOOK: AgAObYj3o8BC0xnSOSSKzXw6ChSaAfax/X04isNQNRppygIkACuli9u0ywvcG3q0uneiqzr2XLj1gKzF1nknufn3QnPsina6LcuGsvok7HbzjSy9MSHjq+/9O+S5GSYuY5rhQIlpXXzhmt5HJjbX2KSxoq6CMaJYKTzEL1mkjWHgXnECbFHlANwJFRfCI6lUblz6Kvb3yjKAR3HAqo7P4c5YoI3N7ZBW77y+vgZiM+n7Iwpq1liFLW68gg1t8kGY/y+OeUdTfcyf4svhPg4la+gUHCLwufxfSI3vQDZcndFGBA2MPJ/Eoc2itKoBMzA66w3CQPU8nAS5qBln51OASqCsvX6/Ipd0bFZyBrU7j8jPN1gpaWOIwMEhKT2t5nmLyzXN1aaYv8vl9hY6NpFN6T2QOBFsJ849KyoXdN6wfmCV5rPL9blSLSAuS0sy4LOvR5COgsZyCwUycRb8ZLq/gg+r97ySPYliQnuVbMcoce78+YyZGZn3+5T3tRQQ/E7qx7ZEgALKXGwL97LDuTXmuV8BNT0fwnqWqR5n0ZQvjo7FuxQAC4XufBAAdsxb429qflDfpZf5PYOoDUnTdmx2g7enmBb/WDa3Vxz4LTxollRf31HtkH9d02EVP0JQZ7u3fQkeEm3RKffeyNoIy/1K3+PdaVGre8PEuPRTF0RnLYQjyVka4sbJJwX1MvR/6uQX4iKF50dWfohcnt8z44chQKQQhQbzK6tVUIbfhN3XT4b1SWbbMxVXTxxtXjVVnE85yQ7XEo9f1RHbDi3WL2MMnFBnYxwjdHWcZhiHhppIVa+lsGCoO2i+HN4BEfwWP9oecgdLhSZ4OLC9Ps+z9QzF9jDEQuMML5Hpj7zZ + MONGO_URI: AgC1aNi0ISr4nYuMufqC0LK769TzVltSosJZXXJ2fxYwFNylIVUzolc1QrMmMBZLGDy1Jr7aOVKCz58LK0xUd2JIlejuzVHcwu7m6l0Qkqv8ghGgZ5CF7w2vlqWXnhBOffmUjvlrWB0UXSeY50M/0M5c8VcvbnEyoQ4+00cA/VJmzoWbZ0P573IQgRax6TZa7wTjLjKcxODFmtitxPZGAio1tEkqDmbvxbBGYHdDj/ZRfUH1FDbcMjlLhFu/l46zYAYW33372J1qTwL8/111XqJREvmsEna/CtpGoqBkPI0MOH6Tm5ggN8GnpmKbZby2eDgLAptu3rqQYAFrdrwAfUBoPrATYYTgyfe9quYJlZj8cxNVNH/y5fVdZZQWJLzqzPSjww6BV9SfTzU1Eo9/cdEKaGWjZsDsYYgicvkj1GLhiN/qPKMmdatF48x02oefT51toFIjb6lu+s0bqDVnk/w07w0ASN1VbJL5s6Z1/aqeZIWYGcRdpj500UI4zVQuI+3AYEBJJzGSYQNluNhhJBv9TAhh2TddTkENEijLJLjthke0yztDvNRrIXhziKOr8TWhQcIv5Hb0kg0J+Cvq/9Fu1BlDydHNcIc4/a3OhHPnqfhVlRwiCjJ8I56wYIQjkLoKK80qjBs86RCC/sKC9w65+deG4KSsclpDY8kNpf9MKg9OreLFMneN6CvQR19yEIaqCoA9moyeyY+EYvgioN4lTjlg0Kbn6D65yleQiO0LEYYutYAZRdJuLI/0oLHtRLsnr0+FL+SDAqGKPyNZ9JNpBCGPVml4Y2Czh4qCV24CxulbAIAiBmbcQos= + NUMBER_ONE_WEBHOOK: AgAsmhgyllPfA0/z+vFG/cwT7IOix/x7QG/5SR0xfCSOXwn/oD9Y2MpAVegjFlQ1nEh5o/pVFqPKY44VtoNXy+8R8rnRK+gvRraYliobHBJf8OBNgW8B24lbg8RfO+ll84VrdV4tmK1TTaliuHpb+CyP2i4VMD54Zgu2xqoJzWSS4TGkzaECSr9kEsmW0mPf1wlVxWq3QKZIjJrZn5B0qM8qoUS1Q78eN297Lz4y000Ncb7dR4FybrQdBBcFGkGrPvFyMSVASK8AwGooMSxAU6PcE7XHGUgN8KofQ94CSrtlngrdcq/5XapUU5my2EkWB1/qsEhBwHj1f7HiRxPbBosvc6beszif65c97mEc7GANdDN9H4ywo6Gba/pChnd2EVn+Rr41UlnkPlhjjIuLSe32LdWXZMkjohuuhbmTH38KtYd1rwA9rYVUHVnJW9mIvteA/eiEdvGzcdppxek/ywxv1lwB5eifX/e2ScaRtREot9q7N7XbyUOwKbVJLIpAlhEWREZmwtOdka6xJEWSHNVPKM1M3WO3KyaQKz01P0ADamCcQs5timHArizDFWK28FgVtNR480WBfd19xwYxh8hC3Z+a4LQfGUy/ZPIoan7cQ55S0bnROWTblXRSkrGAWdF5b3dc5ltiE1+OnE1gVx2/KMg17treDkbeG31KkmISfIZA7gUhP/vfagD0ljEb5cpiYo+cOqBrkomW3BKGHmJqebS+EF9+eGA/xAQopQrxQCOzp1CLq+rjW5UPBCICNpaUSMKkln1sRLamwoobc8By8DLcNks743gDOn2+igYj+twoD+mRwY2EK85tBMzuHali1iTHU3B4kxRsf0Z9YE1FJl2l+ugo4H3J + TRACKED_PLAYERS_WEBHOOK: AgCPeq3/S63zyqlzYuLncX5BcAagVwVu/nDhRl0e5NmBctZ2eZ2bq1z7rwz7LldVDIs/gAGWe4WzrxPzDTLu1xgTnvzCrSDuZQHsvHph8v9obz+8qSbogtRrPmRkPgIXFJ0KTN1B64aarPBzeAsUW/BMe2M+UeT44JVyVLT3Z1Pq/+a4g2Bc6FLlFiJMBFnGtYsKE4OMhpvKK31S6yG4+OEcYL+RLbSwGPHOsQDh9hYoOYekkAtcOp2+0Ee972dgBq3qbYMDRL29ETBuof4TxasHDwRE9L+HElL1RvsfU+wKfLuaq5Uhm6pNb+zhBtQd8M6XuRpNUQ2J2L0WAWBTdK98unej3ebUnsIIan0l0LfBa5ZqnG3YB9+aaAntx+l/ZQztHlUIOwmRQm0p1hjJoZXhNLWrzNtRLMGbvyJMeSPvuwxoHh3qlcgbC2opMpX6yYRzTIMy6ZVJDBnTIMDYXESIDW/t70v6SqBWens++XMm20tHAAEQF/0gVw3wYZ7Z+EOHH/By7YFGC79xakb8d6Fh98V/s4KeDpxPJSekZEo7ETHOsju5ApPW+6CS1weWGrQPP2+SU5vAysEcu+Mjvs+xWHgxML5YPBfjfpdCMtpA1P0Cdsr28WYWYdUTv8FZbSna+I4oqxn0sx5ONv5VMAUN0Cd4OdfCfiiHgWWGfGHB3QQHvQuPpwR2nl0mrQHEQkaCuUSDNDvrs/h6fXLgd5JsgEz1CeBx5A+j4h03TB1sRIN8KmcC3vgwEIyEoEoqTdJDbo07ttzs41f0i01GFwT/hO9KIxGwTW6yr/iRfu29MJL11xKhVPKx5BOZcSaK2fBq/jMW8rQJbHqMukrHzo+urNzY9iH+wcpq template: metadata: creationTimestamp: null diff --git a/bun.lockb b/bun.lockb index ac67934204ad59234795e7b779f9d29f6f18d951..951287211cabe7bddae624f8fc994b3f2b63b969 100755 GIT binary patch delta 14279 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 CsJ^TK diff --git a/projects/backend/Dockerfile b/projects/backend/Dockerfile index ecb4bbd..2a5ebb2 100644 --- a/projects/backend/Dockerfile +++ b/projects/backend/Dockerfile @@ -1,5 +1,18 @@ FROM oven/bun:1.1.30-alpine AS base +# Install system dependencies for node-canvas +RUN apk add --no-cache \ + build-base \ + cairo-dev \ + pango-dev \ + giflib-dev \ + libjpeg-turbo-dev \ + freetype-dev \ + fontconfig-dev \ + pixman-dev \ + python3 \ + pkgconfig + # Install dependencies FROM base AS depends WORKDIR /app @@ -24,4 +37,4 @@ RUN bun --filter '@ssr/common' build # Copy the backend project COPY --from=depends /app/projects/backend ./projects/backend -CMD ["bun", "run", "--filter", "backend", "start"] \ No newline at end of file +CMD ["bun", "run", "--filter", "backend", "start"] diff --git a/projects/backend/package.json b/projects/backend/package.json index 8e628b1..45399f1 100644 --- a/projects/backend/package.json +++ b/projects/backend/package.json @@ -15,12 +15,14 @@ "@tqman/nice-logger": "^1.0.1", "@typegoose/typegoose": "^12.8.0", "@vercel/og": "^0.6.3", + "canvas": "^3.0.0-rc2", "discord-webhook-node": "^1.1.8", "elysia": "latest", "elysia-autoroutes": "^0.5.0", "elysia-decorators": "^1.0.2", "elysia-helmet": "^2.0.0", "elysia-rate-limit": "^4.1.0", + "extract-colors": "^4.1.0", "ky": "^1.7.2", "mongoose": "^8.7.0", "node-cache": "^5.1.2", diff --git a/projects/backend/src/common/config.ts b/projects/backend/src/common/config.ts index 7077c4a..aeca6ca 100644 --- a/projects/backend/src/common/config.ts +++ b/projects/backend/src/common/config.ts @@ -2,4 +2,5 @@ export const Config = { mongoUri: process.env.MONGO_URI, apiUrl: process.env.API_URL || "https://ssr.fascinated.cc/api", trackedPlayerWebhook: process.env.TRACKED_PLAYERS_WEBHOOK, + numberOneWebhook: process.env.NUMBER_ONE_WEBHOOK, }; diff --git a/projects/backend/src/controller/image.controller.ts b/projects/backend/src/controller/image.controller.ts index c94ed3a..e695a39 100644 --- a/projects/backend/src/controller/image.controller.ts +++ b/projects/backend/src/controller/image.controller.ts @@ -4,6 +4,16 @@ import { ImageService } from "../service/image.service"; @Controller("/image") export default class ImageController { + @Get("/averagecolor/:url", { + config: {}, + params: t.Object({ + url: t.String({ required: true }), + }), + }) + public async getImageAverageColor({ params: { url } }: { params: { url: string } }) { + return await ImageService.getAverageImageColor(url); + } + @Get("/player/:id", { config: {}, params: t.Object({ diff --git a/projects/backend/src/index.ts b/projects/backend/src/index.ts index ed1c7b1..6c711f7 100644 --- a/projects/backend/src/index.ts +++ b/projects/backend/src/index.ts @@ -21,6 +21,10 @@ import { delay } from "@ssr/common/utils/utils"; import { connectScoreSaberWebSocket } from "@ssr/common/websocket/scoresaber-websocket"; import ImageController from "./controller/image.controller"; import ReplayController from "./controller/replay.controller"; +// @ts-ignore +import { MessageBuilder, Webhook } from "discord-webhook-node"; +import { formatPp } from "@ssr/common/utils/number-utils"; +import { ScoreService } from "./service/score.service"; // Load .env file dotenv.config({ @@ -33,8 +37,9 @@ await mongoose.connect(Config.mongoUri!); // Connect to MongoDB setLogLevel("DEBUG"); connectScoreSaberWebSocket({ - onScore: async score => { - await PlayerService.trackScore(score); + onScore: async playerScore => { + await PlayerService.trackScore(playerScore); + await ScoreService.notifyNumberOne(playerScore); }, }); diff --git a/projects/backend/src/service/image.service.tsx b/projects/backend/src/service/image.service.tsx index a1e8225..24a96a6 100644 --- a/projects/backend/src/service/image.service.tsx +++ b/projects/backend/src/service/image.service.tsx @@ -9,11 +9,58 @@ import NodeCache from "node-cache"; import ScoreSaberLeaderboardToken from "@ssr/common/types/token/scoresaber/score-saber-leaderboard-token"; import ScoreSaberPlayer, { getScoreSaberPlayerFromToken } from "@ssr/common/types/player/impl/scoresaber-player"; import { Config } from "../common/config"; +import ky from "ky"; +import { createCanvas, loadImage } from "canvas"; +import { extractColors } from "extract-colors"; const cache = new NodeCache({ stdTTL: 60 * 60, checkperiod: 120 }); const imageOptions = { width: 1200, height: 630 }; export class ImageService { + /** + * Gets the average color of an image + * + * @param src the image url + * @returns the average color + * @private + */ + public static async getAverageImageColor(src: string): Promise<{ color: string } | undefined> { + src = decodeURIComponent(src); + + return await this.fetchWithCache<{ color: string }>(`average_color-${src}`, async () => { + try { + const response = await ky.get(src); + if (response.status !== 200) { + throw new Error(`Failed to fetch image: ${src}`); + } + + const imageBuffer = await response.arrayBuffer(); + + // Create an image from the buffer using canvas + const img = await loadImage(Buffer.from(imageBuffer)); + const canvas = createCanvas(img.width, img.height); + const ctx = canvas.getContext("2d"); + + // Draw the image onto the canvas + ctx.drawImage(img, 0, 0); + + // Get the pixel data from the canvas + const imageData = ctx.getImageData(0, 0, img.width, img.height); + const { data, width, height } = imageData; + + // Extract the colors + const color = await extractColors({ data, width, height }); + return { + color: color[2].hex, + }; + } catch (error) { + return { + color: "#fff", + }; + } + }); + } + /** * Fetches data with caching. * diff --git a/projects/backend/src/service/score.service.ts b/projects/backend/src/service/score.service.ts new file mode 100644 index 0000000..ce6fbba --- /dev/null +++ b/projects/backend/src/service/score.service.ts @@ -0,0 +1,36 @@ +import ScoreSaberPlayerScoreToken from "@ssr/common/types/token/scoresaber/score-saber-player-score-token"; +// @ts-ignore +import { MessageBuilder, Webhook } from "discord-webhook-node"; +import { Config } from "../common/config"; +import { formatPp } from "@ssr/common/utils/number-utils"; + +export class ScoreService { + public static async notifyNumberOne(playerScore: ScoreSaberPlayerScoreToken) { + const { score, leaderboard } = playerScore; + const player = score.leaderboardPlayerInfo; + + // Not ranked + if (leaderboard.stars <= 0) { + return; + } + // Not #1 rank + if (score.rank !== 1) { + return; + } + + const hook = new Webhook({ + url: Config.numberOneWebhook, + }); + hook.setUsername("Number One Feed"); + const embed = new MessageBuilder(); + embed.setTitle(`${player.name} set a #${score.rank} on ${leaderboard.songName} ${leaderboard.songSubName}`); + embed.setDescription(` + **Player:** https://ssr.fascinated.cc/player/${player.id} + **Leaderboard:** https://ssr.fascinated.cc/leaderboard/${leaderboard.id} + **PP:** ${formatPp(score.pp)} + `); + embed.setThumbnail(leaderboard.coverImage); + embed.setColor("#00ff00"); + await hook.send(embed); + } +} diff --git a/projects/website/src/app/(pages)/leaderboard/[...slug]/page.tsx b/projects/website/src/app/(pages)/leaderboard/[...slug]/page.tsx index fbe76a8..b8a10a8 100644 --- a/projects/website/src/app/(pages)/leaderboard/[...slug]/page.tsx +++ b/projects/website/src/app/(pages)/leaderboard/[...slug]/page.tsx @@ -103,14 +103,8 @@ export async function generateViewport(props: Props): Promise { } const color = await getAverageColor(leaderboard.coverImage); - if (color === undefined) { - return { - themeColor: Colors.primary, - }; - } - return { - themeColor: color?.hex, + themeColor: color.hex, }; } diff --git a/projects/website/src/app/(pages)/player/[...slug]/page.tsx b/projects/website/src/app/(pages)/player/[...slug]/page.tsx index ec33d48..8b20438 100644 --- a/projects/website/src/app/(pages)/player/[...slug]/page.tsx +++ b/projects/website/src/app/(pages)/player/[...slug]/page.tsx @@ -117,14 +117,8 @@ export async function generateViewport(props: Props): Promise { } const color = await getAverageColor(player.avatar); - if (color === undefined) { - return { - themeColor: Colors.primary, - }; - } - return { - themeColor: color?.hex, + themeColor: color.hex, }; } diff --git a/projects/website/src/common/image-utils.ts b/projects/website/src/common/image-utils.ts index 19060c0..46ee234 100644 --- a/projects/website/src/common/image-utils.ts +++ b/projects/website/src/common/image-utils.ts @@ -1,4 +1,6 @@ import { config } from "../../config"; +import ky from "ky"; +import { Colors } from "@/common/colors"; /** * Proxies all non-localhost images to make them load faster. @@ -17,7 +19,11 @@ export function getImageUrl(originalUrl: string) { * @returns the average color */ export const getAverageColor = async (src: string) => { - return { - hex: "#fff", - }; + try { + return await ky.get<{ hex: string }>(`${config.siteApi}/image/averagecolor/${encodeURIComponent(src)}`).json(); + } catch { + return { + hex: Colors.primary, + }; + } };