commit 0b8db88e2b34ae5c8b6b12496037a358bc48b154 Author: sophie Date: Mon Jul 1 13:30:52 2024 +0300 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c686e05 --- /dev/null +++ b/.gitignore @@ -0,0 +1,177 @@ +# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore + +# Logs + +logs +_.log +npm-debug.log_ +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Caches + +.cache + +# Diagnostic reports (https://nodejs.org/api/report.html) + +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# Runtime data + +pids +_.pid +_.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover + +lib-cov + +# Coverage directory used by tools like istanbul + +coverage +*.lcov + +# nyc test coverage + +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) + +.grunt + +# Bower dependency directory (https://bower.io/) + +bower_components + +# node-waf configuration + +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) + +build/Release + +# Dependency directories + +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) + +web_modules/ + +# TypeScript cache + +*.tsbuildinfo + +# Optional npm cache directory + +.npm + +# Optional eslint cache + +.eslintcache + +# Optional stylelint cache + +.stylelintcache + +# Microbundle cache + +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history + +.node_repl_history + +# Output of 'npm pack' + +*.tgz + +# Yarn Integrity file + +.yarn-integrity + +# dotenv environment variable files + +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) + +.parcel-cache + +# Next.js build output + +.next +out + +# Nuxt.js build / generate output + +.nuxt +dist + +# Gatsby files + +# Comment in the public line in if your project uses Gatsby and not Next.js + +# https://nextjs.org/blog/next-9-1#public-directory-support + +# public + +# vuepress build output + +.vuepress/dist + +# vuepress v2.x temp and cache directory + +.temp + +# Docusaurus cache and generated files + +.docusaurus + +# Serverless directories + +.serverless/ + +# FuseBox cache + +.fusebox/ + +# DynamoDB Local files + +.dynamodb/ + +# TernJS port file + +.tern-port + +# Stores VSCode versions used for testing VSCode extensions + +.vscode-test + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store +config.json +minecraft.otf \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..a941ca6 --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# see_leaderboard_bot + +Please set your configuration options in `config.json`. Copy `config.example.json` and make it config.json and replace everything. +Add minecraft's font as `minecraft.otf`. +Run index.ts (also `bun i`). Write ".lb" in chat and it'll show you the leaderboard. + +This is made for BACAP's "bac_advancements" leaderboard. +Just change all mentions of "Advancements" or "bac_advancements" and you'll have a solution. + +This app uses the whitelist to get every player that's playing. If you have a better idea, contact me with my socials from [sad.ovh](https://sad.ovh). You can also modify the AdvancementAPI to make it work differently. To your choosing. + +This project was created using `bun init` in bun v1.1.13. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. diff --git a/advancementAPI.ts b/advancementAPI.ts new file mode 100644 index 0000000..7a4f83f --- /dev/null +++ b/advancementAPI.ts @@ -0,0 +1,77 @@ +//@ts-nocheck +import Rcon from "ts-rcon"; +import config from "./config.json" assert { type: "json" }; + +const advancements = new Map(); +let advancementsRequired = 0; +let authenicated = false; + +const advancementRegex = /^([^ ]*) has (\d*) \[Advancements\]$/gm; +const playerRegex = /There are \d* whitelisted player\(s\): /gm; +const noAdvancements = + /^Can't get value of bac_advancements for ([^;]*); none is set$/gm; + +const client = new Rcon( + config.rcon.host, + config.rcon.port, + config.rcon.password +); + +client.on("connect", () => { + console.log("RCON connected."); +}); + +client.on("auth", () => { + console.log("RCON authenicated."); + authenicated = true; +}); + +client.on("response", (a: string) => { + if (playerRegex.test(a)) { + const whitelistedPlayers = a.replace(playerRegex, "").split(", "); + advancementsRequired = whitelistedPlayers.length; + + for (let i = 0; i < whitelistedPlayers.length; i++) { + setTimeout(() => { + const name = whitelistedPlayers[i]; + client.send(`scoreboard players get ${name} bac_advancements`, 0x02); + }, i * 20); + } + } + + if (advancementRegex.test(a)) { + advancements.set( + a.replace(advancementRegex, "$1"), + +a.replace(advancementRegex, "$2") + ); + } + if (noAdvancements.test(a)) { + advancements.set(a.replace(noAdvancements, "$1"), 0); + } + if (advancements.size == advancementsRequired) { + client.emit("done"); + } +}); + +export function getAdvancements(): Promise> { + return new Promise(async (res, rej) => { + if (!authenicated) { + await new Promise((res2, rej2) => { + client.once("auth", () => { + res2(""); + }); + client.once("error", () => { + rej2(); + }); + }); + } + advancementsRequired = 0; + advancements.clear(); + client.send("whitelist list", 0x02); + client.once("done", () => { + res(advancements); + }); + }); +} + +client.connect(); diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000..5761d1e Binary files /dev/null and b/bun.lockb differ diff --git a/config.example.json b/config.example.json new file mode 100644 index 0000000..c21c2d4 --- /dev/null +++ b/config.example.json @@ -0,0 +1,8 @@ +{ + "token": "discord bot token", + "rcon": { + "port": 25567, + "host": "my.server.com", + "password": "very secure password" + } +} diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..921df02 --- /dev/null +++ b/index.ts @@ -0,0 +1,62 @@ +import { getAdvancements } from "./advancementAPI"; +import { Client, Events, GatewayIntentBits } from "discord.js"; +import { createCanvas, GlobalFonts } from "@napi-rs/canvas"; +import config from "./config.json" assert { type: "json" }; + +GlobalFonts.registerFromPath("minecraft.otf", "Minecraft"); + +const client = new Client({ + intents: [ + GatewayIntentBits.Guilds, + GatewayIntentBits.GuildMessages, + GatewayIntentBits.MessageContent, + ], +}); + +async function makeCanvas(): Promise { + const canvas = createCanvas(170, 255); + const ctx = canvas.getContext("2d"); + + ctx.fillRect(0, 0, 200, 400); + ctx.fillStyle = "white"; + ctx.font = "15px Minecraft"; + + { + const width = ctx.measureText("Advancements").width; + + ctx.fillText("Advancements", canvas.width / 2 - width / 2, 15); + } + + const advancements = [...(await getAdvancements()).entries()]; + advancements.sort((a, b) => b[1] - a[1]); + + for (let i = 0; i < Math.min(advancements.length, 15); i++) { + const n = advancements[i]; + + // render name + const firstOffset = (i + 2) * 15; + ctx.fillText(n[0], 5, firstOffset); + + // render number + const secondOffset = ctx.measureText("" + n[1]).width; + ctx.fillStyle = "yellow"; + ctx.fillText("" + n[1], canvas.width - secondOffset - 5, firstOffset); + ctx.fillStyle = "white"; + } + + return canvas.toBuffer("image/png"); +} + +client.once(Events.ClientReady, async (readyClient) => { + console.log(`DISCORD connected. ${readyClient.user.tag}`); +}); + +client.on("messageCreate", async (message) => { + if (message.content == ".lb") { + message.channel.send({ + files: [await makeCanvas()], + }); + } +}); + +client.login(config.token); diff --git a/package.json b/package.json new file mode 100644 index 0000000..8aa3a36 --- /dev/null +++ b/package.json @@ -0,0 +1,19 @@ +{ + "name": "see_leaderboard_bot", + "module": "index.ts", + "type": "module", + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "dependencies": { + "@napi-rs/canvas": "^0.1.53", + "bufferutil": "^4.0.8", + "discord.js": "^14.15.3", + "ts-rcon": "^1.2.1", + "utf-8-validate": "^6.0.4", + "zlib-sync": "^0.1.9" + } +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..238655f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + // Enable latest features + "lib": ["ESNext", "DOM"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +}