Compare commits

..

2 Commits

Author SHA1 Message Date
7f5587546c cleanup and track friends data (if not being already tracked)
Some checks failed
Deploy Backend / deploy (push) Successful in 2m48s
Deploy Website / deploy (push) Has been cancelled
2024-10-17 07:12:03 +01:00
64f918c325 fix env vars 2024-10-17 06:53:31 +01:00
20 changed files with 70 additions and 39 deletions

@ -31,4 +31,14 @@ spec:
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: ssr-backend-secret name: ssr-backend-secret
key: MONGO_URI key: MONGO_URI
- name: NUMBER_ONE_WEBHOOK
valueFrom:
secretKeyRef:
name: ssr-backend-secret
key: NUMBER_ONE_WEBHOOK
- name: TRACKED_PLAYERS_WEBHOOK
valueFrom:
secretKeyRef:
name: ssr-backend-secret
key: TRACKED_PLAYERS_WEBHOOK

@ -1,6 +0,0 @@
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,
};

@ -10,7 +10,6 @@ import { etag } from "@bogeychan/elysia-etag";
import AppController from "./controller/app.controller"; import AppController from "./controller/app.controller";
import * as dotenv from "@dotenvx/dotenvx"; import * as dotenv from "@dotenvx/dotenvx";
import mongoose from "mongoose"; import mongoose from "mongoose";
import { Config } from "./common/config";
import { setLogLevel } from "@typegoose/typegoose"; import { setLogLevel } from "@typegoose/typegoose";
import PlayerController from "./controller/player.controller"; import PlayerController from "./controller/player.controller";
import { PlayerService } from "./service/player.service"; import { PlayerService } from "./service/player.service";
@ -22,6 +21,7 @@ import { connectScoreSaberWebSocket } from "@ssr/common/websocket/scoresaber-web
import ImageController from "./controller/image.controller"; import ImageController from "./controller/image.controller";
import ReplayController from "./controller/replay.controller"; import ReplayController from "./controller/replay.controller";
import { ScoreService } from "./service/score.service"; import { ScoreService } from "./service/score.service";
import { Config } from "@ssr/common/config";
// Load .env file // Load .env file
dotenv.config({ dotenv.config({

@ -8,9 +8,9 @@ import { GlobeIcon } from "../../components/globe-icon";
import NodeCache from "node-cache"; 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 { Jimp } from "jimp"; import { Jimp } from "jimp";
import { extractColors } from "extract-colors"; import { extractColors } from "extract-colors";
import { Config } from "@ssr/common/config";
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 };

@ -8,9 +8,9 @@ import ScoreSaberPlayerScoreToken from "@ssr/common/types/token/scoresaber/score
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
import { MessageBuilder, Webhook } from "discord-webhook-node"; import { MessageBuilder, Webhook } from "discord-webhook-node";
import { Config } from "../common/config";
import { formatPp } from "@ssr/common/utils/number-utils"; import { formatPp } from "@ssr/common/utils/number-utils";
import { isProduction } from "@ssr/common/utils/utils"; import { isProduction } from "@ssr/common/utils/utils";
import { Config } from "@ssr/common/config";
export class PlayerService { export class PlayerService {
/** /**

@ -2,9 +2,9 @@ import ScoreSaberPlayerScoreToken from "@ssr/common/types/token/scoresaber/score
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
import { MessageBuilder, Webhook } from "discord-webhook-node"; import { MessageBuilder, Webhook } from "discord-webhook-node";
import { Config } from "../common/config";
import { formatPp } from "@ssr/common/utils/number-utils"; import { formatPp } from "@ssr/common/utils/number-utils";
import { isProduction } from "@ssr/common/utils/utils"; import { isProduction } from "@ssr/common/utils/utils";
import { Config } from "@ssr/common/config";
export class ScoreService { export class ScoreService {
/** /**

@ -0,0 +1,14 @@
export const Config = {
/**
* All projects
*/
websiteUrl: process.env.NEXT_PUBLIC_SITE_URL || "https://ssr.fascinated.cc",
apiUrl: process.env.NEXT_PUBLIC_SITE_API || "https://ssr.fascinated.cc/api",
/**
* Backend
*/
trackedPlayerWebhook: process.env.TRACKED_PLAYERS_WEBHOOK,
numberOneWebhook: process.env.NUMBER_ONE_WEBHOOK,
mongoUri: process.env.MONGO_URI,
} as const;

@ -4,6 +4,7 @@ import { PlayerHistory } from "../player-history";
import ScoreSaberPlayerToken from "../../token/scoresaber/score-saber-player-token"; import ScoreSaberPlayerToken from "../../token/scoresaber/score-saber-player-token";
import { formatDateMinimal, getDaysAgoDate, getMidnightAlignedDate } from "../../../utils/time-utils"; import { formatDateMinimal, getDaysAgoDate, getMidnightAlignedDate } from "../../../utils/time-utils";
import { getPageFromRank } from "../../../utils/utils"; import { getPageFromRank } from "../../../utils/utils";
import { Config } from "../../../config";
/** /**
* A ScoreSaber player. * A ScoreSaber player.
@ -75,12 +76,10 @@ export default interface ScoreSaberPlayer extends Player {
* Gets the ScoreSaber Player from an {@link ScoreSaberPlayerToken}. * Gets the ScoreSaber Player from an {@link ScoreSaberPlayerToken}.
* *
* @param token the player token * @param token the player token
* @param apiUrl the api url for SSR
* @param playerIdCookie the id of the claimed player * @param playerIdCookie the id of the claimed player
*/ */
export async function getScoreSaberPlayerFromToken( export async function getScoreSaberPlayerFromToken(
token: ScoreSaberPlayerToken, token: ScoreSaberPlayerToken,
apiUrl: string,
playerIdCookie?: string playerIdCookie?: string
): Promise<ScoreSaberPlayer> { ): Promise<ScoreSaberPlayer> {
const bio: ScoreSaberBio = { const bio: ScoreSaberBio = {
@ -105,7 +104,7 @@ export async function getScoreSaberPlayerFromToken(
.get<{ .get<{
statistics: { [key: string]: PlayerHistory }; statistics: { [key: string]: PlayerHistory };
}>( }>(
`${apiUrl}/player/history/${token.id}${playerIdCookie && playerIdCookie == token.id ? "?createIfMissing=true" : ""}` `${Config.apiUrl}/player/history/${token.id}${playerIdCookie && playerIdCookie == token.id ? "?createIfMissing=true" : ""}`
) )
.json(); .json();
if (history) { if (history) {

@ -1,4 +1,6 @@
import { PlayerHistory } from "../types/player/player-history"; import { PlayerHistory } from "../types/player/player-history";
import { kyFetch } from "./utils";
import { Config } from "../config";
/** /**
* Sorts the player history based on date, * Sorts the player history based on date,
@ -11,3 +13,12 @@ export function sortPlayerHistory(history: Map<string, PlayerHistory>) {
(a, b) => Date.parse(b[0]) - Date.parse(a[0]) // Sort in descending order (a, b) => Date.parse(b[0]) - Date.parse(a[0]) // Sort in descending order
); );
} }
/**
* Ensure the player is being tracked.
*
* @param id the player id
*/
export async function trackPlayer(id: string) {
await kyFetch(`${Config.apiUrl}/player/history/${id}?createIfMissing=true`);
}

@ -1,4 +0,0 @@
export const config = {
siteUrl: process.env.NEXT_PUBLIC_SITE_URL || "https://ssr.fascinated.cc",
siteApi: process.env.NEXT_PUBLIC_SITE_API || "https://ssr.fascinated.cc/api",
};

@ -7,7 +7,7 @@ import { scoresaberService } from "@ssr/common/service/impl/scoresaber";
import ScoreSaberLeaderboardScoresPageToken from "@ssr/common/types/token/scoresaber/score-saber-leaderboard-scores-page-token"; import ScoreSaberLeaderboardScoresPageToken from "@ssr/common/types/token/scoresaber/score-saber-leaderboard-scores-page-token";
import NodeCache from "node-cache"; 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 { config } from "../../../../../config"; import { Config } from "@ssr/common/config";
const UNKNOWN_LEADERBOARD = { const UNKNOWN_LEADERBOARD = {
title: "ScoreSaber Reloaded - Unknown Leaderboard", title: "ScoreSaber Reloaded - Unknown Leaderboard",
@ -84,7 +84,7 @@ export async function generateMetadata(props: Props): Promise<Metadata> {
description: `View the scores for ${leaderboard.songName} by ${leaderboard.songAuthorName}!`, description: `View the scores for ${leaderboard.songName} by ${leaderboard.songAuthorName}!`,
images: [ images: [
{ {
url: `${config.siteApi}/image/leaderboard/${leaderboard.id}`, url: `${Config.apiUrl}/image/leaderboard/${leaderboard.id}`,
}, },
], ],
}, },

@ -1,14 +1,14 @@
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import Link from "next/link"; import Link from "next/link";
import { config } from "../../../config";
import { AppStatistics } from "@ssr/common/types/backend/app-statistics"; import { AppStatistics } from "@ssr/common/types/backend/app-statistics";
import Statistic from "@/components/home/statistic"; import Statistic from "@/components/home/statistic";
import { kyFetch } from "@ssr/common/utils/utils"; import { kyFetch } from "@ssr/common/utils/utils";
import { Config } from "@ssr/common/config";
export const dynamic = "force-dynamic"; // Always generate the page on load export const dynamic = "force-dynamic"; // Always generate the page on load
export default async function HomePage() { export default async function HomePage() {
const statistics = await kyFetch<AppStatistics>(config.siteApi + "/statistics"); const statistics = await kyFetch<AppStatistics>(Config.apiUrl + "/statistics");
return ( return (
<main className="flex flex-col items-center w-full gap-6 text-center"> <main className="flex flex-col items-center w-full gap-6 text-center">

@ -7,9 +7,9 @@ import { ScoreSort } from "@ssr/common/types/score/score-sort";
import { scoresaberService } from "@ssr/common/service/impl/scoresaber"; import { scoresaberService } from "@ssr/common/service/impl/scoresaber";
import ScoreSaberPlayerScoresPageToken from "@ssr/common/types/token/scoresaber/score-saber-player-scores-page-token"; import ScoreSaberPlayerScoresPageToken from "@ssr/common/types/token/scoresaber/score-saber-player-scores-page-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 "../../../../../config";
import NodeCache from "node-cache"; import NodeCache from "node-cache";
import { getCookieValue } from "@ssr/common/utils/cookie-utils"; import { getCookieValue } from "@ssr/common/utils/cookie-utils";
import { Config } from "@ssr/common/config";
const UNKNOWN_PLAYER = { const UNKNOWN_PLAYER = {
title: "ScoreSaber Reloaded - Unknown Player", title: "ScoreSaber Reloaded - Unknown Player",
@ -55,8 +55,7 @@ const getPlayerData = async ({ params }: Props, fetchScores: boolean = true): Pr
} }
const playerToken = await scoresaberService.lookupPlayer(id); const playerToken = await scoresaberService.lookupPlayer(id);
const player = const player = playerToken && (await getScoreSaberPlayerFromToken(playerToken, await getCookieValue("playerId")));
playerToken && (await getScoreSaberPlayerFromToken(playerToken, config.siteApi, await getCookieValue("playerId")));
let scores: ScoreSaberPlayerScoresPageToken | undefined; let scores: ScoreSaberPlayerScoresPageToken | undefined;
if (fetchScores) { if (fetchScores) {
scores = await scoresaberService.lookupPlayerScores({ scores = await scoresaberService.lookupPlayerScores({
@ -98,7 +97,7 @@ export async function generateMetadata(props: Props): Promise<Metadata> {
description: `Click here to view the scores for ${player.name}!`, description: `Click here to view the scores for ${player.name}!`,
images: [ images: [
{ {
url: `${config.siteApi}/image/player/${player.id}`, url: `${Config.apiUrl}/image/player/${player.id}`,
}, },
], ],
}, },

@ -5,6 +5,7 @@ import { Friend } from "@/common/database/types/friends";
import ScoreSaberPlayerToken from "@ssr/common/types/token/scoresaber/score-saber-player-token"; import ScoreSaberPlayerToken from "@ssr/common/types/token/scoresaber/score-saber-player-token";
import { scoresaberService } from "@ssr/common/service/impl/scoresaber"; import { scoresaberService } from "@ssr/common/service/impl/scoresaber";
import { setCookieValue } from "@ssr/common/utils/cookie-utils"; import { setCookieValue } from "@ssr/common/utils/cookie-utils";
import { trackPlayer } from "@ssr/common/utils/player-utils";
const SETTINGS_ID = "SSR"; // DO NOT CHANGE const SETTINGS_ID = "SSR"; // DO NOT CHANGE
@ -114,7 +115,12 @@ export default class Database extends Dexie {
const friends = await this.friends.toArray(); const friends = await this.friends.toArray();
const players = await Promise.all( const players = await Promise.all(
friends.map(async ({ id }) => { friends.map(async ({ id }) => {
return await scoresaberService.lookupPlayer(id, true); const token = await scoresaberService.lookupPlayer(id, true);
if (token == undefined) {
return undefined;
}
await trackPlayer(id); // Track the player
return token;
}) })
); );
return players.filter(player => player !== undefined) as ScoreSaberPlayerToken[]; return players.filter(player => player !== undefined) as ScoreSaberPlayerToken[];

@ -1,6 +1,6 @@
import { config } from "../../config";
import ky from "ky"; import ky from "ky";
import { Colors } from "@/common/colors"; import { Colors } from "@/common/colors";
import { Config } from "@ssr/common/config";
/** /**
* Proxies all non-localhost images to make them load faster. * Proxies all non-localhost images to make them load faster.
@ -9,7 +9,7 @@ import { Colors } from "@/common/colors";
* @returns the new image url * @returns the new image url
*/ */
export function getImageUrl(originalUrl: string) { export function getImageUrl(originalUrl: string) {
return `${!config.siteUrl.includes("localhost") ? "https://img.fascinated.cc/upload/q_70/" : ""}${originalUrl}`; return `${!Config.websiteUrl.includes("localhost") ? "https://img.fascinated.cc/upload/q_70/" : ""}${originalUrl}`;
} }
/** /**
@ -20,7 +20,7 @@ export function getImageUrl(originalUrl: string) {
*/ */
export const getAverageColor = async (src: string) => { export const getAverageColor = async (src: string) => {
try { try {
return await ky.get<{ color: string }>(`${config.siteApi}/image/averagecolor/${encodeURIComponent(src)}`).json(); return await ky.get<{ color: string }>(`${Config.apiUrl}/image/averagecolor/${encodeURIComponent(src)}`).json();
} catch { } catch {
return { return {
color: Colors.primary, color: Colors.primary,

@ -3,9 +3,9 @@
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { getApiHealth } from "@ssr/common/utils/api-utils"; import { getApiHealth } from "@ssr/common/utils/api-utils";
import { config } from "../../../config";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
import { useIsFirstRender } from "@uidotdev/usehooks"; import { useIsFirstRender } from "@uidotdev/usehooks";
import { Config } from "@ssr/common/config";
export function ApiHealth() { export function ApiHealth() {
const { toast } = useToast(); const { toast } = useToast();
@ -16,7 +16,7 @@ export function ApiHealth() {
useQuery({ useQuery({
queryKey: ["api-health"], queryKey: ["api-health"],
queryFn: async () => { queryFn: async () => {
const status = (await getApiHealth(config.siteApi + "/health")).online; const status = (await getApiHealth(Config.apiUrl + "/health")).online;
setOnline(status); setOnline(status);
return status; return status;
}, },

@ -1,9 +1,9 @@
"use client"; "use client";
import { useLiveQuery } from "dexie-react-hooks"; import { useLiveQuery } from "dexie-react-hooks";
import { config } from "../../config";
import { getImageUrl } from "@/common/image-utils"; import { getImageUrl } from "@/common/image-utils";
import useDatabase from "../hooks/use-database"; import useDatabase from "../hooks/use-database";
import { Config } from "@ssr/common/config";
export default function BackgroundCover() { export default function BackgroundCover() {
const database = useDatabase(); const database = useDatabase();
@ -22,7 +22,7 @@ export default function BackgroundCover() {
backgroundCover = backgroundCover.substring(1); backgroundCover = backgroundCover.substring(1);
} }
if (prependWebsiteUrl) { if (prependWebsiteUrl) {
backgroundCover = config.siteUrl + "/" + backgroundCover; backgroundCover = Config.websiteUrl + "/" + backgroundCover;
} }
// Static background color // Static background color

@ -7,6 +7,7 @@ import Tooltip from "../tooltip";
import { Button } from "../ui/button"; import { Button } from "../ui/button";
import { PersonIcon } from "@radix-ui/react-icons"; import { PersonIcon } from "@radix-ui/react-icons";
import ScoreSaberPlayer from "@ssr/common/types/player/impl/scoresaber-player"; import ScoreSaberPlayer from "@ssr/common/types/player/impl/scoresaber-player";
import { trackPlayer } from "@ssr/common/utils/player-utils";
type Props = { type Props = {
/** /**
@ -27,6 +28,7 @@ export default function AddFriend({ player }: Props) {
* Adds this player as a friend * Adds this player as a friend
*/ */
async function addFriend() { async function addFriend() {
await trackPlayer(id);
await database.addFriend(id); await database.addFriend(id);
toast({ toast({
title: "Friend Added", title: "Friend Added",

@ -14,7 +14,6 @@ import ScoreSaberPlayer, { getScoreSaberPlayerFromToken } from "@ssr/common/type
import ScoreSaberPlayerScoresPageToken from "@ssr/common/types/token/scoresaber/score-saber-player-scores-page-token"; import ScoreSaberPlayerScoresPageToken from "@ssr/common/types/token/scoresaber/score-saber-player-scores-page-token";
import { ScoreSort } from "@ssr/common/types/score/score-sort"; import { ScoreSort } from "@ssr/common/types/score/score-sort";
import { scoresaberService } from "@ssr/common/service/impl/scoresaber"; import { scoresaberService } from "@ssr/common/service/impl/scoresaber";
import { config } from "../../../config";
import useDatabase from "@/hooks/use-database"; import useDatabase from "@/hooks/use-database";
import { useLiveQuery } from "dexie-react-hooks"; import { useLiveQuery } from "dexie-react-hooks";
@ -40,16 +39,17 @@ export default function PlayerData({
const isMiniRankingsVisible = useIsVisible(miniRankingsRef); const isMiniRankingsVisible = useIsVisible(miniRankingsRef);
const database = useDatabase(); const database = useDatabase();
const settings = useLiveQuery(() => database.getSettings()); const settings = useLiveQuery(() => database.getSettings());
const isFriend = useLiveQuery(() => database.isFriend(initialPlayerData.id));
let player = initialPlayerData; let player = initialPlayerData;
const { data, isLoading, isError } = useQuery({ const { data, isLoading, isError } = useQuery({
queryKey: ["playerData", player.id, settings?.playerId], queryKey: ["playerData", player.id, settings?.playerId, isFriend],
queryFn: async (): Promise<ScoreSaberPlayer | undefined> => { queryFn: async (): Promise<ScoreSaberPlayer | undefined> => {
const playerResponse = await scoresaberService.lookupPlayer(player.id); const playerResponse = await scoresaberService.lookupPlayer(player.id);
if (playerResponse == undefined) { if (playerResponse == undefined) {
return undefined; return undefined;
} }
return await getScoreSaberPlayerFromToken(playerResponse, config.siteApi, settings?.playerId); return await getScoreSaberPlayerFromToken(playerResponse, settings?.playerId);
}, },
refetchInterval: REFRESH_INTERVAL, refetchInterval: REFRESH_INTERVAL,
refetchIntervalInBackground: false, refetchIntervalInBackground: false,

@ -2,12 +2,12 @@
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import ky from "ky"; import ky from "ky";
import { config } from "../../../config";
import Tooltip from "@/components/tooltip"; import Tooltip from "@/components/tooltip";
import { InformationCircleIcon } from "@heroicons/react/16/solid"; import { InformationCircleIcon } from "@heroicons/react/16/solid";
import { formatNumberWithCommas } from "@ssr/common/utils/number-utils"; import { formatNumberWithCommas } from "@ssr/common/utils/number-utils";
import { PlayerTrackedSince } from "@ssr/common/types/player/player-tracked-since"; import { PlayerTrackedSince } from "@ssr/common/types/player/player-tracked-since";
import ScoreSaberPlayer from "@ssr/common/types/player/impl/scoresaber-player"; import ScoreSaberPlayer from "@ssr/common/types/player/impl/scoresaber-player";
import { Config } from "@ssr/common/config";
type Props = { type Props = {
player: ScoreSaberPlayer; player: ScoreSaberPlayer;
@ -16,7 +16,7 @@ type Props = {
export default function PlayerTrackedStatus({ player }: Props) { export default function PlayerTrackedStatus({ player }: Props) {
const { data, isLoading, isError } = useQuery({ const { data, isLoading, isError } = useQuery({
queryKey: ["playerIsBeingTracked", player.id], queryKey: ["playerIsBeingTracked", player.id],
queryFn: () => ky.get<PlayerTrackedSince>(`${config.siteApi}/player/tracked/${player.id}`).json(), queryFn: () => ky.get<PlayerTrackedSince>(`${Config.apiUrl}/player/tracked/${player.id}`).json(),
}); });
if (isLoading || isError || !data?.tracked) { if (isLoading || isError || !data?.tracked) {