diff --git a/.gitignore b/.gitignore index 9829184..6d43082 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,12 @@ # Ignore the main node_modules directory node_modules/ -build/ \ No newline at end of file +config.js +.build/ +build/ +dist/ +*.zip +*-cache.json + +.vs/ +local.env +.env \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..cd309fd --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,130 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "command": "npm run build", + "name": "Run npm build", + "request": "launch", + "type": "node-terminal" + }, + { + "type": "node", + "request": "launch", + "name": "Launch Program", + "skipFiles": [ + "/**" + ], + "program": "${file}", + "outFiles": [ + "${workspaceFolder}/**/*.js" + ] + }, + { + "type": "node", + "request": "launch", + "name": "Launch ThirdEye", + "skipFiles": [ + "/**" + ], + "program": "${workspaceFolder}/src/start.ts", + "runtimeArgs": [ + "--loader", + "ts-node/esm" + ], + "outFiles": [ + "${workspaceFolder}/**/*.js" + ], + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen" + }, + { + "type": "node", + "request": "launch", + "name": "Launch Compiled Version", + "skipFiles": [ + "/**" + ], + "program": "${workspaceFolder}/.build/start.js", + "outFiles": [ + "${workspaceFolder}/.build/**/*.js" + ], + "console": "integratedTerminal", + "preLaunchTask": "npm: build" + }, + { + "type": "node", + "request": "launch", + "name": "Build (Linux)", + "skipFiles": [ + "/**" + ], + "runtimeExecutable": "npm", + "runtimeArgs": [ + "run", + "build" + ], + "console": "integratedTerminal" + }, + { + "type": "node", + "request": "launch", + "name": "Build (Windows)", + "skipFiles": [ + "/**" + ], + "runtimeExecutable": "npm", + "runtimeArgs": [ + "run", + "build_win" + ], + "console": "integratedTerminal" + }, + { + "type": "node", + "request": "launch", + "name": "Bundle (Linux)", + "skipFiles": [ + "/**" + ], + "runtimeExecutable": "npm", + "runtimeArgs": [ + "run", + "bundle" + ], + "console": "integratedTerminal" + }, + { + "type": "node", + "request": "launch", + "name": "Bundle (Windows)", + "skipFiles": [ + "/**" + ], + "runtimeExecutable": "npm", + "runtimeArgs": [ + "run", + "bundle_win" + ], + "console": "integratedTerminal" + } + ], + "compounds": [ + { + "name": "Build and Run", + "configurations": [ + "Build (Linux)", + "Launch Compiled Version" + ] + }, + { + "name": "Build (Windows) and Run", + "configurations": [ + "Build (Windows)", + "Launch Compiled Version" + ] + } + ] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..b6475d4 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,18 @@ +{ + "version": "2.0.0", + "command": "npm", + "isShellCommand": true, + "suppressTaskName": true, + "tasks": [ + { + "command": "npm", + "taskName": "build", + "label": "build", + "isBuildCommand": true, + "args": [ + "run", + "build" + ] + } + ] +} \ No newline at end of file diff --git a/README.md b/README.md index 6b2b6b7..c5d25d9 100644 --- a/README.md +++ b/README.md @@ -110,3 +110,46 @@

Kvr#7119: Original Logo

+ +## Building and Publishing + +### Building the Project + +For Linux: +```bash +npm run build +npm run bundle +``` + +For Windows: +```bash +npm run build_win +npm run bundle_win +``` + +### Publishing a Release to GitHub + +To publish a new release to GitHub, you need to have the GitHub CLI installed and authenticated. + +For Linux: +```bash +npm run publish +``` + +For Windows: +```bash +npm run publish:win +``` + +This will: +1. Build the project +2. Create a zip file with the build artifacts +3. Create a GitHub release with the current version number +4. Upload the zip file to the release + +You can also use the following commands to increment the version number and then build: +```bash +npm run release:patch # Increment patch version (1.0.0 -> 1.0.1) +npm run release:minor # Increment minor version (1.0.0 -> 1.1.0) +npm run release:major # Increment major version (1.0.0 -> 2.0.0) +``` diff --git a/local.env.example b/local.env.example new file mode 100644 index 0000000..bd6c4cc --- /dev/null +++ b/local.env.example @@ -0,0 +1,43 @@ +# Discord Bot Settings +TOKEN=your_discord_bot_token +GUILD=your_discord_guild_id +CHANNEL=your_discord_channel_id + +# Minecraft Settings +USERNAME=your_minecraft_username +IS_REALM=false +REALM_INVITE_CODE= +IP=localhost +PORT=19132 + +# Anti-Cheat Settings +ANTI_CHEAT_ENABLED=true +ANTI_CHEAT_CHANNEL_ID=your_anticheat_channel_id +ANTI_CHEAT_LOGS_CHANNEL=your_anticheat_logs_channel_id + +# Discord Command Settings +CMD_PREFIX=! +LOG_SYSTEM_COMMANDS=false +SYSTEM_COMMANDS_CHANNEL=your_system_commands_channel_id + +# Appearance Settings +USE_EMBED=true +SET_COLOR=0,153,255 +SET_TITLE=My Servers Name! +LOGO_URL=https://i.imgur.com/XfoZ8XS.jpg + +# Voice Chat Settings +VOICE_CHANNEL_COMMAND_PREFIX=$ +VOICE_CHANNELS_CATEGORY=Voice Channels +VOICE_ADMIN_ROLE_ID=your_admin_role_id + +# Security Settings +AUTH_TYPE=false +ADMINS=admin1_id,admin2_id +BLACKLIST_DEVICE_TYPES= +LOG_BAD_ACTORS=true + +# Misc Settings +DEBUG=false +USE_SYSTEM_PLAYER_JOIN_MESSAGE=false +SEND_WHISPER_MESSAGES=false diff --git a/package-lock.json b/package-lock.json index 6956015..d896b26 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,20 +1,23 @@ { "name": "thirdeye", - "version": "1.0.9", + "version": "1.1.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "thirdeye", - "version": "1.0.9", + "version": "1.1.3", + "dependencies": { + "dotenv": "^16.4.7" + }, "devDependencies": { - "@types/node": "20.14.2 ", - "bedrock-protocol": "3.39.0", - "discord-api-types": "0.37.89", + "@types/node": "22.10.1 ", + "bedrock-protocol": "3.43.0", + "discord-api-types": "0.37.110", "discord.js": "14.15.3", - "minecraft-data": "3.68.0", - "prettier": "3.3.2", - "typescript": "5.4.5" + "minecraft-data": "3.84.1", + "prettier": "3.4.2", + "typescript": "5.7.2" } }, "node_modules/@azure/msal-common": { @@ -231,12 +234,13 @@ } }, "node_modules/@types/node": { - "version": "20.14.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.2.tgz", - "integrity": "sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q==", + "version": "22.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.1.tgz", + "integrity": "sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==", "dev": true, + "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.20.0" } }, "node_modules/@types/ws": { @@ -361,10 +365,11 @@ "dev": true }, "node_modules/bedrock-protocol": { - "version": "3.39.0", - "resolved": "https://registry.npmjs.org/bedrock-protocol/-/bedrock-protocol-3.39.0.tgz", - "integrity": "sha512-mP69Em9ru5+dn3XPFTRBGTQ6BPJJh4Tux51dEzoWKs/j2awBw+HpyP0VGnyniOX/usNykM60oaSIF/0nIneF+A==", + "version": "3.43.0", + "resolved": "https://registry.npmjs.org/bedrock-protocol/-/bedrock-protocol-3.43.0.tgz", + "integrity": "sha512-f/GG8uckdAryZ59wc2fRN4Gml1tYKiXt+HgSWnqhxKqA7kNuSwFkwldUy8VKUskVAfAcg1gzEJS9bBz5BFoz9g==", "dev": true, + "license": "MIT", "dependencies": { "debug": "^4.3.1", "jsonwebtoken": "^9.0.0", @@ -619,10 +624,11 @@ "dev": true }, "node_modules/discord-api-types": { - "version": "0.37.89", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.89.tgz", - "integrity": "sha512-+hiYDULKma/0B7Zd9/IYK0XnYlYhWSsPYKNahUk33QEy6yuHdGwlglIfbiTBn683cmJ44IFnFYZa8UX1Ka+kDg==", - "dev": true + "version": "0.37.110", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.110.tgz", + "integrity": "sha512-wVaAJkrSgNRo8nd523qKYPqkClTNHhjKOk/g6265rzHuc7TNS6Ivz06DPW4iZvnhFobbH95hKlgsRf6jcAbtlA==", + "dev": true, + "license": "MIT" }, "node_modules/discord.js": { "version": "14.15.3", @@ -656,6 +662,18 @@ "integrity": "sha512-urGGYeWtWNYMKnYlZnOnDHm8fVRffQs3U0SpE8RHeiuLKb/u92APS8HoQnPTFbnXmY1vVnXjXO4dOxcAn3J+DA==", "dev": true }, + "node_modules/dotenv": { + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/duplexer2": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", @@ -1155,10 +1173,11 @@ "dev": true }, "node_modules/minecraft-data": { - "version": "3.68.0", - "resolved": "https://registry.npmjs.org/minecraft-data/-/minecraft-data-3.68.0.tgz", - "integrity": "sha512-pNBTi39a1zbFpN9itwi0YSL3hqAsSw38D7pE9C6m+aURmXljpBlNTO+TkpZxxDv4KqqtNBOhmkj4x46IDW6R+Q==", - "dev": true + "version": "3.84.1", + "resolved": "https://registry.npmjs.org/minecraft-data/-/minecraft-data-3.84.1.tgz", + "integrity": "sha512-0yPsnu4rYjbokPgm6aMqhIm70fhsUUYFMEbbqrLG7QGLQDUy3lauuVlh3ctRxtPP6vX/ywLo1p5Uczz3Snnocg==", + "dev": true, + "license": "MIT" }, "node_modules/minecraft-folder-path": { "version": "1.2.0", @@ -1307,10 +1326,11 @@ } }, "node_modules/prettier": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.2.tgz", - "integrity": "sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz", + "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==", "dev": true, + "license": "MIT", "bin": { "prettier": "bin/prettier.cjs" }, @@ -1621,10 +1641,11 @@ "dev": true }, "node_modules/typescript": { - "version": "5.4.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", - "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", "dev": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -1643,10 +1664,11 @@ } }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" }, "node_modules/universalify": { "version": "0.1.2", diff --git a/package.json b/package.json index 86e26c2..84970fc 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { "name": "thirdeye", - "version": "1.0.11", + "version": "1.1.3", "productName": "ThirdEye", - "description": "Two way chat between Discord and Minecraft Bedrock along with logs for Anticheats.", + "description": "Two way chat between Discord and Minecraft Bedrock", "type": "module", "private": true, "devDependencies": { @@ -12,7 +12,7 @@ "prettier": "3.4.2", "typescript": "5.7.2", "discord-api-types": "0.37.110", - "minecraft-data" : "3.84.1" + "minecraft-data": "3.84.1" }, "prettier": { "trailingComma": "es5", @@ -24,14 +24,24 @@ "scripts": { "format": "npx prettier --write --ignore-path .prettierignore ./", "linux//": " --- BUILD (Linux) --- ", - "clean": "rm -rf build/", - "mkDirs": "mkdir -p build", - "copy:assets": "cp -R src/Install_NodeJS_Modules.bat src/Run.bat src/whitelist.json package.json LICENSE README.md build", - "build": "npm run clean; ./node_modules/typescript/bin/tsc -p tsconfig.json; npm run copy:assets; node modify-package.js", + "clean": "rm -rf .build/", + "mkDirs": "mkdir -p .build", + "copy:assets": "cp -R src/Install_NodeJS_Modules.bat src/Run.bat src/whitelist.json package.json LICENSE README.md .build", + "build": "npm run clean; npm run mkDirs; ./node_modules/typescript/bin/tsc -p tsconfig.json; npm run copy:assets; node modify-package.js", "windows//": " --- BUILD (Windows) --- ", - "clean_win": "rd /s /q build>nul 2>&1|echo.>nul", - "mkdir_win": "@if exist build (rd /s /q build && mkdir build) else (mkdir build)", - "copy_assets_win": "@powershell Copy-Item -Path ^(\\\"src\\Install_NodeJS_Modules.bat\\\",\\\"src\\Run.bat\\\",\\\"src\\whitelist.json\\\",\\\"package.json\\\",\\\"LICENSE\\\",\\\"README.md\\\"^) -Destination \"build\" -Recurse", - "build_win": "npm run clean_win && npm run mkdir_win 1>nul && node node_modules\\typescript\\bin\\tsc -p tsconfig.json && npm run copy_assets_win 1>nul && node modify-package.js" + "clean_win": "rd /s /q .build>nul 2>&1|echo.>nul", + "mkdir_win": "@if exist .build (rd /s /q .build && mkdir .build) else (mkdir .build)", + "copy_assets_win": "@powershell Copy-Item -Path ^(\\\"src\\Install_NodeJS_Modules.bat\\\",\\\"src\\Run.bat\\\",\\\"src\\whitelist.json\\\",\\\"package.json\\\",\\\"LICENSE\\\",\\\"README.md\\\"^) -Destination \".build\" -Recurse", + "build_win": "npm run clean_win && npm run mkdir_win 1>nul && node node_modules\\typescript\\bin\\tsc -p tsconfig.json && npm run copy_assets_win 1>nul && node modify-package.js", + "bundle": "npm run build && mkdir -p dist && (cd .build && zip -r ../dist/thirdeye-v$(node -p \"require('./package.json').version\").zip .) && zip -j dist/thirdeye-v$(node -p \"require('./package.json').version\").zip local.env.example", + "bundle_win": "npm run build_win && if not exist dist mkdir dist && powershell -Command \"Compress-Archive -Path .build\\*, local.env.example -DestinationPath dist/thirdeye-v$((Get-Content package.json | ConvertFrom-Json).version).zip -Force\"", + "release:patch": "npm version patch && npm run bundle", + "release:minor": "npm version minor && npm run bundle", + "release:major": "npm version major && npm run bundle", + "publish": "npm run bundle && gh release create v$(node -p \"require('./package.json').version\") --title \"ThirdEye v$(node -p \"require('./package.json').version\")\" --generate-notes ./dist/thirdeye-v$(node -p \"require('./package.json').version\").zip", + "publish:win": "npm run bundle_win && gh release create v%npm_package_version% --title \"ThirdEye v%npm_package_version%\" --generate-notes ./dist/thirdeye-v%npm_package_version%.zip" + }, + "dependencies": { + "dotenv": "^16.4.7" } } diff --git a/src/anticheat_listener/anticheat_logs.ts b/src/anticheat_listener/anticheat_logs.ts index f8395f7..dff2861 100644 --- a/src/anticheat_listener/anticheat_logs.ts +++ b/src/anticheat_listener/anticheat_logs.ts @@ -1,7 +1,7 @@ import { EmbedBuilder, MessageCreateOptions, MessagePayload, TextBasedChannel } from "discord.js"; import config from "../config.js"; import { autoCorrect } from "../main.js"; -import { correction } from "../main.js"; +// import { correction } from "../main.js"; import { Client } from "bedrock-protocol"; let thumbUrl: string; export function setupAntiCheatListener(bot: Client, channelId: TextBasedChannel) { @@ -37,10 +37,10 @@ export function setupAntiCheatListener(bot: Client, channelId: TextBasedChannel) rawText.includes("§l§o§6[§4Paradox AntiCheat Command Help§6]§r§o") ) { antiCheatMsg = rawText; - correctedText = autoCorrect(antiCheatMsg, correction); + correctedText = autoCorrect(antiCheatMsg); } else if (rawText.startsWith("§r§6[§aScythe§6]§r")) { antiCheatMsg = rawText; - correctedText = autoCorrect(antiCheatMsg, correction); + correctedText = autoCorrect(antiCheatMsg); } if (correctedText) { diff --git a/src/config.ts b/src/config.ts index d622867..2f4df28 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,4 +1,37 @@ -export default { +// Define config as a type to serve as a template +export type ConfigTemplate = { + debug: boolean; + token: string; + username: string; + isRealm: boolean; + realmInviteCode: string; + ip: string; + port: number; + guild: string; + channel: string; + antiCheatEnabled: boolean; + antiCheatChannelId: string; + antiCheatLogsChannel: string; + cmdPrefix: string; + useSystemPlayerJoinMessage: boolean; + logSystemCommands: boolean; + systemCommandsChannel: string; + sendWhisperMessages: boolean; + useEmbed: boolean; + setColor: readonly [number, number, number]; + setTitle: string; + AuthType: boolean; + admins: string[]; + blacklistDeviceTypes: string[]; + voiceChannelCommandPrefix: string; + voiceChannelsCategory: string; + voiceAdminRoleID: string; + logBadActors: boolean; + logoURL: string; +}; + +// Default configuration used as fallback +export const defaultConfig: ConfigTemplate = { debug: false, token: "", username: "", @@ -9,6 +42,7 @@ export default { guild: "", channel: "", antiCheatEnabled: true, + antiCheatChannelId: "", antiCheatLogsChannel: "", cmdPrefix: "!", useSystemPlayerJoinMessage: false, @@ -34,3 +68,5 @@ export default { //New logo image if you dont like it feel free to change it. logoURL: "https://i.imgur.com/XfoZ8XS.jpg", }; + +export default defaultConfig; diff --git a/src/configLoader.ts b/src/configLoader.ts new file mode 100644 index 0000000..2e4a95d --- /dev/null +++ b/src/configLoader.ts @@ -0,0 +1,111 @@ +import { readFileSync, existsSync } from "fs"; +import { ConfigTemplate, defaultConfig } from "./config.js"; +import path from "path"; +import * as dotenv from "dotenv"; + +/** + * Load configuration from local.env file + */ +export function loadConfig(): ConfigTemplate { + // Define the path to local.env + const envPath = path.join(process.cwd(), "local.env"); + + // Check if local.env exists + if (!existsSync(envPath)) { + console.warn("local.env file not found. Using default configuration."); + return defaultConfig; + } + + // Load environment variables from local.env + const envConfig = dotenv.parse(readFileSync(envPath)); + + // Create config by merging default with environment variables + const config: ConfigTemplate = { + ...defaultConfig, + debug: parseBoolean(envConfig.DEBUG, defaultConfig.debug), + token: envConfig.TOKEN || defaultConfig.token, + username: envConfig.USERNAME || defaultConfig.username, + isRealm: parseBoolean(envConfig.IS_REALM, defaultConfig.isRealm), + realmInviteCode: envConfig.REALM_INVITE_CODE || defaultConfig.realmInviteCode, + ip: envConfig.IP || defaultConfig.ip, + port: parseInt(envConfig.PORT || String(defaultConfig.port)), + guild: envConfig.GUILD || defaultConfig.guild, + channel: envConfig.CHANNEL || defaultConfig.channel, + antiCheatEnabled: parseBoolean(envConfig.ANTI_CHEAT_ENABLED, defaultConfig.antiCheatEnabled), + antiCheatChannelId: envConfig.ANTI_CHEAT_CHANNEL_ID || defaultConfig.antiCheatChannelId, + antiCheatLogsChannel: envConfig.ANTI_CHEAT_LOGS_CHANNEL || defaultConfig.antiCheatLogsChannel, + cmdPrefix: envConfig.CMD_PREFIX || defaultConfig.cmdPrefix, + useSystemPlayerJoinMessage: parseBoolean(envConfig.USE_SYSTEM_PLAYER_JOIN_MESSAGE, defaultConfig.useSystemPlayerJoinMessage), + logSystemCommands: parseBoolean(envConfig.LOG_SYSTEM_COMMANDS, defaultConfig.logSystemCommands), + systemCommandsChannel: envConfig.SYSTEM_COMMANDS_CHANNEL || defaultConfig.systemCommandsChannel, + sendWhisperMessages: parseBoolean(envConfig.SEND_WHISPER_MESSAGES, defaultConfig.sendWhisperMessages), + useEmbed: parseBoolean(envConfig.USE_EMBED, defaultConfig.useEmbed), + setColor: parseColor(envConfig.SET_COLOR, defaultConfig.setColor), + setTitle: envConfig.SET_TITLE || defaultConfig.setTitle, + AuthType: parseBoolean(envConfig.AUTH_TYPE, defaultConfig.AuthType), + admins: parseArray(envConfig.ADMINS, defaultConfig.admins), + blacklistDeviceTypes: parseArray(envConfig.BLACKLIST_DEVICE_TYPES, defaultConfig.blacklistDeviceTypes), + voiceChannelCommandPrefix: envConfig.VOICE_CHANNEL_COMMAND_PREFIX || defaultConfig.voiceChannelCommandPrefix, + voiceChannelsCategory: envConfig.VOICE_CHANNELS_CATEGORY || defaultConfig.voiceChannelsCategory, + voiceAdminRoleID: envConfig.VOICE_ADMIN_ROLE_ID || defaultConfig.voiceAdminRoleID, + logBadActors: parseBoolean(envConfig.LOG_BAD_ACTORS, defaultConfig.logBadActors), + logoURL: envConfig.LOGO_URL || defaultConfig.logoURL, + }; + + // Validate required configurations + validateConfig(config); + + return config; +} + +/** + * Parse a string to boolean + */ +function parseBoolean(value: string | undefined, defaultValue: boolean): boolean { + if (value === undefined) return defaultValue; + return value.toLowerCase() === "true"; +} + +/** + * Parse a string to an array + */ +function parseArray(value: string | undefined, defaultValue: string[]): string[] { + if (value === undefined) return defaultValue; + return value.split(",").map((item) => item.trim()); +} + +/** + * Parse a color string to RGB array + */ +function parseColor(value: string | undefined, defaultValue: readonly [number, number, number]): readonly [number, number, number] { + if (value === undefined) return defaultValue; + + try { + const colorArray = value.split(",").map((n) => parseInt(n.trim())); + if (colorArray.length === 3 && colorArray.every((n) => !isNaN(n) && n >= 0 && n <= 255)) { + return [colorArray[0], colorArray[1], colorArray[2]] as const; + } + } catch (error) { + console.warn(`Invalid color format. Using default.`); + } + + return defaultValue; +} + +/** + * Validate that critical config values are set + */ +function validateConfig(config: ConfigTemplate): void { + const criticalFields: Array<{ field: keyof ConfigTemplate; name: string }> = [ + { field: "token", name: "Discord Bot Token" }, + { field: "username", name: "Minecraft Username" }, + { field: "guild", name: "Discord Guild ID" }, + { field: "channel", name: "Discord Channel ID" }, + ]; + + const missingFields = criticalFields.filter(({ field }) => !config[field]).map(({ name }) => name); + + if (missingFields.length > 0) { + console.warn(`Warning: The following critical configuration values are missing: ${missingFields.join(", ")}`); + } +} diff --git a/src/main.ts b/src/main.ts index 4a6af83..1469bc2 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,7 +1,8 @@ import { readFileSync, writeFileSync } from "fs"; -import { Client, GatewayIntentBits, EmbedBuilder, TextBasedChannel, Guild } from "discord.js"; -import { createClient, ClientOptions } from "bedrock-protocol"; -import config from "./config.js"; +import { Client, GatewayIntentBits, EmbedBuilder, ColorResolvable } from "discord.js"; +import { createClient } from "bedrock-protocol"; +import { defaultConfig, ConfigTemplate } from "./config.js"; +import { loadConfig } from "./configLoader.js"; import { setupDeathListener } from "./death_listener/deathMessage.js"; import { addPlayerListener } from "./player_device_listener/playerDeviceLogging.js"; import { setupSystemCommandsListener } from "./system_commands_listener/systemCommandsLogging.js"; @@ -10,68 +11,109 @@ import { checkAndDeleteEmptyChannels } from "./voiceChat_listener/voiceChatClean import { setupAntiCheatListener } from "./anticheat_listener/anticheat_logs.js"; import { idList } from "./badActors.js"; -const { MessageContent, GuildMessages, Guilds } = GatewayIntentBits; -const channel: string = config.channel; -let channelId: TextBasedChannel; -const token = config.token; -const anticheatChannel: string = config.antiCheatLogsChannel; -let anticheatChannelId: TextBasedChannel; -const systemCommandsChannel: string = config.systemCommandsChannel; -let systemCommandsChannelId: TextBasedChannel; -const anticheatLogs = config.antiCheatEnabled; -const cmdPrefix = config.cmdPrefix; -const logSystemCommands = config.logSystemCommands; -let clientPermissionLevel: string = ""; -let clientGamemode: string = ""; -let clientEntityID: BigInt; +/** + * Validates the loaded configuration against the default configuration structure + * @param config The loaded configuration + * @returns Validated configuration with any missing properties filled from default + */ +function validateConfig(config: Partial): ConfigTemplate { + const validatedConfig = { ...defaultConfig }; + let missingKeys: string[] = []; + let warningShown = false; + + // Check all keys in default config exist in loaded config + Object.keys(defaultConfig).forEach((key) => { + const typedKey = key as keyof ConfigTemplate; + if (config[typedKey] === undefined) { + missingKeys.push(key); + warningShown = true; + } else { + (validatedConfig[typedKey] as (typeof config)[typeof typedKey]) = config[typedKey]; + } + }); + + // Log warnings for missing configuration + if (missingKeys.length > 0) { + console.warn(`⚠️ The following configuration keys are missing and will use default values: ${missingKeys.join(", ")}`); + } + + // Check for critical configuration values + const criticalConfigs = [ + { key: "token", value: validatedConfig.token, name: "Discord Bot Token" }, + { key: "username", value: validatedConfig.username, name: "Minecraft Username" }, + { key: "ip", value: validatedConfig.ip, name: "Server IP" }, + { key: "guild", value: validatedConfig.guild, name: "Discord Guild ID" }, + { key: "channel", value: validatedConfig.channel, name: "Discord Channel ID" }, + ]; + + const emptyConfigs = criticalConfigs.filter(({ value }) => !value).map(({ name }) => name); + + if (emptyConfigs.length > 0) { + console.error(`❌ ERROR: The following critical configuration values are empty: ${emptyConfigs.join(", ")}`); + console.error("Please update your local.env file with these values."); + process.exit(1); + } + + // Check for potentially problematic configurations + if (!validatedConfig.antiCheatEnabled && validatedConfig.logSystemCommands) { + console.warn("⚠️ System command logging is enabled but anti-cheat is disabled."); + } + + // Check for extra keys that don't exist in default config + const extraKeys = Object.keys(config).filter((key) => !Object.prototype.hasOwnProperty.call(defaultConfig, key)); + + if (extraKeys.length > 0) { + console.warn(`⚠️ Found unknown configuration keys: ${extraKeys.join(", ")}. These will be ignored.`); + } + + // Log successful validation if no warnings + if (!warningShown && extraKeys.length === 0) { + console.log("✅ Configuration validated successfully"); + } + + return validatedConfig; +} + +// Load configuration from local.env and validate it +const loadedConfig = loadConfig(); +const config = validateConfig(loadedConfig); + +const programName = "Phoneix Epsilon"; +let __GLOBALS = { + clientGamemode: "", +}; +const { MessageContent, GuildMessages, Guilds, GuildVoiceStates } = GatewayIntentBits; +const { channel, token, systemCommandsChannel, cmdPrefix, logSystemCommands, guild, admins, antiCheatLogsChannel } = config; + +// Import TextChannel type +import { TextBasedChannel } from "discord.js"; + +// Typed channel variables +let channelId: TextBasedChannel, anticheatChannelId: TextBasedChannel, systemCommandsChannelId: TextBasedChannel; +const antiCheatEnabled = config.antiCheatEnabled; + export const correction = { - "§r§4[§6Paradox§4]§r": "Paradox", - "§4[§6Paradox§4]": "Paradox", - "§l§6[§4Paradox§6]§r": "Paradox", - "§4P": "P", - "§l": "", - "§r": "", - "§a": "", - "§b": "", - "§c": "", - "§d": "", - "§f": "", - "§9": "", - "§8": "", - "§7": "", - "§6": "", - "§5": "", - "§4": "", - "§3": "", - "§2": "", - "§1": "", - "§0": "", - "§o": "", - "§k": "", - "§¶": "", - "§r§6[§aScythe§6]§r": "", - "§f§4[§6Paradox§4]§f": "Paradox", - "\\n§l§o§6[§4Paradox AntiCheat Command Help§6]§r§o\\n": "Paradox AntiCheat Command Help", + // TODO: Add correction mappings }; let WhitelistRead = JSON.parse(readFileSync("whitelist.json", "utf-8")); -// create new discord client that can see what servers the bot is in, as well as the messages in those servers -const client = new Client({ intents: [Guilds, GuildMessages, MessageContent, "GuildVoiceStates"] }); +const client = new Client({ intents: [Guilds, GuildMessages, MessageContent, GuildVoiceStates] }); client.login(token); let options; + console.log("ThirdEye v1.0.10"); -// bot options + if (config.isRealm) { - //Handel the realm config here! console.log("Connecting to a realm"); options = { + host: "realm.bedrock.server", + port: 19132, + username: config.username, + offline: false, profilesFolder: "authentication_tokens", - realms: { - realmInvite: config.realmInviteCode, - }, - } as ClientOptions; + }; } else { console.log("Connecting to a server"); options = { @@ -80,507 +122,309 @@ if (config.isRealm) { username: config.username, offline: config.AuthType, profilesFolder: "authentication_tokens", - } as ClientOptions; + }; } -// join server + const bot = createClient(options); bot.on("spawn", () => { console.log(`Bedrock bot logged in as ${config.username}`); - if (config.useEmbed === true) { - const msgEmbed = new EmbedBuilder() - .setColor(config.setColor) - .setTitle(config.setTitle) - .setDescription("[ThirdEye]:" + " Client is logged in.") - .setAuthor({ name: "‎", iconURL: config.logoURL }); - - if (typeof anticheatChannelId === "object") { - return anticheatChannelId.send({ embeds: [msgEmbed] }); - } else { - return console.log("I could not find the paradoxLogs channel in Discord. 1"); - } - } else { - if (typeof anticheatChannelId === "object") { - return anticheatChannelId.send("[ThirdEye]: Client is logged in."); - } else { - return console.log("I could not find the paradoxLogs channel in Discord. 2"); - } - } + sendMessageToChannel(anticheatChannelId, `[${programName}]: Client is logged in.`); }); -/**when this packet is sent it contains the clients entityID which will be used to verify if the bot has op status - *and has been able to enter into creative mode - */ -bot.on("start_game", (packet: StartGame) => { - clientPermissionLevel = packet.permission_level.toString(); - clientGamemode = packet.player_gamemode.toString(); + +bot.on("start_game", (packet) => { + __GLOBALS.clientGamemode = packet.player_gamemode.toString(); }); -//bot -// when discord client is ready, send login message -client.once("ready", (c) => { + +client.once("ready", async (c) => { console.log(`Discord bot logged in as ${c.user.tag}`); - const channelObj = client.channels.cache.get(channel); - if (channelObj) { - channelId = channelObj as TextBasedChannel; - // Call function if channel exists + console.log("Channel ID from config:", channel); + + try { + const fetchedChannel = await client.channels.fetch(channel); + if (!fetchedChannel?.isTextBased()) { + throw new Error("Channel is not text-based"); + } + channelId = fetchedChannel as TextBasedChannel; + const channelName = "name" in fetchedChannel ? fetchedChannel.name : "Direct Message"; + console.log(`Found channel: ${channelName} (ID: ${channelId.id})`); setupDeathListener(bot, channelId); addPlayerListener(bot, channelId, WhitelistRead); - } else { - console.log(`I could not find the in-game channel in Discord. 1`); - } + } catch (error) { + if (antiCheatEnabled) { + const channel = client.channels.cache.get(antiCheatLogsChannel); + if (channel?.isTextBased()) { + anticheatChannelId = channel; + setupAntiCheatListener(bot, anticheatChannelId); + } else { + console.log(`I could not find the paradoxLogs Channel in Discord.`); + } + } + if (logSystemCommands) { + const channel = client.channels.cache.get(systemCommandsChannel); + if (channel?.isTextBased()) { + systemCommandsChannelId = channel; + setupSystemCommandsListener(bot, systemCommandsChannelId); + } else { + console.log(`I could not find the systemLogs Channel in Discord.`); + } + } - if (anticheatLogs === true) { - const anticheatChannelObj = client.channels.cache.get(anticheatChannel); - if (anticheatChannelObj) { - anticheatChannelId = anticheatChannelObj as TextBasedChannel; - setupAntiCheatListener(bot, anticheatChannelId); - } else { - console.log(`I could not find the paradoxLogs Channel in Discord. 3`); + if (logSystemCommands) { + const channel = client.channels.cache.get(systemCommandsChannel); + if (channel?.isTextBased()) { + systemCommandsChannelId = channel; + setupSystemCommandsListener(bot, systemCommandsChannelId); + } else { + console.log(`I could not find the systemLogs Channel in Discord.`); + } } - } - if (logSystemCommands === true) { - const systemChannelObj = client.channels.cache.get(systemCommandsChannel); - if (systemChannelObj) { - systemCommandsChannelId = systemChannelObj as TextBasedChannel; - setupSystemCommandsListener(bot, systemCommandsChannelId); + const guildObj = client.guilds.cache.get(guild); + if (guildObj) { + console.log(`Found guild: ${guildObj.name}`); } else { - console.log(`I could not find the systemLogs Channel in Discord. 3`); + console.error(`Guild with ID ${guild} not found.`); } - } - if (!channel) { - console.log(`I could not find the in game channel in Discord. Not Ready?`); + setupVoiceChatListener(bot, guildObj); } - if (!anticheatChannel) { - console.log(`I could not find the paradoxLogs Channel in Discord. Not Ready?`); - } - //pass guild - const guild = client.guilds.cache.get(config.guild); - if (guild) { - console.log(`Found guild: ${guild.name}`); - } else { - console.error(`Guild with ID ${config.guild} not found.`); - } - //Voice command listener - setupVoiceChatListener(bot, guild as Guild); }); client.on("messageCreate", (message) => { - if (message.author.bot === true) { - /**This check will prevent a loop back message. - * If the incoming message is from a bot it will ignore it. - */ - } else { - //get the list if admins - const admins = config.admins; - if (message.content.startsWith(cmdPrefix) && !message.content.startsWith(cmdPrefix + "/") && admins.includes(message.author.id) && anticheatChannel && message.channel.id === anticheatChannelId.id) { - console.log("command received: " + message.content + " From: " + message.author.id); - bot.queue("text", { - type: "chat", - needs_translation: false, - source_name: config.username, - xuid: "", - platform_chat_id: "", - message: `${message.content}`, - filtered_message: "", - }); - return; - } - //Check to see if the user is running a minecraft slash command - if (message.content.startsWith(cmdPrefix + "/") && admins.includes(message.author.id) && anticheatChannel && message.channel.id === anticheatChannelId.id) { - console.log("command received: " + message.content + " From: " + message.author.id); - //remove the prefix data and create the command - let cmd = message.content.slice(2); - bot.queue("command_request", { - command: cmd, - origin: { - type: "player", - uuid: "", - request_id: "", - }, - internal: false, - version: 52, - }); + if (message.author.bot) return; - return; - } - - if (message.content.startsWith("$") && admins.includes(message.author.id) && anticheatChannel && message.channel.id === anticheatChannelId.id && !message.content.endsWith("-r") && !message.content.includes("$reboot")) { - //add the user to the whitelist. - const msg = message.content.replace("$", ""); - WhitelistRead.whitelist.push(msg); - writeFileSync("whitelist.json", JSON.stringify(WhitelistRead, null, 2), "utf-8"); - console.log("Data has been written to the file successfully."); - WhitelistRead = JSON.parse(readFileSync("whitelist.json", "utf-8")); - console.log("Reloaded contents:", WhitelistRead.whitelist); - return; - } - if (message.content.startsWith("$") && admins.includes(message.author.id) && anticheatChannel && message.channel.id === anticheatChannelId.id && message.content.endsWith("-r") && !message.content.includes("$reboot")) { - // remove the user from the whitelist. - const msg = message.content.replace("$", ""); - const msgdel = msg.replace("-r", ""); - console.log("Removing: " + msgdel + "from the whitelist."); - WhitelistRead.whitelist = WhitelistRead.whitelist.filter((name: string) => name !== msgdel); - writeFileSync("whitelist.json", JSON.stringify(WhitelistRead, null, 2), "utf-8"); - console.log("Data has been written to the file successfully."); - WhitelistRead = JSON.parse(readFileSync("whitelist.json", "utf-8")); - console.log("Reloaded contents:", WhitelistRead.whitelist); - return; - } - if (message.content === "$reboot" && admins.includes(message.author.id) && anticheatChannel && message.channel.id === anticheatChannelId.id) { - console.log("Forcing a re connect."); - process.exit(); // Exit the script - } - if (channel && message.channel.id === channelId.id) { - let cmd; - //Check to make sure the Discord User is not on the know bad actors ID list. - if (idList.includes(message.author.id)) { - //We will then send a command to the server to trigger the message sent in discord. - cmd = `/tellraw @a {"rawtext":[{"text":"§8[§9Discord§8] §4${message.author.username} (Known Hacker/Troll) : §f${message.content}"}]}`; - //If configured Log the message to anticheat channel. - if (config.logBadActors === true) { - const msgEmbed = new EmbedBuilder() - .setColor(config.setColor) - .setTitle(config.setTitle) - .setDescription("Message sent to the bot from Discord from Author: " + message.author.username + " Content: " + message.content + " Unique ID: " + message.author.id) - .setAuthor({ name: "‎", iconURL: config.logoURL }) - .setThumbnail("https://static.wikia.nocookie.net/minecraft_gamepedia/images/7/76/Impulse_Command_Block.gif/revision/latest?cb=20191017044126"); - anticheatChannelId.send({ embeds: [msgEmbed] }); - } - } else { - //We will then send a command to the server to trigger the message sent in discord. - cmd = `/tellraw @a {"rawtext":[{"text":"§8[§9Discord§8] §7${message.author.username}: §f${message.content}"}]}`; - } + const isAdmin = admins.includes(message.author.id); + const isAnticheatChannel = anticheatChannelId && message.channel.id === anticheatChannelId.id; - bot.queue("command_request", { - command: cmd, - origin: { - type: "player", - uuid: "", - request_id: "", - }, - internal: false, - version: 52, - }); - } + if (message.content.startsWith(cmdPrefix)) { + handleCommand(message, isAdmin, isAnticheatChannel); + } else if (message.content.startsWith("$")) { + handleWhitelistCommand(message, isAdmin, isAnticheatChannel); + } else if (channelId && message.channel.id === channelId.id) { + handleChatMessage(message); } }); + client.on("voiceStateUpdate", (newState) => { checkAndDeleteEmptyChannels(newState.guild); }); bot.on("close", () => { console.log("The server has closed the connection."); + handleDisconnection(); }); + bot.on("login", () => { console.log("Client has been authenticated by the server."); }); + bot.on("join", () => { console.log("The client is ready to receive game packets."); }); -bot.on("close", () => { - console.log("Client disconnected:", bot.entityId); - let remainingTime = 2 * 60; // 2 minutes in seconds, allowing for slower servers to reboot in time ready for a new connection - if (config.useEmbed === true) { - const msgEmbed = new EmbedBuilder() - .setColor(config.setColor) - .setTitle(config.setTitle) - .setDescription("[ThirdEye]:" + " The client has lost connection to the server and will initiate a reboot in: " + remainingTime + " Seconds") - .setAuthor({ name: "‎", iconURL: config.logoURL }); - if (typeof anticheatChannelId === "object") { - anticheatChannelId.send({ embeds: [msgEmbed] }); - } else { - return console.log("I could not find the paradoxLogs channel in Discord. 4"); - } - } else { - if (typeof anticheatChannelId === "object") { - anticheatChannelId.send(`[ThirdEye]: The client has lost connection to the server and will initiate a reboot in: **${remainingTime} ** Seconds`); - } else { - console.log("I could not find the paradoxLogs channel in Discord. 5"); - } - } - console.log(`Waiting for ${remainingTime} seconds before reconnecting client: ${client.application.name}`); +bot.on("text", (packet) => { + let message = packet.message; + const sender = packet.source_name; - const timer = setInterval(() => { - remainingTime--; - console.log(`Remaining time: ${remainingTime} seconds`); - if (remainingTime <= 5) { - if (config.useEmbed === true) { - const msgEmbed = new EmbedBuilder() - .setColor(config.setColor) - .setTitle(config.setTitle) - .setDescription("[ThirdEye]:" + " Client is rebooting in: " + remainingTime + " Seconds") - .setAuthor({ name: "‎", iconURL: config.logoURL }); - - if (typeof anticheatChannelId === "object") { - anticheatChannelId.send({ embeds: [msgEmbed] }); - } else { - return console.log("I could not find the paradoxLogs channel in Discord. 6"); - } - } else { - if (typeof anticheatChannelId === "object") { - anticheatChannelId.send(`[ThirdEye]: Client is rebooting in: **${remainingTime} ** Seconds`); - } else { - return console.log("I could not find the paradoxLogs channel in Discord. 7"); - } - } - } + // Skip messages that originated from Discord (sent by the bot using /say) + if (message.match(/\[(?:§[0-9a-lr])*Discord(?:§[0-9a-lr])*\]/)) { + return; + } - if (remainingTime <= 0) { - clearInterval(timer); + // Remove username patterns like "[Username]" or "Username:" from the start of the message + message = message.replace(new RegExp(`^\\[${sender}\\]\\s*`), ""); + message = message.replace(new RegExp(`^${sender}:\\s*`), ""); - process.exit(); // Exit the script - } - }, 1000); // Delay of 1 second + if (channelId) { + const discordMessage = `**${sender}**: ${message}`; + channelId.send(discordMessage).catch(console.error); + } else { + console.log("Channel not found."); + } }); -//Send ingame message to discord. -bot.on("text", (packet: JsonPacket | ChatPacket) => { - //Check the packet type. - switch (packet.type) { - case "json": { - const msg = packet.message; - const obj = JSON.parse(msg); - /*As paradox is now using json packets for chat due to the restrictions in the API via the 1.20.60 update - this checks to make sure the packet is not a command result etc where the text value is not present. - */ - if (obj.rawtext[0].translate) { - break; - } - if (obj.rawtext[0].text.includes("Discord")) { - //don't send the message otherwise it will loop - break; - } - //Patch to prevent blank messages from paradox - if (obj.rawtext[0].text === "") { - break; - } - //continue to send the message to discord - - if ( - obj.rawtext[0].text.includes("§r§4[§6Paradox§4]§r") || - obj.rawtext[0].text.includes("§r§6[§aScythe§6]§r") || - obj.rawtext[0].text.includes("§l§6[§4Paradox§6]§r") || - obj.rawtext[0].text.includes("§l§6[§4Paradox AntiCheat Command Help§6]") || - obj.rawtext[0].text.includes("§f§o§4[§6Paradox§4]§f§o") || - obj.rawtext[0].text.includes("§f§4[§6Paradox§4]§f") || - obj.rawtext[0].text.includes("§2[§7Available Commands§2]§r") || - obj.rawtext[0].text.includes("§2[§7Paradox§2]§o§7") || - obj.rawtext[0].text.includes("§l§o§6[§4Paradox AntiCheat Command Help§6]§r§o") - ) { - // this will prevent it crashing. or logging to the wrong channel. - return; - } +interface MessageChannel { + send: (content: string | { embeds: EmbedBuilder[] }) => Promise; +} - const correctedText = autoCorrect(obj.rawtext[0].text, correction); - if (config.useEmbed === true) { - const msgEmbed = new EmbedBuilder() - .setColor(config.setColor) - .setTitle(config.setTitle) - .setDescription("[In Game] " + correctedText) - .setAuthor({ name: "‎", iconURL: config.logoURL }); - - if (typeof channelId === "object") { - return channelId.send({ embeds: [msgEmbed] }); - } else { - return console.log("I could not find the in-game channel in Discord. 6"); - } - } else { - if (typeof channelId === "object") { - return channelId.send(`[In Game] ${correctedText}`); - } else { - return console.log("I could not find the in-game channel in Discord. 7"); - } - } - } +function parseColor(color: string | number | number[] | readonly number[]): ColorResolvable { + // If it's already a number, return it + if (typeof color === "number") { + return color; + } - // Normal chat message - case "chat": { - if (config.useEmbed === true) { - const msgEmbed = new EmbedBuilder() - .setColor(config.setColor) - .setTitle(config.setTitle) - .setDescription("[In Game] " + packet.source_name + ": " + packet.message) - .setAuthor({ name: "‎", iconURL: config.logoURL }); - if (typeof channelId === "object") { - return channelId.send({ embeds: [msgEmbed] }); - } else { - return console.log("I could not find the in-game channel in Discord. 8"); - } - } else { - if (typeof channelId === "object") { - return channelId.send(`[In Game] **${packet.source_name}**: ${packet.message}`); - } else { - return console.log("I could not find the in-game channel in Discord. 9"); - } - } - } + // If it's an array of RGB values, convert to hex number + if (Array.isArray(color) && color.length >= 3) { + const [r, g, b] = color; + return (r << 16) | (g << 8) | b; } -}); -// Player leave messages. -bot.on("text", (packet: WhisperPacket | ChatPacket) => { - //Check for player leaving and report this back to discord. - if (packet.message.includes("§e%multiplayer.player.left")) { - const msg = packet.parameters + ": Has left the server."; - const username = "Server"; - if (config.useEmbed === true) { - const msgEmbed = new EmbedBuilder() - .setColor([255, 0, 0]) - .setTitle(config.setTitle) - .setDescription("[In Game] " + username + ": " + msg) - .setAuthor({ name: "‎", iconURL: config.logoURL }); - if (typeof channelId === "object") { - return channelId.send({ embeds: [msgEmbed] }); - } else { - return console.log("I could not find the paradoxLogs channel in Discord. 14"); - } - } else { - if (typeof channelId === "object") { - return channelId.send(`[In Game] **${username}**: ${msg}`); - } else { - return console.log("I could not find the paradoxLogs channel in Discord. 15"); - } + + // Handle hex colors (with or without #) + if (typeof color === "string") { + // If it's a hex string starting with #, convert to number + if (color.startsWith("#")) { + return parseInt(color.slice(1), 16); } - } -}); -// Handling the multiplayer.player.joined system message -bot.on("text", (packet: WhisperPacket | ChatPacket) => { - if (packet.message.includes("§e%multiplayer.player.joined")) { - /* we don't want to duplicate the join message as this is handled in the add_player packet. - in the event that the packet is not sent by the server allow the user to enable this message. - */ - if (config.useSystemPlayerJoinMessage === true) { - const msg = packet.parameters + ": Has joined the server."; - const username = "Server"; - if (config.useEmbed === true) { - const msgEmbed = new EmbedBuilder() - .setColor([0, 255, 0]) - .setTitle(config.setTitle) - .setDescription("[In Game] " + username + ": " + msg) - .setAuthor({ name: "‎", iconURL: config.logoURL }); - - if (typeof channelId === "object") { - return channelId.send({ embeds: [msgEmbed] }); - } else { - return console.log("I could not find the in-game channel in Discord. 16"); - } - } else { - if (typeof channelId === "object") { - return channelId.send(`[In Game] **${username}**: ${msg}`); - } else { - return console.log("I could not find the in-game channel in Discord. 17"); - } - } + // If it's a hex string without #, convert to number + if (/^[0-9A-Fa-f]{6}$/.test(color)) { + return parseInt(color, 16); } - //if not enabled it wont be sent. - return; + // Try to handle color names by returning the string + return color as ColorResolvable; } -}); -//grab the records of players online till we find the bot and then set the clientEntityID. -bot.on("player_list", (packet) => { - if (packet.records && packet.records.records && packet.records.records.length > 0) { - const playerRecord = packet.records.records[0]; - const entityUniqueId = playerRecord.entity_unique_id?.toString() || "N/A"; - const username = playerRecord.username || "N/A"; - console.log("Entity Unique ID:", entityUniqueId); - console.log("Username:", username); - // @ts-ignore - if (username === bot.username) { - clientEntityID = entityUniqueId; - console.log("Found the bots ID. This has been saved."); - } + // Default to a safe color (blue) if all else fails + return 0x3498db; +} + +function sendMessageToChannel(channel: MessageChannel | null | undefined, message: string): void { + if (!channel) { + console.log("Channel is undefined or null"); + return; } -}); -bot.on("update_abilities", (packet: UpdateAbilities) => { - const entityUniqueId = packet.entity_unique_id; - const permissionLevel = packet.permission_level; - clientPermissionLevel = permissionLevel; - console.log("Received Update Abilities Packet:"); - console.log("Entity Unique ID:", entityUniqueId); - console.log("Permission Level:", permissionLevel); - if (entityUniqueId.toString() === clientEntityID.toString() && permissionLevel === "operator") { - //update the var clientPermissionLevel. - clientPermissionLevel = permissionLevel; - if (typeof systemCommandsChannelId === "object") { - const msgEmbedOp = new EmbedBuilder().setColor(0x2ffc01).setTitle(config.setTitle).setDescription("[ThirdEye]: The bot is a operator.").setAuthor({ name: "‎", iconURL: config.logoURL }).setThumbnail("https://i.imgur.com/bEgXSej.png"); - systemCommandsChannelId.send({ embeds: [msgEmbedOp] }); + + try { + if (config.useEmbed) { + const msgEmbed = new EmbedBuilder().setColor(parseColor(config.setColor)).setTitle(config.setTitle).setDescription(message).setAuthor({ name: "‎", iconURL: config.logoURL }); + channel.send({ embeds: [msgEmbed] }).catch((error: Error) => { + console.error("Error sending embed message:", error); + }); } else { - console.log("I could not find the channel in Discord. in sendMessageToDiscord()"); - } - //if it has op put it into creative. - if (permissionLevel === "operator") { - const cmd = `/gamemode creative @s`; - bot.queue("command_request", { - command: cmd, - origin: { - type: "player", - uuid: "", - request_id: "", - }, - internal: false, - version: 52, + channel.send(message).catch((error: Error) => { + console.error("Error sending text message:", error); }); - console.log("The bot has tried to put its self into creative mode."); } - } else { - console.log("ID's dont match so bot has not be targeted."); + } catch (error) { + console.error("Error in sendMessageToChannel:", error); } -}); -bot.on("update_player_game_type", (packet: UpdatePlayerGameType) => { - let PlayerUniqueId = packet.player_unique_id; - if (PlayerUniqueId.toString() === clientEntityID.toString() && packet.gamemode === "creative") { - clientGamemode = packet.gamemode; - console.log("Bot is now in creative mode."); - if (typeof systemCommandsChannelId === "object") { - const msgEmbedOp = new EmbedBuilder().setColor(0x2ffc01).setTitle(config.setTitle).setDescription("[ThirdEye]: The bot is now in creative mode.").setAuthor({ name: "‎", iconURL: config.logoURL }); - systemCommandsChannelId.send({ embeds: [msgEmbedOp] }); - } else { - console.log("I could not find the channel in Discord. in sendMessageToDiscord()"); - } +} + +interface CommandRequest { + type: "chat"; + needs_translation: boolean; + source_name: string; + xuid: string; + platform_chat_id: string; + message: string; + filtered_message: string; +} + +interface DiscordMessage { + content: string; + author: { + id: string; + }; +} + +function handleCommand(message: DiscordMessage, isAdmin: boolean, isAnticheatChannel: boolean): void { + if (!isAdmin || !isAnticheatChannel) return; + + const command: string = message.content.startsWith(cmdPrefix + "/") ? message.content.slice(2) : message.content; + const type: "command_request" | "text" = message.content.startsWith(cmdPrefix + "/") ? "command_request" : "text"; + + bot.queue(type, { + type: "chat", + needs_translation: false, + source_name: config.username, + xuid: "", + platform_chat_id: "", + message: command, + filtered_message: "", + } as CommandRequest); + console.log(`Command received: ${command} From: ${message.author.id}`); +} + +interface WhitelistData { + whitelist: string[]; +} + +function handleWhitelistCommand(message: { content: string; author: { id: string } }, isAdmin: boolean, isAnticheatChannel: boolean): void { + if (!isAdmin || !isAnticheatChannel) return; + + const content: string = message.content.replace("$", ""); + if (content === "reboot") { + console.log("Forcing a reconnect."); + process.exit(); + } else if (content.endsWith("-r")) { + const name: string = content.replace("-r", ""); + WhitelistRead.whitelist = WhitelistRead.whitelist.filter((n: string) => n !== name); + console.log(`Removed ${name} from the whitelist.`); } else { - console.log("Error in update_player_game_type"); - console.log("PlayerUniqueId: " + PlayerUniqueId); - console.log("clientEntityID: " + clientEntityID); - console.log("clientGamemode:" + clientGamemode); - console.log("packet.gamemode: " + packet.gamemode); + WhitelistRead.whitelist.push(content); + console.log(`Added ${content} to the whitelist.`); } -}); + writeFileSync("whitelist.json", JSON.stringify(WhitelistRead, null, 2), "utf-8"); + WhitelistRead = JSON.parse(readFileSync("whitelist.json", "utf-8")) as WhitelistData; +} -//Check to see what the current permission level is alert the user via discord if the client needs to be opped. -let intervalId: NodeJS.Timeout; - -function sendMessageToDiscord() { - if (clientPermissionLevel === "member") { - if (typeof systemCommandsChannelId === "object") { - const msgEmbedOp = new EmbedBuilder() - .setColor(0xffff00) - .setTitle(config.setTitle) - .setDescription("[ThirdEye]: You need to op the bot via the server console.") - .setAuthor({ name: "‎", iconURL: config.logoURL }) - .setThumbnail("https://i.imgur.com/SO1qc2B.png"); - console.log("Sending warning message to discord to op the bot."); - systemCommandsChannelId.send({ embeds: [msgEmbedOp] }); - } else { - console.log("I could not find the channel in Discord. Function sendMessageToDiscord()."); - } - } else if (clientPermissionLevel === "operator") { - clearInterval(intervalId); +interface DiscordChatMessage { + author: { + id: string; + username: string; + }; + content: string; +} + +interface CommandRequest { + command: string; + origin: { + type: string; + uuid: string; + request_id: string; + }; + internal: boolean; + version: number; +} + +function handleChatMessage(message: DiscordChatMessage): void { + const isBadActor: boolean = idList.includes(message.author.id); + const cmd: string = isBadActor ? `/say §8[§9Discord§8] §4${message.author.username} (Known Hacker/Troll) : §f${message.content}` : `/say §8[§9Discord§8] §7${message.author.username}: §f${message.content}`; + + console.log(`Sending command to Minecraft server: ${cmd}`); + bot.queue("command_request", { + command: cmd, + origin: { + type: "player", + uuid: "", + request_id: "", + }, + internal: false, + version: 52, + } as CommandRequest); + + if (isBadActor && config.logBadActors) { + const msgEmbed: EmbedBuilder = new EmbedBuilder() + .setColor(parseColor(config.setColor)) + .setTitle(config.setTitle) + .setDescription(`Message sent to the bot from Discord from Author: ${message.author.username} Content: ${message.content} Unique ID: ${message.author.id}`) + .setAuthor({ name: "‎", iconURL: config.logoURL }) + .setThumbnail("https://static.wikia.nocookie.net/minecraft_gamepedia/images/7/76/Impulse_Command_Block.gif/revision/latest?cb=20191017044126"); + anticheatChannelId.send({ embeds: [msgEmbed] }); } } -if (clientPermissionLevel !== "operator") { - intervalId = setInterval(sendMessageToDiscord, 10000); +function handleDisconnection() { + let remainingTime = 2 * 60; + sendMessageToChannel(anticheatChannelId, `[${programName}]: The client has lost connection to the server and will initiate a reboot in: ${remainingTime} Seconds`); + + const interval = setInterval(() => { + remainingTime--; + if (remainingTime <= 0) { + clearInterval(interval); + process.exit(); + } + }, 1000); } -export function autoCorrect(text: string, correction: { [key: string]: string }): string { - const reg = new RegExp(Object.keys(correction).join("|"), "g"); - return text.replace(reg, (matched) => correction[matched as keyof typeof correction]); +interface CorrectionMap { + [key: string]: string; } -if (config.debug == true) { - bot.on("text", (packet) => { - const message = packet.message; - console.log(message); - }); + +export function autoCorrect(message: string): string { + let correctedMessage = message; + for (const [key, value] of Object.entries(correction as CorrectionMap)) { + correctedMessage = correctedMessage.replace(new RegExp(key, "g"), value); + } + return correctedMessage; } diff --git a/tsconfig.json b/tsconfig.json index b81fc53..a66a2c9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,8 +1,14 @@ { "compileOnSave": false, "compilerOptions": { - "typeRoots": ["node_modules/@discordjs", "bedrock-protocol", "src/types"], - "lib": ["ESNext"], + "typeRoots": [ + "node_modules/@discordjs", + "bedrock-protocol", + "src/types" + ], + "lib": [ + "ESNext" + ], "target": "ESNext", "module": "ESNext", "sourceMap": false, @@ -16,8 +22,13 @@ "noUnusedLocals": true, "noImplicitAny": true, "resolveJsonModule": true, - "allowSyntheticDefaultImports": true + "allowSyntheticDefaultImports": true, + "skipLibCheck": true }, - "include": ["src"], - "exclude": ["node_modules"], + "include": [ + "src" + ], + "exclude": [ + "node_modules" + ], } \ No newline at end of file