re-add per page/leaderboard embed colors
This commit is contained in:
parent
7f42a27d8f
commit
ff9408fb8c
@ -7,8 +7,9 @@ metadata:
|
|||||||
namespace: public-services
|
namespace: public-services
|
||||||
spec:
|
spec:
|
||||||
encryptedData:
|
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=
|
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=
|
||||||
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
|
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:
|
template:
|
||||||
metadata:
|
metadata:
|
||||||
creationTimestamp: null
|
creationTimestamp: null
|
||||||
|
BIN
bun.lockb
BIN
bun.lockb
Binary file not shown.
@ -1,5 +1,18 @@
|
|||||||
FROM oven/bun:1.1.30-alpine AS base
|
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
|
# Install dependencies
|
||||||
FROM base AS depends
|
FROM base AS depends
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
@ -24,4 +37,4 @@ RUN bun --filter '@ssr/common' build
|
|||||||
# Copy the backend project
|
# Copy the backend project
|
||||||
COPY --from=depends /app/projects/backend ./projects/backend
|
COPY --from=depends /app/projects/backend ./projects/backend
|
||||||
|
|
||||||
CMD ["bun", "run", "--filter", "backend", "start"]
|
CMD ["bun", "run", "--filter", "backend", "start"]
|
||||||
|
@ -15,12 +15,14 @@
|
|||||||
"@tqman/nice-logger": "^1.0.1",
|
"@tqman/nice-logger": "^1.0.1",
|
||||||
"@typegoose/typegoose": "^12.8.0",
|
"@typegoose/typegoose": "^12.8.0",
|
||||||
"@vercel/og": "^0.6.3",
|
"@vercel/og": "^0.6.3",
|
||||||
|
"canvas": "^3.0.0-rc2",
|
||||||
"discord-webhook-node": "^1.1.8",
|
"discord-webhook-node": "^1.1.8",
|
||||||
"elysia": "latest",
|
"elysia": "latest",
|
||||||
"elysia-autoroutes": "^0.5.0",
|
"elysia-autoroutes": "^0.5.0",
|
||||||
"elysia-decorators": "^1.0.2",
|
"elysia-decorators": "^1.0.2",
|
||||||
"elysia-helmet": "^2.0.0",
|
"elysia-helmet": "^2.0.0",
|
||||||
"elysia-rate-limit": "^4.1.0",
|
"elysia-rate-limit": "^4.1.0",
|
||||||
|
"extract-colors": "^4.1.0",
|
||||||
"ky": "^1.7.2",
|
"ky": "^1.7.2",
|
||||||
"mongoose": "^8.7.0",
|
"mongoose": "^8.7.0",
|
||||||
"node-cache": "^5.1.2",
|
"node-cache": "^5.1.2",
|
||||||
|
@ -2,4 +2,5 @@ export const Config = {
|
|||||||
mongoUri: process.env.MONGO_URI,
|
mongoUri: process.env.MONGO_URI,
|
||||||
apiUrl: process.env.API_URL || "https://ssr.fascinated.cc/api",
|
apiUrl: process.env.API_URL || "https://ssr.fascinated.cc/api",
|
||||||
trackedPlayerWebhook: process.env.TRACKED_PLAYERS_WEBHOOK,
|
trackedPlayerWebhook: process.env.TRACKED_PLAYERS_WEBHOOK,
|
||||||
|
numberOneWebhook: process.env.NUMBER_ONE_WEBHOOK,
|
||||||
};
|
};
|
||||||
|
@ -4,6 +4,16 @@ import { ImageService } from "../service/image.service";
|
|||||||
|
|
||||||
@Controller("/image")
|
@Controller("/image")
|
||||||
export default class ImageController {
|
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", {
|
@Get("/player/:id", {
|
||||||
config: {},
|
config: {},
|
||||||
params: t.Object({
|
params: t.Object({
|
||||||
|
@ -21,6 +21,10 @@ import { delay } from "@ssr/common/utils/utils";
|
|||||||
import { connectScoreSaberWebSocket } from "@ssr/common/websocket/scoresaber-websocket";
|
import { connectScoreSaberWebSocket } from "@ssr/common/websocket/scoresaber-websocket";
|
||||||
import ImageController from "./controller/image.controller";
|
import ImageController from "./controller/image.controller";
|
||||||
import ReplayController from "./controller/replay.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
|
// Load .env file
|
||||||
dotenv.config({
|
dotenv.config({
|
||||||
@ -33,8 +37,9 @@ await mongoose.connect(Config.mongoUri!); // Connect to MongoDB
|
|||||||
setLogLevel("DEBUG");
|
setLogLevel("DEBUG");
|
||||||
|
|
||||||
connectScoreSaberWebSocket({
|
connectScoreSaberWebSocket({
|
||||||
onScore: async score => {
|
onScore: async playerScore => {
|
||||||
await PlayerService.trackScore(score);
|
await PlayerService.trackScore(playerScore);
|
||||||
|
await ScoreService.notifyNumberOne(playerScore);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -9,11 +9,58 @@ import NodeCache from "node-cache";
|
|||||||
import ScoreSaberLeaderboardToken from "@ssr/common/types/token/scoresaber/score-saber-leaderboard-token";
|
import ScoreSaberLeaderboardToken from "@ssr/common/types/token/scoresaber/score-saber-leaderboard-token";
|
||||||
import ScoreSaberPlayer, { getScoreSaberPlayerFromToken } from "@ssr/common/types/player/impl/scoresaber-player";
|
import ScoreSaberPlayer, { getScoreSaberPlayerFromToken } from "@ssr/common/types/player/impl/scoresaber-player";
|
||||||
import { Config } from "../common/config";
|
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 cache = new NodeCache({ stdTTL: 60 * 60, checkperiod: 120 });
|
||||||
const imageOptions = { width: 1200, height: 630 };
|
const imageOptions = { width: 1200, height: 630 };
|
||||||
|
|
||||||
export class ImageService {
|
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.
|
* Fetches data with caching.
|
||||||
*
|
*
|
||||||
|
36
projects/backend/src/service/score.service.ts
Normal file
36
projects/backend/src/service/score.service.ts
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -103,14 +103,8 @@ export async function generateViewport(props: Props): Promise<Viewport> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const color = await getAverageColor(leaderboard.coverImage);
|
const color = await getAverageColor(leaderboard.coverImage);
|
||||||
if (color === undefined) {
|
|
||||||
return {
|
|
||||||
themeColor: Colors.primary,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
themeColor: color?.hex,
|
themeColor: color.hex,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,14 +117,8 @@ export async function generateViewport(props: Props): Promise<Viewport> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const color = await getAverageColor(player.avatar);
|
const color = await getAverageColor(player.avatar);
|
||||||
if (color === undefined) {
|
|
||||||
return {
|
|
||||||
themeColor: Colors.primary,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
themeColor: color?.hex,
|
themeColor: color.hex,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import { config } from "../../config";
|
import { config } from "../../config";
|
||||||
|
import ky from "ky";
|
||||||
|
import { Colors } from "@/common/colors";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Proxies all non-localhost images to make them load faster.
|
* Proxies all non-localhost images to make them load faster.
|
||||||
@ -17,7 +19,11 @@ export function getImageUrl(originalUrl: string) {
|
|||||||
* @returns the average color
|
* @returns the average color
|
||||||
*/
|
*/
|
||||||
export const getAverageColor = async (src: string) => {
|
export const getAverageColor = async (src: string) => {
|
||||||
return {
|
try {
|
||||||
hex: "#fff",
|
return await ky.get<{ hex: string }>(`${config.siteApi}/image/averagecolor/${encodeURIComponent(src)}`).json();
|
||||||
};
|
} catch {
|
||||||
|
return {
|
||||||
|
hex: Colors.primary,
|
||||||
|
};
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
Reference in New Issue
Block a user