mirror of
https://github.com/DeveloLongScript/MHSF.git
synced 2026-05-07 19:35:00 -05:00
fix(www): optimize statistics
This commit is contained in:
parent
788051f8b3
commit
45e0924808
@ -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[];
|
||||
};
|
||||
};
|
||||
@ -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 };
|
||||
}
|
||||
}
|
||||
),
|
||||
],
|
||||
});
|
||||
|
||||
@ -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 });
|
||||
}
|
||||
@ -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() });
|
||||
}
|
||||
@ -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
|
||||
@ -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<string> | 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 });
|
||||
}
|
||||
@ -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 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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) });
|
||||
}
|
||||
}
|
||||
@ -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<string> = 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;
|
||||
}
|
||||
428
apps/www/src/pages/api/v1/server/bulk.ts
Normal file
428
apps/www/src/pages/api/v1/server/bulk.ts
Normal file
@ -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<string, MHSFData | null> }>
|
||||
) {
|
||||
// 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<string, MHSFData | null> = {};
|
||||
|
||||
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<any>[] = [];
|
||||
const promiseResults: Record<string, any> = {};
|
||||
|
||||
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<CustomizationData> {
|
||||
// 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<FavoriteData> {
|
||||
// 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<any[]> {
|
||||
// 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<PlayerData> {
|
||||
// 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<AchievementsData> {
|
||||
// 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 };
|
||||
}
|
||||
329
apps/www/src/pages/api/v1/server/get/[server].ts
Normal file
329
apps/www/src/pages/api/v1/server/get/[server].ts
Normal file
@ -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 };
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user