This repository has been archived on 2024-10-29. You can view files and clone it, but cannot push or open issues or pull requests.
scoresaber-reloadedv3/projects/backend/src/index.ts

196 lines
5.5 KiB
TypeScript
Raw Normal View History

2024-10-08 14:32:02 +00:00
import { Elysia } from "elysia";
import cors from "@elysiajs/cors";
import { decorators } from "elysia-decorators";
import { logger } from "@tqman/nice-logger";
2024-10-09 00:17:00 +00:00
import { swagger } from "@elysiajs/swagger";
import { helmet } from "elysia-helmet";
import { etag } from "@bogeychan/elysia-etag";
2024-10-08 15:36:52 +00:00
import AppController from "./controller/app.controller";
2024-10-09 00:17:00 +00:00
import * as dotenv from "@dotenvx/dotenvx";
import mongoose from "mongoose";
import PlayerController from "./controller/player.controller";
import { PlayerService } from "./service/player.service";
2024-10-10 00:22:09 +00:00
import { cron } from "@elysiajs/cron";
import { scoresaberService } from "@ssr/common/service/impl/scoresaber";
import { delay, isProduction } from "@ssr/common/utils/utils";
2024-10-15 18:31:50 +00:00
import ImageController from "./controller/image.controller";
import { ScoreService } from "./service/score.service";
import { Config } from "@ssr/common/config";
import { PlayerDocument, PlayerModel } from "@ssr/common/model/player";
import ScoresController from "./controller/scores.controller";
import LeaderboardController from "./controller/leaderboard.controller";
2024-10-19 06:15:28 +00:00
import { getAppVersion } from "./common/app.util";
import { connectScoresaberWebsocket } from "@ssr/common/websocket/scoresaber-websocket";
import { connectBeatLeaderWebsocket } from "@ssr/common/websocket/beatleader-websocket";
2024-10-22 16:32:09 +00:00
import { DiscordChannels, initDiscordBot, logToChannel } from "./bot/bot";
import { EmbedBuilder } from "discord.js";
2024-10-08 14:32:02 +00:00
2024-10-09 00:17:00 +00:00
// Load .env file
dotenv.config({
2024-10-13 00:14:51 +00:00
logLevel: (await Bun.file(".env").exists()) ? "success" : "warn",
2024-10-09 00:17:00 +00:00
path: ".env",
override: true,
});
// Connect to Mongo
2024-10-09 00:17:00 +00:00
await mongoose.connect(Config.mongoUri!); // Connect to MongoDB
2024-10-08 14:32:02 +00:00
// Connect to websockets
connectScoresaberWebsocket({
onScore: async score => {
await ScoreService.trackScoreSaberScore(score);
await ScoreService.notifyNumberOne(score);
2024-10-15 03:09:47 +00:00
},
2024-10-22 16:32:09 +00:00
onDisconnect: async error => {
await logToChannel(
DiscordChannels.backendLogs,
new EmbedBuilder().setDescription(`ScoreSaber websocket disconnected: ${JSON.stringify(error)}`)
);
},
});
connectBeatLeaderWebsocket({
onScore: async score => {
await ScoreService.trackBeatLeaderScore(score);
},
2024-10-22 16:32:09 +00:00
onDisconnect: async error => {
await logToChannel(
DiscordChannels.backendLogs,
new EmbedBuilder().setDescription(`BeatLeader websocket disconnected: ${JSON.stringify(error)}`)
);
},
2024-10-15 03:09:47 +00:00
});
export const app = new Elysia();
2024-10-10 00:22:09 +00:00
app.use(
cron({
name: "player-statistics-tracker-cron",
2024-10-13 00:14:51 +00:00
pattern: "1 0 * * *", // Every day at 00:01
timezone: "Europe/London", // UTC time
2024-10-10 00:22:09 +00:00
run: async () => {
2024-10-12 01:37:08 +00:00
const pages = 20; // top 1000 players
2024-10-12 01:36:44 +00:00
const cooldown = 60_000 / 250; // 250 requests per minute
2024-10-11 17:47:33 +00:00
let toTrack: PlayerDocument[] = await PlayerModel.find({});
const toRemoveIds: string[] = [];
// loop through pages to fetch the top players
console.log(`Fetching ${pages} pages of players from ScoreSaber...`);
for (let i = 0; i < pages; i++) {
const pageNumber = i + 1;
console.log(`Fetching page ${pageNumber}...`);
const page = await scoresaberService.lookupPlayers(pageNumber);
if (page === undefined) {
console.log(`Failed to fetch players on page ${pageNumber}, skipping page...`);
2024-10-11 17:48:04 +00:00
await delay(cooldown);
continue;
}
for (const player of page.players) {
2024-10-12 01:36:44 +00:00
const foundPlayer = await PlayerService.getPlayer(player.id, true, player);
await PlayerService.trackScoreSaberPlayer(foundPlayer, player);
toRemoveIds.push(foundPlayer.id);
}
2024-10-11 17:48:04 +00:00
await delay(cooldown);
}
console.log(`Finished tracking player statistics for ${pages} pages, found ${toRemoveIds.length} players.`);
// remove all players that have been tracked
toTrack = toTrack.filter(player => !toRemoveIds.includes(player.id));
console.log(`Tracking ${toTrack.length} player statistics...`);
for (const player of toTrack) {
2024-10-10 00:40:21 +00:00
await PlayerService.trackScoreSaberPlayer(player);
2024-10-11 17:47:33 +00:00
await delay(cooldown);
2024-10-10 00:22:09 +00:00
}
console.log("Finished tracking player statistics.");
},
})
);
2024-10-08 14:32:02 +00:00
/**
* Custom error handler
*/
app.onError({ as: "global" }, ({ code, error }) => {
// Return default error for type validation
if (code === "VALIDATION") {
return error.all;
}
2024-10-16 06:47:52 +00:00
const status = "status" in error ? error.status : undefined;
2024-10-08 14:32:02 +00:00
return {
...((status && { statusCode: status }) || { status: code }),
...(error.message != code && { message: error.message }),
timestamp: new Date().toISOString(),
};
});
2024-10-08 15:36:52 +00:00
/**
* Enable E-Tags
*/
app.use(etag());
2024-10-08 14:32:02 +00:00
/**
* Enable CORS
*/
app.use(cors());
/**
* Request logger
*/
app.use(
logger({
2024-10-16 10:27:04 +00:00
enabled: true,
2024-10-08 14:32:02 +00:00
mode: "combined",
})
);
2024-10-08 15:36:52 +00:00
/**
* Security settings
*/
2024-10-09 00:17:00 +00:00
app.use(
helmet({
hsts: false, // Disable HSTS
contentSecurityPolicy: false, // Disable CSP
dnsPrefetchControl: true, // Enable DNS prefetch
})
);
2024-10-08 15:36:52 +00:00
2024-10-08 14:32:02 +00:00
/**
* Controllers
*/
app.use(
decorators({
2024-10-19 16:51:24 +00:00
controllers: [AppController, PlayerController, ImageController, ScoresController, LeaderboardController],
2024-10-08 14:32:02 +00:00
})
);
2024-10-08 15:36:52 +00:00
/**
* Swagger Documentation
*/
2024-10-19 05:56:07 +00:00
app.use(
swagger({
2024-10-19 06:15:28 +00:00
documentation: {
info: {
title: "ScoreSaber Reloaded Documentation",
version: await getAppVersion(),
},
},
2024-10-23 15:19:29 +00:00
scalarConfig: {
servers: [
{
url: "https://ssr.fascinated.cc/api",
description: "Production server",
},
],
},
2024-10-19 05:56:07 +00:00
})
);
2024-10-08 15:36:52 +00:00
2024-10-08 14:32:02 +00:00
app.onStart(() => {
console.log("Listening on port http://localhost:8080");
if (isProduction()) {
initDiscordBot();
}
2024-10-08 14:32:02 +00:00
});
app.listen(8080);