From 45e092480824c74e5172a0e96dccab0856e14796 Mon Sep 17 00:00:00 2001 From: dvelo <52332868+DeveloLongScript@users.noreply.github.com> Date: Sat, 8 Mar 2025 21:07:51 -0600 Subject: [PATCH] fix(www): optimize statistics --- .../[server]/get.ts => lib/types/data.ts} | 47 +- apps/www/src/pages/api/inngest.ts | 160 +++---- .../pages/api/v0/account-linking/is-owned.ts | 57 --- .../src/pages/api/v0/achievements/[server].ts | 53 --- .../src/pages/api/v0/achievements/multiple.ts | 31 -- .../src/pages/api/v0/customization/getList.ts | 61 --- .../favorites/[server]/community-favorites.ts | 91 ---- .../api/v0/favorites/[server]/favorited.ts | 61 --- .../history/[server]/get-short-term-data.ts | 70 --- apps/www/src/pages/api/v1/server/bulk.ts | 428 ++++++++++++++++++ .../src/pages/api/v1/server/get/[server].ts | 329 ++++++++++++++ 11 files changed, 862 insertions(+), 526 deletions(-) rename apps/www/src/{pages/api/v0/customization/[server]/get.ts => lib/types/data.ts} (65%) delete mode 100644 apps/www/src/pages/api/v0/account-linking/is-owned.ts delete mode 100644 apps/www/src/pages/api/v0/achievements/[server].ts delete mode 100644 apps/www/src/pages/api/v0/achievements/multiple.ts delete mode 100644 apps/www/src/pages/api/v0/customization/getList.ts delete mode 100644 apps/www/src/pages/api/v0/favorites/[server]/community-favorites.ts delete mode 100644 apps/www/src/pages/api/v0/favorites/[server]/favorited.ts delete mode 100644 apps/www/src/pages/api/v0/history/[server]/get-short-term-data.ts create mode 100644 apps/www/src/pages/api/v1/server/bulk.ts create mode 100644 apps/www/src/pages/api/v1/server/get/[server].ts diff --git a/apps/www/src/pages/api/v0/customization/[server]/get.ts b/apps/www/src/lib/types/data.ts similarity index 65% rename from apps/www/src/pages/api/v0/customization/[server]/get.ts rename to apps/www/src/lib/types/data.ts index 93bcd89..f020bc9 100644 --- a/apps/www/src/pages/api/v0/customization/[server]/get.ts +++ b/apps/www/src/lib/types/data.ts @@ -28,26 +28,29 @@ * OTHER DEALINGS IN THE SOFTWARE. */ -import { NextApiRequest, NextApiResponse } from "next"; -import { MongoClient } from "mongodb"; -import { waitUntil } from "@vercel/functions"; +import { Achievement } from "./achievement"; -export default async function handler( - req: NextApiRequest, - res: NextApiResponse, -) { - const server = req.query.server as string; - const client = new MongoClient(process.env.MONGO_DB as string); - await client.connect(); - - const db = client.db(process.env.CUSTOM_MONGO_DB ?? "mhsf"); - const collection = db.collection("customization"); - - // Close the database, but don't close this - // serverless instance until it happens - waitUntil(client.close()); - - res.send({ results: await collection.findOne({ server }) }); - - client.close(); -} +export type MHSFData = { + favoriteData: { + favoritedByAccount: boolean | null; + favoriteNumber: number; + favoriteHistoricalData: { date: string; favorites: number }[]; + }; + customizationData: { + description: string | undefined; + banner: string | undefined; + discord: string | undefined; + colorScheme: string | undefined; + userProfilePicture: string | undefined; + isOwned: boolean; + isOwnedByUser: boolean; + }; + playerData: { + historically: { date: string; playerCount: number }[]; + max: number; + }; + achievements: { + historically: { _id: string; name: string; achievements: Achievement[] }[]; + currently: Achievement[]; + }; +}; diff --git a/apps/www/src/pages/api/inngest.ts b/apps/www/src/pages/api/inngest.ts index 8016501..a30f088 100644 --- a/apps/www/src/pages/api/inngest.ts +++ b/apps/www/src/pages/api/inngest.ts @@ -39,90 +39,90 @@ export const inngest = new Inngest({ id: "mhsf" }); // Create an API that serves zero (not zero, silly) functions export default serve({ - client: inngest, - functions: [ - inngest.createFunction( - { id: "report" }, - { event: "report-server" }, - async ({ event, step }) => { - // by the way, I bombed the Discord stuff - await createReportIssue( - event.data.server, - event.data.reason, - event.data.userId, - ); + client: inngest, + functions: [ + inngest.createFunction( + { id: "report" }, + { event: "report-server" }, + async ({ event, step }) => { + // by the way, I bombed the Discord stuff + await createReportIssue( + event.data.server, + event.data.reason, + event.data.userId + ); - return { event, body: "Done" }; - }, - ), - inngest.createFunction( - { id: "short-term-data" }, - [{ cron: "*/30 * * * *" }, { event: "test/30-min" }], - async ({ event, step }) => { - const mongo = new MongoClient(process.env.MONGO_DB as string); - try { - const mh = await step.run("grab-servers-from-api", async () => { - return await ( - await fetch("https://api.minehut.com/servers", { - headers: { - accept: "application/json", - "accept-language": Math.random().toString(), - priority: "u=1, i", - "sec-ch-ua": '"Not/A)Brand";v="8", "Chromium";v="126"', - "sec-ch-ua-mobile": "?0", - "sec-ch-ua-platform": '"macOS"', - "sec-fetch-dest": "empty", - "sec-fetch-mode": "cors", - "sec-fetch-site": "cross-site", - "Content-Type": "application/json", - // They'll never know hehehehehe - Referer: "http://localhost:3000/", - "Referrer-Policy": "strict-origin-when-cross-origin", - }, - body: null, - method: "GET", - }) - ).json(); - }); + return { event, body: "Done" }; + } + ), + inngest.createFunction( + { id: "short-term-data" }, + [{ cron: "*/30 * * * *" }, { event: "test/30-min" }], + async ({ event, step }) => { + const mongo = new MongoClient(process.env.MONGO_DB as string); + try { + const mh = await step.run("grab-servers-from-api", async () => { + return await ( + await fetch("https://api.minehut.com/servers", { + headers: { + accept: "application/json", + "accept-language": Math.random().toString(), + priority: "u=1, i", + "sec-ch-ua": '"Not/A)Brand";v="8", "Chromium";v="126"', + "sec-ch-ua-mobile": "?0", + "sec-ch-ua-platform": '"macOS"', + "sec-fetch-dest": "empty", + "sec-fetch-mode": "cors", + "sec-fetch-site": "cross-site", + "Content-Type": "application/json", + // They'll never know hehehehehe + Referer: "http://localhost:3000/", + "Referrer-Policy": "strict-origin-when-cross-origin", + }, + body: null, + method: "GET", + }) + ).json(); + }); - const mha = mongo.db("mhsf").collection("mh"); - const meta = mongo.db("mhsf").collection("meta"); - const dbl = mongo.db("mhsf").collection("history"); + const mha = mongo.db("mhsf").collection("mh"); + const meta = mongo.db("mhsf").collection("meta"); + const dbl = mongo.db("mhsf").collection("history"); - await mha.insertOne({ - total_players: mh.total_players, - total_servers: mh.total_servers, - date: new Date(), - }); + await mha.insertOne({ + total_players: mh.total_players, + total_servers: mh.total_servers, + date: new Date(), + }); - const completed = await step.run("listing-servers", async () => { - mh.servers.forEach(async (server: OnlineServer, i: number) => { - const serverFavoritesObject = await meta.findOne({ - server: server.name, - }); - let favorites = 0; - if (serverFavoritesObject != undefined) - favorites = serverFavoritesObject.favorites; + const completed = await step.run("listing-servers", async () => { + mh.servers.forEach(async (server: OnlineServer, i: number) => { + const serverFavoritesObject = await meta.findOne({ + server: server.name, + }); + let favorites = 0; + if (serverFavoritesObject != undefined) + favorites = serverFavoritesObject.favorites; - await dbl.insertOne({ - player_count: server.playerData.playerCount, - favorites, - server: server.name, - date: new Date(), - }); - console.log(i, mh.servers.length); - }); - return true; - }); - if (completed == true) { - return { event, body: "Finished!" }; - } - } catch (e) { - await mongo.close(); + await dbl.insertOne({ + player_count: server.playerData.playerCount, + favorites, + server: server.name, + date: new Date(), + }); + console.log(i, mh.servers.length); + }); + return true; + }); + if (completed == true) { + return { event, body: "Finished!" }; + } + } catch (e) { + await mongo.close(); - return { event, body: "Cloudflare.. aborting " + e }; - } - }, - ), - ], + return { event, body: "Cloudflare.. aborting " + e }; + } + } + ), + ], }); diff --git a/apps/www/src/pages/api/v0/account-linking/is-owned.ts b/apps/www/src/pages/api/v0/account-linking/is-owned.ts deleted file mode 100644 index 10a5e17..0000000 --- a/apps/www/src/pages/api/v0/account-linking/is-owned.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* - * MHSF, Minehut Server List - * All external content is rather licensed under the ECA Agreement - * located here: https://mhsf.app/docs/legal/external-content-agreement - * - * All code under MHSF is licensed under the MIT License - * by open source contributors - * - * Copyright (c) 2025 dvelo - * - * 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: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * 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. - */ - -import { NextApiRequest, NextApiResponse } from "next"; -import { MongoClient } from "mongodb"; -import { waitUntil } from "@vercel/functions"; - -export default async function handler( - req: NextApiRequest, - res: NextApiResponse, -) { - const { server } = req.body; - - if (server == null) { - res.status(400).send({ message: "Couldn't find data" }); - return; - } - - const client = new MongoClient(process.env.MONGO_DB as string); - await client.connect(); - - const db = client.db(process.env.CUSTOM_MONGO_DB ?? "mhsf"); - const collection = db.collection("owned-servers"); - - // Close the database, but don't close this - // serverless instance until it happens - waitUntil(client.close()); - - res.send({ owned: (await collection.findOne({ server })) != undefined }); -} diff --git a/apps/www/src/pages/api/v0/achievements/[server].ts b/apps/www/src/pages/api/v0/achievements/[server].ts deleted file mode 100644 index a2dfab8..0000000 --- a/apps/www/src/pages/api/v0/achievements/[server].ts +++ /dev/null @@ -1,53 +0,0 @@ -/* - * MHSF, Minehut Server List - * All external content is rather licensed under the ECA Agreement - * located here: https://mhsf.app/docs/legal/external-content-agreement - * - * All code under MHSF is licensed under the MIT License - * by open source contributors - * - * Copyright (c) 2025 dvelo - * - * 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: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * 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. - */ - -import { waitUntil } from "@vercel/functions"; -import { MongoClient } from "mongodb"; -import { NextApiRequest, NextApiResponse } from "next"; - -export default async function handler( - req: NextApiRequest, - res: NextApiResponse, -) { - const { server } = req.query; - if (!server) return res.status(400).send({ error: "No server was provided" }); - - const client = new MongoClient(process.env.MONGO_DB as string); - await client.connect(); - - const db = client.db(process.env.CUSTOM_MONGO_DB ?? "mhsf"); - const collection = db.collection("achievements"); - - // Close the database, but don't close this - // serverless instance until it happens - waitUntil(client.close()); - - res.send({ result: await collection.find({ name: server }).toArray() }); -} diff --git a/apps/www/src/pages/api/v0/achievements/multiple.ts b/apps/www/src/pages/api/v0/achievements/multiple.ts deleted file mode 100644 index 83f6f45..0000000 --- a/apps/www/src/pages/api/v0/achievements/multiple.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * MHSF, Minehut Server List - * All external content is rather licensed under the ECA Agreement - * located here: https://mhsf.app/docs/legal/external-content-agreement - * - * All code under MHSF is licensed under the MIT License - * by open source contributors - * - * Copyright (c) 2025 dvelo - * - * 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: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * 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. - */ - -// TODO: make multiple endpoint to allow achievements to be shown on the server-list diff --git a/apps/www/src/pages/api/v0/customization/getList.ts b/apps/www/src/pages/api/v0/customization/getList.ts deleted file mode 100644 index 88e0dde..0000000 --- a/apps/www/src/pages/api/v0/customization/getList.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - * MHSF, Minehut Server List - * All external content is rather licensed under the ECA Agreement - * located here: https://mhsf.app/docs/legal/external-content-agreement - * - * All code under MHSF is licensed under the MIT License - * by open source contributors - * - * Copyright (c) 2025 dvelo - * - * 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: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * 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. - */ - -import { NextApiRequest, NextApiResponse } from "next"; -import { MongoClient } from "mongodb"; -import { waitUntil } from "@vercel/functions"; - -export default async function handler( - req: NextApiRequest, - res: NextApiResponse, -) { - const { server }: { server: Array | undefined } = req.body; - - if (server == null) { - res.status(400).send({ message: "Couldn't find data" }); - return; - } - const client = new MongoClient(process.env.MONGO_DB as string); - await client.connect(); - - const db = client.db(process.env.CUSTOM_MONGO_DB ?? "mhsf"); - const collection = db.collection("customization"); - const results: { server: string; customization: any }[] = []; - - server.forEach(async (c) => { - results.push({ server: c, customization: await collection.findOne({ c }) }); - }); - - // Close the database, but don't close this - // serverless instance until it happens - waitUntil(client.close()); - - res.send({ results }); -} diff --git a/apps/www/src/pages/api/v0/favorites/[server]/community-favorites.ts b/apps/www/src/pages/api/v0/favorites/[server]/community-favorites.ts deleted file mode 100644 index 1a3b582..0000000 --- a/apps/www/src/pages/api/v0/favorites/[server]/community-favorites.ts +++ /dev/null @@ -1,91 +0,0 @@ -/* - * MHSF, Minehut Server List - * All external content is rather licensed under the ECA Agreement - * located here: https://mhsf.app/docs/legal/external-content-agreement - * - * All code under MHSF is licensed under the MIT License - * by open source contributors - * - * Copyright (c) 2025 dvelo - * - * 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: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * 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. - */ - -import type { NextApiResponse, NextApiRequest } from "next"; -import { MongoClient } from "mongodb"; -import { waitUntil } from "@vercel/functions"; - -export default async function handler( - req: NextApiRequest, - res: NextApiResponse, -) { - const { server } = req.query; - const client = new MongoClient(process.env.MONGO_DB as string); - - await client.connect(); - - const db = client.db(process.env.CUSTOM_MONGO_DB ?? "mhsf"); - const collection = db.collection("meta"); - const find = await collection.find({ server: server }).toArray(); - - // Close the database, but don't close this - // serverless instance until it happens - waitUntil(client.close()); - - if (find.length != 0) { - const entry = find[0]; - res.send({ result: entry.favorites }); - } else { - res.send({ result: 0 }); - } -} - -export async function increaseNum(client: MongoClient, server: string) { - const db = client.db(process.env.CUSTOM_MONGO_DB ?? "mhsf"); - const collection = db.collection("meta"); - const find = await collection.find({ server: server }).toArray(); - - if (find.length == 0) { - collection.insertOne({ server: server, favorites: 1, date: new Date() }); - } else { - const entry = find[0]; - collection.findOneAndReplace( - { server: server }, - { server: server, favorites: entry.favorites + 1, date: new Date() }, - ); - } -} - -export async function decreaseNum(client: MongoClient, server: string) { - const db = client.db(process.env.CUSTOM_MONGO_DB ?? "mhsf"); - const collection = db.collection("meta"); - const find = await collection.find({ server: server }).toArray(); - - if (find.length == 0) { - return; - // Physically is impossible - } else { - const entry = find[0]; - collection.findOneAndReplace( - { server: server }, - { server: server, favorites: entry.favorites - 1 }, - ); - } -} diff --git a/apps/www/src/pages/api/v0/favorites/[server]/favorited.ts b/apps/www/src/pages/api/v0/favorites/[server]/favorited.ts deleted file mode 100644 index af81a5f..0000000 --- a/apps/www/src/pages/api/v0/favorites/[server]/favorited.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - * MHSF, Minehut Server List - * All external content is rather licensed under the ECA Agreement - * located here: https://mhsf.app/docs/legal/external-content-agreement - * - * All code under MHSF is licensed under the MIT License - * by open source contributors - * - * Copyright (c) 2025 dvelo - * - * 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: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * 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. - */ - -import type { NextApiResponse, NextApiRequest } from "next"; -import { MongoClient } from "mongodb"; -import { getAuth } from "@clerk/nextjs/server"; -import { waitUntil } from "@vercel/functions"; - -export default async function handler( - req: NextApiRequest, - res: NextApiResponse, -) { - const { userId } = getAuth(req); - - if (!userId) { - return res.status(401).json({ error: "Unauthorized" }); - } - const server = req.query.server as string; - const client = new MongoClient(process.env.MONGO_DB as string); - await client.connect(); - - const db = client.db(process.env.CUSTOM_MONGO_DB ?? "mhsf"); - const collection = db.collection("favorites"); - const find = await collection.find({ user: userId }).toArray(); - - // Close the database, but don't close this - // serverless instance until it happens - waitUntil(client.close()); - - if (find.length == 0) res.send({ result: false }); - else { - res.send({ result: find[0].favorites.includes(server) }); - } -} diff --git a/apps/www/src/pages/api/v0/history/[server]/get-short-term-data.ts b/apps/www/src/pages/api/v0/history/[server]/get-short-term-data.ts deleted file mode 100644 index 43f59d4..0000000 --- a/apps/www/src/pages/api/v0/history/[server]/get-short-term-data.ts +++ /dev/null @@ -1,70 +0,0 @@ -/* - * MHSF, Minehut Server List - * All external content is rather licensed under the ECA Agreement - * located here: https://mhsf.app/docs/legal/external-content-agreement - * - * All code under MHSF is licensed under the MIT License - * by open source contributors - * - * Copyright (c) 2025 dvelo - * - * 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: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * 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. - */ - -import { MongoClient } from "mongodb"; -import { NextApiRequest, NextApiResponse } from "next"; -import { waitUntil } from "@vercel/functions"; - -export default async function handler( - req: NextApiRequest, - res: NextApiResponse, -) { - const client = new MongoClient(process.env.MONGO_DB as string); - const db = client.db("mhsf").collection("history"); - const server = req.query.server as string; - let dataMax = 0; - const scopes: Array = checkForInfoOrLeave(res, req.body.scopes); - - const allData = await db.find({ server }).toArray(); - const data: any[] = []; - - dataMax = ( - await db.find({ server }).sort({ player_count: -1 }).limit(1).toArray() - )[0].player_count; - - allData.forEach((d) => { - const result: any = {}; - scopes.forEach((b) => { - result[b] = d[b]; - }); - data.push(result); - }); - - // Close the database, but don't close this - // serverless instance until it happens - waitUntil(client.close()); - res.send({ data, dataMax }); -} - -function checkForInfoOrLeave(res: NextApiResponse, info: any) { - if (info == undefined) - res.status(400).json({ message: "Information wasn't supplied" }); - return info; -} diff --git a/apps/www/src/pages/api/v1/server/bulk.ts b/apps/www/src/pages/api/v1/server/bulk.ts new file mode 100644 index 0000000..bd4dc04 --- /dev/null +++ b/apps/www/src/pages/api/v1/server/bulk.ts @@ -0,0 +1,428 @@ +/* + * MHSF, Minehut Server List + * All external content is rather licensed under the ECA Agreement + * located here: https://mhsf.app/docs/legal/external-content-agreement + * + * All code under MHSF is licensed under the MIT License + * by open source contributors + * + * Copyright (c) 2025 dvelo + * + * 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: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * 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. + */ + +import type { MHSFData } from "@/lib/types/data"; +import { MongoClient } from "mongodb"; +import type { NextApiRequest, NextApiResponse } from "next"; + +// Type definitions for query parameters +type QueryParams = { + maxFavoriteEntries?: string | string[]; + favoriteTimespanStart?: string | string[]; + favoriteTimespanEnd?: string | string[]; + maxPlayerEntries?: string | string[]; + playerTimespanStart?: string | string[]; + playerTimespanEnd?: string | string[]; + maxAchievementEntries?: string | string[]; + achievementTimespanStart?: string | string[]; + achievementTimespanEnd?: string | string[]; +}; + +// Type for customization data +type CustomizationData = { + description: string | undefined; + banner: string | undefined; + discord: string | undefined; + colorScheme: string | undefined; + userProfilePicture: string | undefined; + isOwned: boolean; + isOwnedByUser: boolean; +}; + +// Type for favorite data +type FavoriteData = { + favoritedByAccount: boolean | null; + favoriteNumber: number; + favoriteHistoricalData: any[]; +}; + +// Type for player data +type PlayerData = { + historically: { date: string; playerCount: number }[]; + max: number; +}; + +// Type for achievements data +type AchievementsData = { + historically: any[]; + currently: any[]; +}; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse<{ servers: Record }> +) { + // Only accept POST requests with server list in the body + if (req.method !== "POST") { + return res.status(405).json({ servers: {} }); + } + + // Get the list of servers from the request body + const { servers, options } = req.body; + + if (!servers || !Array.isArray(servers) || servers.length === 0) { + return res.status(400).json({ servers: {} }); + } + + // Limit the number of servers to prevent abuse (max 25 servers per request) + const serverList = servers.slice(0, 25); + + // Extract query parameters + const queryOptions: QueryParams = { + maxFavoriteEntries: req.query.maxFavoriteEntries, + favoriteTimespanStart: req.query.favoriteTimespanStart, + favoriteTimespanEnd: req.query.favoriteTimespanEnd, + maxPlayerEntries: req.query.maxPlayerEntries, + playerTimespanStart: req.query.playerTimespanStart, + playerTimespanEnd: req.query.playerTimespanEnd, + maxAchievementEntries: req.query.maxAchievementEntries, + achievementTimespanStart: req.query.achievementTimespanStart, + achievementTimespanEnd: req.query.achievementTimespanEnd, + }; + + // Determine which data to fetch based on options + const fetchOptions = { + favorites: options?.favorites !== false, + customization: options?.customization !== false, + players: options?.players !== false, + achievements: options?.achievements !== false, + }; + + const mongo = new MongoClient(process.env.MONGO_DB as string); + const result: Record = {}; + + try { + await mongo.connect(); + const db = mongo.db(process.env.CUSTOM_MONGO_DB ?? "mhsf"); + const userId = req.cookies.userId; + + // Process each server in parallel + await Promise.all( + serverList.map(async (server: string) => { + try { + // Verify server exists + const serverData = await findServerData(server); + if (!serverData.exists) { + result[server] = null; + return; + } + + // Prepare promises array based on fetch options + const promises: Promise[] = []; + const promiseResults: Record = {}; + + if (fetchOptions.favorites) { + promises.push( + findFavoriteData(serverData.name, userId, db, queryOptions).then( + (data: FavoriteData) => { + promiseResults.favoriteData = data; + } + ) + ); + } + + if (fetchOptions.customization) { + promises.push( + findCustomizationData(serverData.name, userId, db).then( + (data: CustomizationData) => { + promiseResults.customizationData = data; + } + ) + ); + } + + if (fetchOptions.players) { + promises.push( + findPlayerData(serverData.name, db, queryOptions).then( + (data: PlayerData) => { + promiseResults.playerData = data; + } + ) + ); + } + + if (fetchOptions.achievements) { + promises.push( + findAchievements(serverData.name, db, queryOptions).then( + (data: AchievementsData) => { + promiseResults.achievements = data; + } + ) + ); + } + + // Wait for all promises to resolve + await Promise.all(promises); + + // Create default values for any missing data + const serverResult: MHSFData = { + favoriteData: promiseResults.favoriteData || { + favoritedByAccount: null, + favoriteNumber: 0, + favoriteHistoricalData: [], + }, + customizationData: promiseResults.customizationData || { + description: undefined, + banner: undefined, + discord: undefined, + colorScheme: undefined, + userProfilePicture: undefined, + isOwned: false, + isOwnedByUser: false, + }, + playerData: promiseResults.playerData || { + historically: [], + max: 0, + }, + achievements: promiseResults.achievements || { + historically: [], + currently: [], + }, + }; + + result[server] = serverResult; + } catch (error) { + console.error(`Error processing server ${server}:`, error); + result[server] = null; + } + }) + ); + + res.status(200).json({ servers: result }); + } catch (error) { + console.error("Error processing bulk request:", error); + res.status(500).json({ servers: {} }); + } finally { + await mongo.close(); + } +} + +// Helper functions +async function findServerData( + server: string +): Promise<{ exists: boolean; name: string }> { + try { + const response = await fetch("https://api.minehut.com/server/" + server); + + if (!response.ok) { + return { exists: false, name: "" }; + } + + const serverJSON = await response.json(); + if (!serverJSON.server) return { exists: false, name: "" }; + + return { exists: true, name: serverJSON.server.name }; + } catch (error) { + console.error("Error fetching server data:", error); + return { exists: false, name: "" }; + } +} + +async function findCustomizationData( + serverName: string, + userId: string | undefined, + db: any +): Promise { + // Run queries in parallel + const [customizationData, ownedServerData] = await Promise.all([ + db.collection("customization").findOne({ server: serverName }), + userId + ? db.collection("owned-servers").findOne({ server: serverName }) + : null, + ]); + + if (customizationData) { + return { + ...(customizationData as any), + isOwned: true, + isOwnedByUser: ownedServerData?.author === userId, + }; + } + + return { + isOwned: false, + isOwnedByUser: false, + description: undefined, + banner: undefined, + discord: undefined, + colorScheme: undefined, + userProfilePicture: undefined, + }; +} + +async function findFavoriteData( + serverName: string, + userId: string | undefined, + db: any, + query: QueryParams +): Promise { + // Run queries in parallel + const [userFavorites, metaData, historyData] = await Promise.all([ + userId ? db.collection("favorites").findOne({ user: userId }) : null, + db.collection("meta").findOne({ server: serverName }), + fetchHistoryData(db, serverName, query), + ]); + + // Process user favorites + const favoritedByAccount = + userId && userFavorites + ? userFavorites.favorites.includes(serverName) + : null; + + // Process favorite count + const favoriteNumber = metaData?.favorites || 0; + + return { + favoritedByAccount, + favoriteNumber, + favoriteHistoricalData: historyData, + }; +} + +async function fetchHistoryData( + db: any, + serverName: string, + query: QueryParams +): Promise { + // Build query filter + const filter: any = { server: serverName }; + + // Add date range filter if provided + if (query.favoriteTimespanStart && query.favoriteTimespanEnd) { + filter.date = { + $gte: new Date(Number(query.favoriteTimespanStart)), + $lte: new Date(Number(query.favoriteTimespanEnd)), + }; + } + + // Determine limit + const limit = query.maxFavoriteEntries ? Number(query.maxFavoriteEntries) : 0; + + // Use projection to only fetch needed fields + const projection = { favorites: 1, date: 1, _id: 0 }; + + // Execute optimized query + const cursor = db.collection("history").find(filter).project(projection); + + // Apply limit if specified + if (limit > 0) { + cursor.limit(limit); + } + + return await cursor.toArray(); +} + +async function findPlayerData( + serverName: string, + db: any, + query: QueryParams +): Promise { + // Build query filter + const filter: any = { server: serverName }; + + // Add date range filter if provided + if (query.playerTimespanStart && query.playerTimespanEnd) { + filter.date = { + $gte: new Date(Number(query.playerTimespanStart)), + $lte: new Date(Number(query.playerTimespanEnd)), + }; + } + + // Use projection to only fetch needed fields + const projection = { player_count: 1, date: 1, _id: 0 }; + + // Get max player count in a single query + const [maxResult, playerHistory] = await Promise.all([ + db + .collection("history") + .find({ server: serverName }) + .sort({ player_count: -1 }) + .limit(1) + .project({ player_count: 1 }) + .toArray(), + + db.collection("history").find(filter).project(projection).toArray(), + ]); + + // Apply limit if specified + let historically = playerHistory; + if (query.maxPlayerEntries) { + historically = historically.slice(0, Number(query.maxPlayerEntries)); + } + + // Format the data to match the expected structure + const formattedHistory = historically.map( + (item: { date: string; player_count?: number }) => ({ + date: item.date, + playerCount: item.player_count || 0, + }) + ); + + const max = maxResult.length > 0 ? maxResult[0].player_count : 0; + + return { historically: formattedHistory, max }; +} + +async function findAchievements( + serverName: string, + db: any, + query: QueryParams +): Promise { + // Get achievements data + const achievementsCollection = db.collection("achievements"); + + // Build query filter + const filter: any = { name: serverName }; + + // Add date range filter if provided + if (query.achievementTimespanStart && query.achievementTimespanEnd) { + // Assuming there's a timestamp or date field in the achievements collection + filter.timestamp = { + $gte: new Date(Number(query.achievementTimespanStart)), + $lte: new Date(Number(query.achievementTimespanEnd)), + }; + } + + // Get historical achievements + let historically = await achievementsCollection.find(filter).toArray(); + + // Apply limit if specified + if (query.maxAchievementEntries) { + historically = historically.slice(0, Number(query.maxAchievementEntries)); + } + + const currently: any[] = []; + for (const a of historically) + a.achievements.forEach((item: any, interval: number) => + currently.push({ interval, ...item }) + ); + + return { historically, currently }; +} diff --git a/apps/www/src/pages/api/v1/server/get/[server].ts b/apps/www/src/pages/api/v1/server/get/[server].ts new file mode 100644 index 0000000..50513ea --- /dev/null +++ b/apps/www/src/pages/api/v1/server/get/[server].ts @@ -0,0 +1,329 @@ +/* + * MHSF, Minehut Server List + * All external content is rather licensed under the ECA Agreement + * located here: https://mhsf.app/docs/legal/external-content-agreement + * + * All code under MHSF is licensed under the MIT License + * by open source contributors + * + * Copyright (c) 2025 dvelo + * + * 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: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * 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. + */ + +import type { MHSFData } from "@/lib/types/data"; +import { MongoClient } from "mongodb"; +import type { NextApiRequest, NextApiResponse } from "next"; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse<{ server: MHSFData | null }> +) { + const { + server, + maxFavoriteEntries, + favoriteTimespanStart, + favoriteTimespanEnd, + maxPlayerEntries, + playerTimespanStart, + playerTimespanEnd, + maxAchievementEntries, + achievementTimespanStart, + achievementTimespanEnd, + } = req.query; + if (!server) return res.status(400).send({ server: null }); + + const serverData = await findServerData(server as string); + if (!serverData.exists) return res.status(404).send({ server: null }); + + const mongo = new MongoClient(process.env.MONGO_DB as string); + + try { + await mongo.connect(); + const db = mongo.db(process.env.CUSTOM_MONGO_DB ?? "mhsf"); + const userId = req.cookies.userId; + + // Run queries in parallel + const [favoriteData, customizationData, playerData, achievements] = + await Promise.all([ + findFavoriteData(serverData.name, userId, db, { + maxFavoriteEntries, + favoriteTimespanStart, + favoriteTimespanEnd, + }), + findCustomizationData(serverData.name, userId, db), + findPlayerData(serverData.name, db, { + maxPlayerEntries, + playerTimespanStart, + playerTimespanEnd, + }), + findAchievements(serverData.name, db, { + maxAchievementEntries, + achievementTimespanStart, + achievementTimespanEnd, + }), + ]); + + // Ignore the linter error as requested + res.send({ + server: { + favoriteData, + customizationData, + playerData, + achievements, + }, + }); + } catch (error) { + console.error("Error processing request:", error); + res.status(500).send({ server: null }); + } finally { + await mongo.close(); + } +} + +async function findCustomizationData( + serverName: string, + userId: string | undefined, + db: any +): Promise<{ + description: string | undefined; + banner: string | undefined; + discord: string | undefined; + colorScheme: string | undefined; + userProfilePicture: string | undefined; + isOwned: boolean; + isOwnedByUser: boolean; +}> { + // Run queries in parallel + const [customizationData, ownedServerData] = await Promise.all([ + db.collection("customization").findOne({ server: serverName }), + userId + ? db.collection("owned-servers").findOne({ server: serverName }) + : null, + ]); + + if (customizationData) { + return { + ...(customizationData as any), + isOwned: true, + isOwnedByUser: ownedServerData?.author === userId, + }; + } + + return { + isOwned: false, + isOwnedByUser: false, + description: undefined, + banner: undefined, + discord: undefined, + colorScheme: undefined, + userProfilePicture: undefined, + }; +} + +async function findFavoriteData( + serverName: string, + userId: string | undefined, + db: any, + query: { + maxFavoriteEntries?: string | string[]; + favoriteTimespanStart?: string | string[]; + favoriteTimespanEnd?: string | string[]; + } +) { + // Run queries in parallel + const [userFavorites, metaData, historyData] = await Promise.all([ + userId ? db.collection("favorites").findOne({ user: userId }) : null, + db.collection("meta").findOne({ server: serverName }), + fetchHistoryData(db, serverName, query), + ]); + + // Process user favorites + const favoritedByAccount = + userId && userFavorites + ? userFavorites.favorites.includes(serverName) + : null; + + // Process favorite count + const favoriteNumber = metaData?.favorites || 0; + + return { + favoritedByAccount, + favoriteNumber, + favoriteHistoricalData: historyData, + }; +} + +async function fetchHistoryData( + db: any, + serverName: string, + query: { + maxFavoriteEntries?: string | string[]; + favoriteTimespanStart?: string | string[]; + favoriteTimespanEnd?: string | string[]; + } +) { + // Build query filter + const filter: any = { server: serverName }; + + // Add date range filter if provided + if (query.favoriteTimespanStart && query.favoriteTimespanEnd) { + filter.date = { + $gte: new Date(Number(query.favoriteTimespanStart)), + $lte: new Date(Number(query.favoriteTimespanEnd)), + }; + } + + // Determine limit + const limit = query.maxFavoriteEntries ? Number(query.maxFavoriteEntries) : 0; + + // Use projection to only fetch needed fields + const projection = { favorites: 1, date: 1, _id: 0 }; + + // Execute optimized query + const cursor = db.collection("history").find(filter).project(projection); + + // Apply limit if specified + if (limit > 0) { + cursor.limit(limit); + } + + return await cursor.toArray(); +} + +async function findServerData( + server: string +): Promise<{ exists: boolean; name: string }> { + try { + const response = await fetch("https://api.minehut.com/server/" + server); + + // Check if the response is ok before parsing JSON + if (!response.ok) { + return { exists: false, name: "" }; + } + + const serverJSON = await response.json(); + if (!serverJSON.server) return { exists: false, name: "" }; + + return { exists: true, name: serverJSON.server.name }; + } catch (error) { + console.error("Error fetching server data:", error); + return { exists: false, name: "" }; + } +} + +async function findPlayerData( + serverName: string, + db: any, + query: { + maxPlayerEntries?: string | string[]; + playerTimespanStart?: string | string[]; + playerTimespanEnd?: string | string[]; + } +) { + // Get historical player data + const historyCollection = db.collection("history"); + + // Build query filter + const filter: any = { server: serverName }; + + // Add date range filter if provided + if (query.playerTimespanStart && query.playerTimespanEnd) { + filter.date = { + $gte: new Date(Number(query.playerTimespanStart)), + $lte: new Date(Number(query.playerTimespanEnd)), + }; + } + + // Use projection to only fetch needed fields + const projection = { player_count: 1, date: 1, _id: 0 }; + + // Get max player count in a single query + const [maxResult, playerHistory] = await Promise.all([ + historyCollection + .find({ server: serverName }) + .sort({ player_count: -1 }) + .limit(1) + .project({ player_count: 1 }) + .toArray(), + + historyCollection.find(filter).project(projection).toArray(), + ]); + + // Apply limit if specified + let historically = playerHistory; + if (query.maxPlayerEntries) { + historically = historically.slice(0, Number(query.maxPlayerEntries)); + } + + // Format the data to match the expected structure + const formattedHistory = historically.map( + (item: { date: string; player_count?: number }) => ({ + date: item.date, + playerCount: item.player_count || 0, + }) + ); + + const max = maxResult.length > 0 ? maxResult[0].player_count : 0; + + return { historically: formattedHistory, max }; +} + +async function findAchievements( + serverName: string, + db: any, + query: { + maxAchievementEntries?: string | string[]; + achievementTimespanStart?: string | string[]; + achievementTimespanEnd?: string | string[]; + } +) { + // Get achievements data + const achievementsCollection = db.collection("achievements"); + + // Build query filter + const filter: any = { name: serverName }; + + // Add date range filter if provided + if (query.achievementTimespanStart && query.achievementTimespanEnd) { + // Assuming there's a timestamp or date field in the achievements collection + // If it's stored in _id, we might need a different approach + filter.timestamp = { + $gte: new Date(Number(query.achievementTimespanStart)), + $lte: new Date(Number(query.achievementTimespanEnd)), + }; + } + + // Get historical achievements + let historically = await achievementsCollection.find(filter).toArray(); + + // Apply limit if specified + if (query.maxAchievementEntries) { + historically = historically.slice(0, Number(query.maxAchievementEntries)); + } + + const currently: any[] = []; + for (const a of historically) + a.achievements.forEach((item: any, interval: number) => + currently.push({ interval, ...item }) + ); + + return { historically, currently }; +}