fix(www): optimize statistics

This commit is contained in:
dvelo 2025-03-08 21:07:51 -06:00
parent 788051f8b3
commit 45e0924808
11 changed files with 862 additions and 526 deletions

@ -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[];
};
};

@ -49,11 +49,11 @@ export default serve({
await createReportIssue(
event.data.server,
event.data.reason,
event.data.userId,
event.data.userId
);
return { event, body: "Done" };
},
}
),
inngest.createFunction(
{ id: "short-term-data" },
@ -122,7 +122,7 @@ export default serve({
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;
}

@ -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 };
}

@ -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 };
}