diff --git a/cron/.gitignore b/cron/.gitignore new file mode 100644 index 0000000..7773828 --- /dev/null +++ b/cron/.gitignore @@ -0,0 +1 @@ +dist/ \ No newline at end of file diff --git a/cron/Dockerfile b/cron/Dockerfile new file mode 100755 index 0000000..7db8398 --- /dev/null +++ b/cron/Dockerfile @@ -0,0 +1,17 @@ +FROM node:20-alpine + +RUN mkdir -p /home/node/app/node_modules && chown -R node:node /home/node/app +# This ensures that while in production, the .env file gets read from the current directory instead of the +# previous directory. +ENV MHC_DOCKER=true +WORKDIR /home/node/app + +COPY package.json ./ +COPY dist/index.js ./ +COPY .env.local ./ + +USER node + +RUN npm install + +CMD [ "node", "index.js" ] diff --git a/cron/README.md b/cron/README.md index 8fe243b..c79f0da 100644 --- a/cron/README.md +++ b/cron/README.md @@ -8,20 +8,20 @@ When running Inngest, on Vercel's servers, when doing the `/servers` Minehut API ## How do you run this? +Make sure you have a MongoDB database set-up and ready with a file **in the previous directory (..)** named `.env.local` with the key `MONGO_DB` as the database URL. You can also just set an environment variable. +Simply run the following to test: - -If you're on a Unix based machine, just type the following: - -```bash -# Make sure you already cloned the repo and are in the /cron directory. -# This project uses NPM instead of Yarn for the website +``` npm install - -crontab -e +npm run dev ``` -and in `vi` go into insert mode (type `i`) and type the following: +and to deploy using Docker: ``` -*/30 * * * * cd "/cron/" && npm start +npm run build +docker build -t mhsf-dbref . + +# run the container +docker run --name mhsf-dbref ``` diff --git a/cron/config.yaml b/cron/config.yaml new file mode 100755 index 0000000..533010e --- /dev/null +++ b/cron/config.yaml @@ -0,0 +1,12 @@ +# This is configuration a Home Assistant addon. +name: "DB refresh" +description: "MHSF Cron DB Refresh" +version: "1.0.0" +slug: "mhsf_db_ref" +init: true +arch: + - aarch64 + - amd64 + - armhf + - armv7 + - i386 diff --git a/cron/package.json b/cron/package.json index c1f1d7e..bc345b4 100644 --- a/cron/package.json +++ b/cron/package.json @@ -4,11 +4,13 @@ "description": "In version 1.0, MHSF moved from using Inngest to collect statistics to a self-hosted `crontab` Node.js script.", "main": "dist/index.js", "scripts": { - "start": "tsc --build && node ./dist/index.js" + "dev": "npx tsx src/index.ts", + "build": "npx tsc -p ./tsconfig.json" }, "author": "", "license": "ISC", "dependencies": { + "arguments-parser": "^3.2.1", "chalk": "^5.3.0", "dotenv": "^16.4.5", "mongodb": "^6.8.0" diff --git a/cron/src/index.ts b/cron/src/index.ts index faaaf24..6deddae 100644 --- a/cron/src/index.ts +++ b/cron/src/index.ts @@ -7,23 +7,55 @@ console.log(chalk.yellow(chalk.bold("MHSF crontab scripts"))); console.log(chalk.yellow(chalk.bold("by dvelo - licensed under MIT license"))); console.log(); -import { MongoClient } from "mongodb"; +import { MongoClient, WithId } from "mongodb"; import { config } from "dotenv"; +import { + OnlineServer, + OnlineServerExtended, + ServerResponse, +} from "./types/mh-server.js"; +import { CronJob } from "cron"; +import { Achievement } from "./types/achievement.js"; // set-up config -config({ path: "../.env.local" }); - -const mongo = new MongoClient(process.env.MONGO_DB as string); - -main().catch((e) => { - console.log(chalk.red("[CRON] " + ERROR + " Error while running: ")); - console.error(e); +config({ + path: process.env.MHC_DOCKER != "true" ? "../.env.local" : "./.env.local", }); + +let mongo = new MongoClient(process.env.MONGO_DB as string); + const SUCCESS = chalk.green("SUCCESS"); const ERROR = chalk.red("ERROR"); const WARN = chalk.red("WARN"); const INFO = chalk.blueBright("INFO"); +console.log(INFO, "Starting cron job #1"); + +CronJob.from({ + cronTime: "*/30 * * * *", + onTick: function () { + periodicCronJob().catch((e) => { + console.log(chalk.red("[CRON] " + ERROR + " Error while running: ")); + console.error(e); + }); + }, + start: true, + timeZone: "America/Los_Angeles", +}); + +console.log(INFO, "Starting cron job #2"); +CronJob.from({ + cronTime: "0 */12 * * *", + onTick: function () { + achievementTask().catch((e) => { + console.log(chalk.red("[CRON] " + ERROR + " Error while running: ")); + console.error(e); + }); + }, + start: true, + timeZone: "America/Los_Angeles", +}); + /** * Main function that runs the script. * @@ -32,8 +64,7 @@ const INFO = chalk.blueBright("INFO"); * Then, it iterates over each server and inserts the player count, server name, and date into the "history" collection. * If an error occurs, it logs the error and closes the MongoDB connection. */ -async function main() { - await mongo.connect(); +async function periodicCronJob() { try { // No more mumbo jumbo const mh = await ( @@ -70,7 +101,7 @@ async function main() { let y = 0; - mh.servers.forEach(async (server: any, i: number) => { + mh.servers.forEach(async (server: OnlineServer, i: number) => { const serverFavoritesObject = await meta.findOne({ server: server.name, }); @@ -115,9 +146,142 @@ async function main() { } }); } catch (e) { - await mongo.close(); console.log("[CRON] " + ERROR + " Error while parsing JSON:", e); return; } } + +async function achievementTask() { + try { + const mh = 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", + Referer: "http://localhost:3000/", + "Referrer-Policy": "strict-origin-when-cross-origin", + }, + body: null, + method: "GET", + }) + ).json(); + const meta = mongo.db("mhsf").collection("meta"); + const achievements = mongo.db("mhsf").collection("achievements"); + console.log("adding achievements"); + + 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 srvExt: OnlineServerExtended = { + ...server, + favorites, + position: { joins: i + 1 }, + }; + const prevAchievements = ((await achievements.findOne({ + name: server.name, + })) || { _id: "", name: server.name, achievements: [] }) as WithId<{ + name: string; + achievements: Achievement[]; + }>; + + const achievementsTsk = await achievementEngine( + srvExt, + prevAchievements.achievements + ); + + await achievements.insertOne({ + name: server.name, + achievements: achievementsTsk, + }); + }); + } catch (e) { + console.log("[CRON] " + ERROR + " Error while parsing JSON:", e); + + return; + } +} + +async function achievementEngine( + server: OnlineServerExtended, + currentAchievements: Achievement[] +): Promise { + const achievements: Array = []; + + if ( + server.favorites >= 1000 && + currentAchievements.find((c) => c.type == "has1kFavorites") === undefined + ) { + achievements.push({ + type: "has1kFavorites", + date: new Date().toISOString(), + }); + } + + if ( + server.favorites >= 100000 && + currentAchievements.find((c) => c.type == "has100kFavorites") === undefined + ) { + achievements.push({ + type: "has100kFavorites", + date: new Date().toISOString(), + }); + } + + if ( + server.playerData.playerCount >= 2 && + currentAchievements.find((c) => c.type == "has1kTotalJoins") === undefined + ) { + const v: { server: ServerResponse } = await ( + await fetch( + "https://api.minehut.com/server/" + server.name + "?byName=true" + ) + ).json(); + + if (v.server.joins >= 1000) { + achievements.push({ + type: "has1kTotalJoins", + date: new Date().toISOString(), + }); + } + } + if ( + server.playerData.playerCount >= 10 && + currentAchievements.find((c) => c.type == "has100kTotalJoins") === undefined + ) { + const v: { server: ServerResponse } = await ( + await fetch( + "https://api.minehut.com/server/" + server.name + "?byName=true" + ) + ).json(); + + if (v.server.joins >= 100000) { + achievements.push({ + type: "has100kTotalJoins", + date: new Date().toISOString(), + }); + } + } + + if (server.position.joins === 1) { + achievements.push({ + type: "mostJoined", + date: new Date().toISOString(), + }); + } + + return achievements; +} diff --git a/cron/src/types/achievement.ts b/cron/src/types/achievement.ts new file mode 100644 index 0000000..83e04d9 --- /dev/null +++ b/cron/src/types/achievement.ts @@ -0,0 +1,10 @@ +export type Achievement = { + type: + | "mostJoined" + | "has1kFavorites" + | "has1kTotalJoins" + | "has100kFavorites" + | "has100kTotalJoins"; + /** The ISO time of the gaining of the achievement. */ + date: string; +}; diff --git a/cron/src/types/mh-server.ts b/cron/src/types/mh-server.ts new file mode 100644 index 0000000..acdb746 --- /dev/null +++ b/cron/src/types/mh-server.ts @@ -0,0 +1,89 @@ +export interface ServerResponse { + __unix?: string; + deletion?: Deletion; + _id: string; + categories: string[]; + inheritedCategories: any[]; + purchased_icons: string[]; + backup_slots: number; + suspended: boolean; + server_version_type: string; + proxy: boolean; + connectedServers: any[]; + motd: string; + visibility: boolean; + server_plan: string; + storage_node: string; + default_banner_image: string; + default_banner_tint: string; + owner: string; + name: string; + name_lower: string; + creation: number; + platform: string; + credits_per_day: number; + in_game: boolean; + using_cosmetics: boolean; + __v: number; + port: number; + last_online: number; + joins: number; + active_icon: string; + expired: boolean; + icon: string; + online: boolean; + maxPlayers: number; + playerCount: number; + rawPlan: string; + activeServerPlan: string; +} + +export interface Deletion { + started: boolean; + started_at: number; + reason: string; + completed: boolean; + completed_at: number; + storage_completed: boolean; + storage_completed_at: number; +} + +export interface OnlineServer { + staticInfo: StaticInfo; + maxPlayers: number; + name: string; + motd: string; + icon: string; + playerData: PlayerData; + connectable: boolean; + visibility: boolean; + allCategories: string[]; + usingCosmetics: boolean; + author?: string; + authorRank: string; +} + +export interface StaticInfo { + _id: string; + serverPlan: string; + serviceStartDate: number; + platform: string; + planMaxPlayers: number; + planRam: number; + alwaysOnline: boolean; + rawPlan: string; + connectedServers: any[]; +} + +export interface PlayerData { + playerCount: number; + timeNoPlayers: number; +} + +export interface OnlineServerExtended extends OnlineServer { + favorites: number; + position: { + joins: number; + favorites?: number; + }; +} diff --git a/cron/tsconfig.json b/cron/tsconfig.json index ac1b69f..7c8a569 100644 --- a/cron/tsconfig.json +++ b/cron/tsconfig.json @@ -4,8 +4,8 @@ "noEmitOnError": true, "removeComments": false, "sourceMap": true, - "target": "ES6", - "module": "NodeNext", + "moduleResolution": "Node", + "target": "es6", "outDir": "dist" }, "include": ["src/**/*"] diff --git a/cron/yarn.lock b/cron/yarn.lock new file mode 100644 index 0000000..d993200 --- /dev/null +++ b/cron/yarn.lock @@ -0,0 +1,137 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@mongodb-js/saslprep@^1.1.5": + version "1.1.9" + resolved "https://registry.yarnpkg.com/@mongodb-js/saslprep/-/saslprep-1.1.9.tgz#e974bab8eca9faa88677d4ea4da8d09a52069004" + integrity sha512-tVkljjeEaAhCqTzajSdgbQ6gE6f3oneVwa3iXR6csiEwXXOFsiC6Uh9iAjAhXPtqa/XMDHWjjeNH/77m/Yq2dw== + dependencies: + sparse-bitfield "^3.0.3" + +"@types/webidl-conversions@*": + version "7.0.3" + resolved "https://registry.yarnpkg.com/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz#1306dbfa53768bcbcfc95a1c8cde367975581859" + integrity sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA== + +"@types/whatwg-url@^11.0.2": + version "11.0.5" + resolved "https://registry.yarnpkg.com/@types/whatwg-url/-/whatwg-url-11.0.5.tgz#aaa2546e60f0c99209ca13360c32c78caf2c409f" + integrity sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ== + dependencies: + "@types/webidl-conversions" "*" + +ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +arguments-parser@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/arguments-parser/-/arguments-parser-3.2.1.tgz#c816e1db2d4e3395ea1bfd1ae65b3fe9303c8a7b" + integrity sha512-+wWBD6LKShmR8nszO6kgc+Z4pOqbADh4RH1gWWFD9Vuot1ljOf2Tc0FCn1mE6Yc/eT0GkxGn5OjV0gRxpCq2BQ== + dependencies: + chalk "4.1.2" + +bson@^6.7.0: + version "6.8.0" + resolved "https://registry.yarnpkg.com/bson/-/bson-6.8.0.tgz#5063c41ba2437c2b8ff851b50d9e36cb7aaa7525" + integrity sha512-iOJg8pr7wq2tg/zSlCCHMi3hMm5JTOxLTagf3zxhcenHsFp+c6uOs6K7W5UE7A4QIJGtqh/ZovFNMP4mOPJynQ== + +chalk@4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +chalk@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.3.0.tgz#67c20a7ebef70e7f3970a01f90fa210cb6860385" + integrity sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w== + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +dotenv@^16.4.5: + version "16.4.5" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f" + integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +memory-pager@^1.0.2: + version "1.5.0" + resolved "https://registry.yarnpkg.com/memory-pager/-/memory-pager-1.5.0.tgz#d8751655d22d384682741c972f2c3d6dfa3e66b5" + integrity sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg== + +mongodb-connection-string-url@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.1.tgz#c13e6ac284ae401752ebafdb8cd7f16c6723b141" + integrity sha512-XqMGwRX0Lgn05TDB4PyG2h2kKO/FfWJyCzYQbIhXUxz7ETt0I/FqHjUeqj37irJ+Dl1ZtU82uYyj14u2XsZKfg== + dependencies: + "@types/whatwg-url" "^11.0.2" + whatwg-url "^13.0.0" + +mongodb@^6.8.0: + version "6.8.1" + resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-6.8.1.tgz#3f3a663e296446e412e26d8769315e36945a70fe" + integrity sha512-qsS+gl5EJb+VzJqUjXSZ5Y5rbuM/GZlZUEJ2OIVYP10L9rO9DQ0DGp+ceTzsmoADh6QYMWd9MSdG9IxRyYUkEA== + dependencies: + "@mongodb-js/saslprep" "^1.1.5" + bson "^6.7.0" + mongodb-connection-string-url "^3.0.0" + +punycode@^2.3.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== + +sparse-bitfield@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz#ff4ae6e68656056ba4b3e792ab3334d38273ca11" + integrity sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ== + dependencies: + memory-pager "^1.0.2" + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +tr46@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-4.1.1.tgz#281a758dcc82aeb4fe38c7dfe4d11a395aac8469" + integrity sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw== + dependencies: + punycode "^2.3.0" + +webidl-conversions@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a" + integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g== + +whatwg-url@^13.0.0: + version "13.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-13.0.0.tgz#b7b536aca48306394a34e44bda8e99f332410f8f" + integrity sha512-9WWbymnqj57+XEuqADHrCJ2eSXzn8WXIW/YSGaZtb2WKAInQ6CHfaUUcTyyver0p8BDg5StLQq8h1vtZuwmOig== + dependencies: + tr46 "^4.1.1" + webidl-conversions "^7.0.0" diff --git a/src/components/ServerList.tsx b/src/components/ServerList.tsx index ae3fa3a..3fb9733 100644 --- a/src/components/ServerList.tsx +++ b/src/components/ServerList.tsx @@ -227,7 +227,7 @@ export default function ServerList() {
<> {(!isSignedIn || hero) && ( -
+