From 77645c75b18fba3b94446ba4f6a79412f659becf Mon Sep 17 00:00:00 2001 From: SowinskiBraeden Date: Mon, 29 Sep 2025 17:04:12 -0700 Subject: [PATCH 1/6] convert entry files to ts but there are lots of errors --- package-lock.json | 50 +- src/DayZRBot.js | 559 ------------------ src/DayZRBot.ts | 926 ++++++++++++++++++++++++++++++ src/config/config.ts | 5 +- src/database/armbands.js | 164 ------ src/database/armbands.ts | 93 +++ src/database/destinations.ts | 16 +- src/database/guild.ts | 58 +- src/database/player.js | 131 ----- src/database/player.ts | 232 ++++++++ src/database/weapons.js | 77 --- src/database/weapons.ts | 149 +++++ src/index.ts | 2 +- src/types/discord.d.ts | 23 + src/types/readline.d.ts | 12 + src/util/{Logger.js => Logger.ts} | 30 +- tsconfig.json | 11 +- 17 files changed, 1555 insertions(+), 983 deletions(-) delete mode 100644 src/DayZRBot.js create mode 100644 src/DayZRBot.ts delete mode 100644 src/database/armbands.js create mode 100644 src/database/armbands.ts delete mode 100644 src/database/player.js create mode 100644 src/database/player.ts delete mode 100644 src/database/weapons.js create mode 100644 src/database/weapons.ts create mode 100644 src/types/discord.d.ts create mode 100644 src/types/readline.d.ts rename src/util/{Logger.js => Logger.ts} (62%) diff --git a/package-lock.json b/package-lock.json index ce8c685..74a637b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "dotenv": "^16.0.3", "form-data": "^4.0.0", "mongodb": "^4.12.1", + "tsconfig-paths": "^4.2.0", "winston": "^3.8.1" }, "devDependencies": { @@ -3352,6 +3353,18 @@ "dev": true, "license": "MIT" }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -3511,7 +3524,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -4047,7 +4059,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -4284,6 +4295,20 @@ "strip-json-comments": "^2.0.0" } }, + "node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "license": "MIT", + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/tsconfig/node_modules/strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", @@ -6977,6 +7002,11 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, + "json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==" + }, "keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -7096,8 +7126,7 @@ "minimist": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" }, "mkdirp": { "version": "1.0.4", @@ -7426,8 +7455,7 @@ "strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==" }, "strip-json-comments": { "version": "3.1.1", @@ -7578,6 +7606,16 @@ } } }, + "tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "requires": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, "tslib": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", diff --git a/src/DayZRBot.js b/src/DayZRBot.js deleted file mode 100644 index 38d98f5..0000000 --- a/src/DayZRBot.js +++ /dev/null @@ -1,559 +0,0 @@ -const { RegisterGlobalCommands, RegisterGuildCommands } = require("./util/RegisterSlashCommands"); -const { Collection, Client, EmbedBuilder, Routes, InteractionResponseType, InteractionType, GatewayDispatchEvents } = require("discord.js"); -const MongoClient = require("mongodb").MongoClient; -const { REST } = require("@discordjs/rest"); -const Logger = require("./util/Logger"); -const crypto = require("crypto"); - -// custom util imports -const { DownloadNitradoFile, CheckServerStatus, FetchServerSettings, PostServerSettings, NitradoCredentialStatus } = require("./util/NitradoAPI"); -const { HandlePlayerLogs, HandleActivePlayersList } = require("./util/LogsHandler"); -const { HandleKillfeed, UpdateLastDeathDate } = require("./util/KillfeedHandler"); -const { HandleExpiredUAVs, HandleEvents, PlaceFireplaceInAlarm } = require("./util/AlarmsHandler"); -const { decrypt } = require("./util/Cryptic"); -const { GetWebhook, WebhookSend } = require("./util/WebhookHandler"); - -// Data structures imports -const { getDefaultPlayer, UpdatePlayer } = require("./database/player"); -const { Missions } = require("./database/destinations"); -const { GetGuild } = require("./database/guild"); - -const path = require("path"); -const fs = require("fs"); -const readline = require("readline"); - -const MINUTE = 60000; // 1 minute in milliseconds -const AUTO_RESTART_INTERVAL = 600000; // Set auto-restart interval 10 minutes (600,000ms) - -class DayZR extends Client { - - constructor(options, config) { - super(options); - - this.config = config; - this.commands = new Collection(); - this.interactionHandlers = new Collection(); - this.logger = new Logger(path.join(__dirname, "..", "logs", "Logs.log")); - this.timer = this.config.Dev == "PROD." ? MINUTE * 5 : MINUTE / 4; - - if ( - this.config.Token === "" || - this.config.SecretKey === "" || - this.config.SecretIv === "" - ) { - throw new TypeError( - "The config.js is not filled out. Please make sure nothing is blank, otherwise the bot will not work properly." - ); - } - - if (!["DEV.", "PROD."].includes(this.config.Dev)) { - throw new TypeError( - "The Dev version in the config.js does not match the allowed cases of \"DEV.\" or \"PROD.\"" - ); - } - - // Generate secret hash with crypto to use for encryption - this.key = crypto - .createHash("sha512") - .update(this.config.SecretKey) - .digest("hex") - .substring(0, 32); - - this.encryptionIV = crypto - .createHash("sha512") - .update(this.config.SecretIv) - .digest("hex") - .substring(0, 16); - - this.db; - this.dbo; - - this.databaseConnected = false; - this.arInterval = AUTO_RESTART_INTERVAL; - this.arIntervalIds = new Map(); - this.playerSessions = new Map(); - this.logHistory = new Map(); - this.alarmPingQueue = new Map(); - this.playerListMsgIds = new Map(); - - this.initialize(); - this.LoadCommandsAndInteractionHandlers(); - this.LoadEvents(); - - this.Ready = false; - this.activePlayersTick = 11; - - this.ws.on(GatewayDispatchEvents.InteractionCreate, async (interaction) => { - const start = new Date().getTime(); - if (interaction.type == InteractionType.ApplicationCommand) { - let GuildDB = await GetGuild(this, interaction.guild_id); - - if (this.exists(GuildDB.Nitrado) && this.exists(GuildDB.Nitrado.Auth)) { - GuildDB.Nitrado.Auth = decrypt( - GuildDB.Nitrado.Auth, - this.config.EncryptionMethod, - this.key, - this.encryptionIV - ); - } - - const command = interaction.data.name.toLowerCase(); - const args = interaction.data.options; - - // Free unused armbands for related commands - if (["armbands", "claim", "factions"].includes(command)) { - for (const [factionID, data] of Object.entries(GuildDB.factionArmbands)) { - const guild = this.guilds.cache.get(GuildDB.serverID); - const role = guild.roles.cache.find(role => role.id == factionID); - if (!role) { - let query = { - $pull: { "server.usedArmbands": data.armband }, - $unset: { [`server.factionArmbands.${factionID}`]: "" }, - }; - this.dbo.collection("guilds").updateOne({ "server.serverID": GuildDB.serverID }, query, (err, res) => { - if (err) return this.sendInternalError(interaction, err); - }); - } - } - } - - this.log(`Interaction [${interaction.guild_id}] - ${command}`); - - const rest = new REST({ version: "10" }).setToken(this.config.Token); - - // Easy to send response so ;) - interaction.guild = await this.guilds.fetch(interaction.guild_id); - const handleCallback = async (interactionType, message) => { - return await rest.post(Routes.interactionCallback(interaction.id, interaction.token), { - body: { - type: interactionType, - data: message, - } - }); - } - - // Nicely name our custom callback functions and pass correct type because discord is picky with numbers... - interaction.send = async (message) => handleCallback(InteractionResponseType.ChannelMessageWithSource, message); - interaction.deferReply = async (message) => handleCallback(InteractionResponseType.DeferredChannelMessageWithSource, message); - interaction.showModal = async (message) => handleCallback(InteractionResponseType.Modal, message); - - interaction.editReply = async (message) => { - return await rest.patch(Routes.webhookMessage(this.application.id, interaction.token), { - body: message, - }); - }; - - if (!this.databaseConnected) { - let dbFailedEmbed = new EmbedBuilder() - .setDescription(`**Internal Error:**\nUh Oh D: Its not you, its me.\nThe bot has failed to connect to the database 5 times!\nContact the Developers\nhttps://discord.gg/YCXhvy9uZw`) - .setColor(this.config.Colors.Red) - - return interaction.send({ embeds: [dbFailedEmbed] }); - } - - let cmd = this.commands.get(command); - try { - cmd.SlashCommand.run(this, interaction, args, { GuildDB }, start); // start is only used in ping / stats command - } catch (err) { - this.sendInternalError(interaction, err); - } - } - }); - } - - log(Text) { this.logger.log(Text); } - error(Text) { this.logger.error(Text); } - - async getDateEST(time) { - let timeArray = time.split(" ")[0].split(":"); - let t = new Date(); // Get current date & time (UTC) - let f = new Date(t.getTime() - 4 * 3600000); // Convert UTC into EST time to roll back the day as necessary - f.setUTCHours(timeArray[0], timeArray[1], timeArray[2]); // Apply the supplied EST time to the converted date (EST is the timezone produced from the Nitrado logs). - return new Date(f.getTime() + 4 * 3600000); // Add EST time offset to return timestamp in UTC - } - - async readLogs(guild) { - const fileStream = fs.createReadStream(`./logs/${guild.Nitrado.ServerID}-logs.ADM`); - - const rl = readline.createInterface({ - input: fileStream, - crlfDelay: Infinity - }); - let lines = []; - for await (const line of rl) { lines.push(line); } - - let logIndex = lines.indexOf(this.logHistory.get(guild.Nitrado.ServerID)); - - if (this.playerSessions.get(guild.Nitrado.ServerID).size === 0) { - let players = await this.dbo.collection("players").find({ "nitradoServerID": guild.Nitrado.ServerID }) // Get all players of this server - .toArray().then(all => all.filter(p => p.connected).map(p => p.connected = false)); // assume all players who were previously connected are not connected on init only. - - for (let i = 0; i < players.length; i++) { - await UpdatePlayer(this, players[i]) - } - } - - for (let i = logIndex + 1; i < lines.length; i++) { - // Handle lines to skip - if (lines[i].includes("| ####")) continue; - if (lines[i].includes("(id=Unknown") || lines[i].includes("Player \"Unknown Entity\"")) continue; - if ((i - 1) >= 0 && lines[i] == lines[i - 1]) continue; // continue if this line is a duplicate of the last line - - // Handle general logs - if (lines[i].includes("connected") || lines[i].includes("pos=<")) await HandlePlayerLogs(guild.Nitrado.ServerID, this, guild, lines[i], guild.combatLogTimer); - if (lines[i].includes("killed by Zmb") || lines[i].includes(">) died.")) await UpdateLastDeathDate(guild.Nitrado.ServerID, this, lines[i]); // Updates users last death date for non PVP deaths. - if (lines[i].includes(") placed Fireplace")) await PlaceFireplaceInAlarm(this, guild, lines[i]); - - // Handle killfeed logs - if ( - (lines[i].includes("killed by with") || lines[i].includes("killed by LandMineTrap")) || // Handle explosive deaths - (!(i + 1 >= lines.length) && lines[i + 1].includes("killed by") && lines[i].includes("TransportHit")) || // Handle vehicle deaths - (!(i + 1 >= lines.length) && lines[i + 1].includes("killed by Player") && lines[i].includes("hit by Player")) || // Handle PVP deaths - (lines[i].includes("killed by Player") && !lines[i - 1].includes("hit by Player")) // Handle deaths missing hit by log - ) await HandleKillfeed(guild.Nitrado.ServerID, this, guild, lines[i]); - } - - // Handle alarm pings - const maxEmbed = 10; - - this.alarmPingQueue.forEach(queue => { - queue.forEach(async (data, channel_id) => { - const channel = this.GetChannel(channel_id); - if (!channel) return; - - const NAME = "DayZ.R Zone Alert"; - const webhook = await GetWebhook(this, NAME, channel_id); - - data.forEach(async (embeds, role) => { - let embedArrays = []; - while (embeds.length > 0) - embedArrays.push(embeds.splice(0, maxEmbed)); - - for (let i = 0; i < embedArrays.length; i++) { - let content = { embeds: embedArrays[i] }; - if (role != "-no-role-ping-") content.content = `<@&${role}>`; - WebhookSend(this, webhook, content); - - // if (role == "-no-role-ping-") channel.send({ embeds: embedArrays[i] }); - // else channel.send({ content: `<@&${role}>`, embeds: embedArrays[i] }); - } - }); - }); - }); - - this.alarmPingQueue.set(guild.serverID, new Map()); // Clear alarm queue for this guild - - const playerTemplate = /(.*) \| Player \"(.*)\" \(id=(.*) pos=<(.*)>\)/g; - let previouslyConnected = await this.dbo.collection("players").find({ "nitradoServerID": guild.Nitrado.ServerID }) - .toArray().then(players => players.filter(p => p.connected)); // All players with connection log captured above and no disconnect log - let lastDetectedTime; - - for (let i = lines.length - 1; i > 0; i--) { - if (lines[i].includes("PlayerList log:")) { - for (let j = i + 1; j < lines.length; j++) { - let line = lines[j]; - if (line.includes("| ####")) break; - - let data = [...line.matchAll(playerTemplate)][0]; - if (!data) continue; - - let info = { - time: data[1], - player: data[2], - playerID: data[3], - }; - - if (!this.exists(info.player) || !this.exists(info.playerID)) continue; // Skip this player if the player does not exist. - - lastDetectedTime = await this.getDateEST(info.time); - - let playerStat = await this.dbo.collection("players").findOne({ "playerID": info.playerID }); - if (!this.exists(playerStat)) playerStat = getDefaultPlayer(info.player, info.playerID, guild.Nitrado.ServerID); - - if (!previouslyConnected.includes(playerStat) && this.exists(playerStat.lastDisconnectionDate) && playerStat.lastDisconnectionDate !== null && playerStat.lastDisconnectionDate.getTime() > lastDetectedTime.getTime()) continue; // Skip this player if the lastDisconnectionDate time is later than the player log entry. - - // Track adjusted sessions this instance has handled (e.g. no bot crashes or restarts). - if (this.playerSessions.get(guild.Nitrado.ServerID).has(info.playerID)) { - // Player is already in a session, update the session"s end time. - const session = this.playerSessions.get(guild.Nitrado.ServerID).get(info.playerID); - session.endTime = lastDetectedTime; // Update end time. - } else { - // Player is not in a session, create a new session. - const newSession = { - startTime: lastDetectedTime, - endTime: null, // Initialize end time as null. - }; - this.playerSessions.get(guild.Nitrado.ServerID).set(info.playerID, newSession); - - // Check if the player has been marked as connected before, but only if a session doesn"t exist - // in the map, indicating the connection was discovered in the logs during this session. - if (!previouslyConnected.includes(playerStat)) { - playerStat.connected = true; - playerStat.lastConnectionDate = lastDetectedTime; // Update last connection date. - } - } - - await UpdatePlayer(this, playerStat); - } - break; - } - } - - const lastLine = lines[lines.length - 1] - this.logHistory.set(guild.Nitrado.ServerID, lastLine); - this.dbo.collection("guilds").updateOne({ "server.serverID": guild.serverID }, { $set: { "server.lastLog": lastLine } }, (err, res) => { - if (err) this.error(`Failed to save last log to guild config [${guild.serverID}] for nitrado server [${guild.Nitrado.ServerID}]`); - }); - } - - async logsUpdateTimer(c) { - c.activePlayersTick++; - - c.guilds.cache.forEach(async (guild) => { - let GuildDB = await GetGuild(c, guild.id); - - /* - Note to self: - return statements do not prematurely exit out of a forEach loop like it does in a for loop. - */ - - if (!c.exists(GuildDB.Nitrado)) return; // Continue if no nitrado credentials - if (GuildDB.Nitrado.Status == NitradoCredentialStatus.FAILED) return; // Continue if these credentials are marked as failed - - const NitradoCred = { - ServerID: GuildDB.Nitrado.ServerID, - UserID: GuildDB.Nitrado.UserID, - Auth: decrypt( - GuildDB.Nitrado.Auth, - c.config.EncryptionMethod, - c.key, - c.encryptionIV - ), - }; - - const response = await FetchServerSettings(NitradoCred, c, "logsUpdateTimer").then(res => res); - if (response == 1) { - c.dbo.collection("guilds").updateOne({ "server.serverID": GuildDB.serverID }, { $set: { "Nitrado.Status": NitradoCredentialStatus.FAILED } }, (err, _) => { - if (err) this.error(`Failed to update Nitrado status to failed. [${GuildDB.serverID}]`); - }); - return; - }; - const settings = response.data.gameserver; - - // Update Nitrado DayZ Mission if change is detected - if (GuildDB.Nitrado.Mission !== Missions[settings.settings.config.mission]) { - c.dbo.collection("guilds").updateOne({ "server.serverID": GuildDB.serverID }, { $set: { "Nitrado.Mission": Missions[settings.settings.config.mission] } }, (err, res) => { - if (err) this.error(`Failed to save mission to guild config [${GuildDB.serverID}] for nitrado server [${GuildDB.Nitrado.ServerID}]`); - }); - } - - GuildDB.Nitrado.Mission = Missions[settings.settings.config.mission]; - - if (settings.game_specific.log_files.length == 0) return; // Ignore if no log files on Nitrado server - const filename = settings.game_specific.log_files.sort((a, b) => a.length - b.length)[0]; - const path = `${settings.game_specific.path.slice(0, -1)}${filename.split(settings.game)[1]}`; - - // Ensure Player List is logged for next update - const playerListEnabled = parseInt(settings.settings.config.adminLogPlayerList) - if (!playerListEnabled) PostServerSettings(NitradoCred, c, "config", "adminLogPlayerList", "1") - - await DownloadNitradoFile(NitradoCred, c, path, `./logs/${NitradoCred.ServerID}-logs.ADM`).then(async (status) => { - if (status == 1) return c.error(`Failed to Download Nitrado Log Files - [${NitradoCred.ServerID}]`); - await c.readLogs(GuildDB).then(async () => { - HandleExpiredUAVs(c, GuildDB); - HandleEvents(c, GuildDB) - if (c.activePlayersTick == 12) await HandleActivePlayersList(NitradoCred, c, GuildDB); - }) - }); - }); - } - - async connectMongo(mongoURI, dbo) { - let failed = false; - - let dbLogDir = path.join(__dirname, "..", "logs", "database-logs.json"); - let databaselogs; - try { - databaselogs = JSON.parse(fs.readFileSync(dbLogDir)); - } catch (err) { - databaselogs = { - attempts: 0, - connected: false, - }; - } - - if (databaselogs.attempts >= 5) { - this.error("Failed to connect to mongodb after multiple attempts"); - return; // prevent further attempts - } - - try { - // Connect to Mongo database. - this.db = await MongoClient.connect(mongoURI, { connectTimeoutMS: 1000 }); - this.dbo = this.db.db(dbo); - this.log("Successfully connected to mongoDB"); - databaselogs.connected = true; - databaselogs.attempts = 0; // reset attempts - this.databaseConnected = true; - } catch (err) { - databaselogs.attempts++; - databaselogs.connected = false; - let db = mongoURI.includes("@") ? mongoURI.split("@")[1] : mongoURI.split("//")[1]; - db = db.includes("/") ? db.split("/")[0] : db; - this.error(`Failed to connect to mongodb (mongodb://${db}/${dbo}): attempt ${databaselogs.attempts} - ${err}`); - failed = true; - } - - // write JSON string to a file - fs.writeFileSync(dbLogDir, JSON.stringify(databaselogs)); - - if (failed) process.exit(-1); - } - - async initialize() { - // Wait for MongoDB to connect - await this.connectMongo(this.config.mongoURI, this.config.dbo); - - if (!this.databaseConnected) return; - - let guilds = await this.dbo.collection("guilds").find({}).toArray(); - - /* - Initialize auto restart for enabled servers - Initialize last logs - Initialize Player Sessions - */ - for (let i = 0; i < guilds.length; i++) { - if (!this.exists(guilds[i].Nitrado)) continue; - if (guilds[i].server.autoRestart) { - const NitradoCred = { - ServerID: guilds[i].Nitrado.ServerID, - UserID: guilds[i].Nitrado.UserID, - Auth: decrypt( - guilds[i].Nitrado.Auth, - this.config.EncryptionMethod, - this.key, - this.encryptionIV - ) - }; - this.arIntervalIds.set(guilds[i].server.serverID, setInterval(CheckServerStatus, this.arInterval, NitradoCred, this)) - } - this.logHistory.set(guilds[i].Nitrado.ServerID, guilds[i].server.lastLog); // Using Nitrado Server ID over guild ID in case of future support for multiple nitrado servers in a single guild - this.playerSessions.set(guilds[i].Nitrado.ServerID, new Map()); // Same reason here as named above. - this.alarmPingQueue.set(guilds[i].server.serverID, new Map()); // Initialize alarm queue to be empty - this.playerListMsgIds.set(guilds[i].server.serverID, ""); // Initialize player list message ids - this.log(`[${guilds[i].server.serverID}] Initialized existing Nitrado Server: (${guilds[i].Nitrado.ServerID})`); - } - } - - async initNewNitradoServer(guildId, Nitrado) { - let guild = await GetGuild(this, guildId) - - if (guild.autoRestart) this.arIntervalIds.set(guildId, setInterval(CheckServerStatus, this.arInterval, Nitrado, this)) - this.logHistory.set(Nitrado.ServerID, guild.lastLog); - this.playerSessions.set(Nitrado.ServerID, new Map()); - this.alarmPingQueue.set(guildId, new Map()); - this.playerListMsgIds.set(guildId, ""); - this.log(`[${guildId}] Initialized new Nitrado`); - } - - exists(n) { return typeof (n) == "number" ? !isNaN(n) : null != n && undefined != n && "" != n } - - secondsToDhms(seconds) { - seconds = Number(seconds); - const d = Math.floor(seconds / (3600 * 24)); - const h = Math.floor(seconds % (3600 * 24) / 3600); - const m = Math.floor(seconds % 3600 / 60); - const s = Math.floor(seconds % 60); - - const dDisplay = d > 0 ? d + (d == 1 ? " day, " : " days, ") : ""; - const hDisplay = h > 0 ? h + (h == 1 ? " hour, " : " hours, ") : ""; - const mDisplay = m > 0 ? m + (m == 1 ? " minute, " : " minutes, ") : ""; - const sDisplay = s > 0 ? s + (s == 1 ? " second" : " seconds") : ""; - return dDisplay + hDisplay + mDisplay + sDisplay; - } - - LoadCommandsAndInteractionHandlers() { - let CommandsDir = path.join(__dirname, "commands"); - fs.readdir(CommandsDir, (err, files) => { - if (err) this.error(err); - else - files.forEach((file) => { - let cmd = require(CommandsDir + "/" + file); - if (!this.exists(cmd.name) || !this.exists(cmd.description)) - return this.error( - "Unable to load Command: " + - file.split(".")[0] + - ", Reason: File doesn't had name/desciption" - ); - this.commands.set(file.split(".")[0].toLowerCase(), cmd); - if (this.exists(cmd.Interactions)) { - for (let [interaction, handler] of Object.entries(cmd.Interactions)) { - this.interactionHandlers.set(interaction, handler); - } - } - this.log("Command Loaded: " + file.split(".")[0]); - }); - }); - } - - LoadEvents() { - let EventsDir = path.join(__dirname, "events"); - fs.readdir(EventsDir, (err, files) => { - if (err) this.error(err); - else - files.forEach((file) => { - const event = require(EventsDir + "/" + file); - if (["interactionCreate", "guildMemberAdd"].includes(file.split(".")[0])) this.on(file.split(".")[0], i => event(this, i)); - else this.on(file.split(".")[0], event.bind(null, this)); - this.log("Event Loaded: " + file.split(".")[0]); - }); - }); - } - - // Allows shorter lines of code elsewhere - GetChannel(channel_id) { return this.channels.cache.get(channel_id); } - - sendError(Channel, Error) { - this.error(Error); - let embed = new EmbedBuilder() - .setColor(this.config.Red) - .setDescription(Error); - - Channel.send(embed); - } - - // Handles internal errors for slash commands. E.g failed to update database from slash command. - sendInternalError(Interaction, Error) { - this.error(Error); - const embed = new EmbedBuilder() - .setDescription(`**Internal Error:**\nUh Oh D: Its not you, its me.\nThis command has crashed\nContact the Developers\nhttps://discord.gg/YCXhvy9uZw`) - .setColor(this.config.Colors.Red) - - try { - Interaction.send({ embeds: [embed] }); - } catch { - Interaction.update({ embeds: [embed], components: [] }); - } - } - - // Calls register for guild and global commands - RegisterSlashCommands() { - RegisterGlobalCommands(this); - let p = Promise.resolve() - this.guilds.cache.forEach((guild) => { - p = p.then(() => { - RegisterGuildCommands(this, guild.id); - return new Promise((resolve) => { - setTimeout(resolve, 500); - }) - }) - }); - } - - build() { - this.login(this.config.Token); - } -} - -module.exports = DayZR; diff --git a/src/DayZRBot.ts b/src/DayZRBot.ts new file mode 100644 index 0000000..635999d --- /dev/null +++ b/src/DayZRBot.ts @@ -0,0 +1,926 @@ +// Discord.js & REST API +import { + Collection, + Client, + EmbedBuilder, + Routes, + InteractionResponseType, + InteractionType, + GatewayDispatchEvents, + ClientOptions, + Guild, + ChatInputCommandInteraction, + Snowflake, + Role, + Message, + TextBasedChannel, + ButtonInteraction, + StringSelectMenuInteraction, + Channel, +} from "discord.js"; +import { REST } from "@discordjs/rest"; + +// MongoDB +import { MongoClient } from "mongodb"; + +// Node.js built-ins +import * as path from "path"; +import * as fs from "node:fs"; +import * as readline from "node:readline"; +import * as crypto from "crypto"; +import { Readable } from "node:stream"; + +// Custom utilities +import Logger from "./util/Logger"; +import { RegisterGlobalCommands, RegisterGuildCommands } from "./util/RegisterSlashCommands"; +import { + DownloadNitradoFile, + CheckServerStatus, + FetchServerSettings, + PostServerSettings, + NitradoCredentialStatus, +} from "./util/NitradoAPI"; +import { HandlePlayerLogs, HandleActivePlayersList } from "./util/LogsHandler"; +import { HandleKillfeed, UpdateLastDeathDate } from "./util/KillfeedHandler"; +import { HandleExpiredUAVs, HandleEvents, PlaceFireplaceInAlarm } from "./util/AlarmsHandler"; +import { decrypt } from "./util/Cryptic"; +import { GetWebhook, WebhookSend } from "./util/WebhookHandler"; +import { Config, Colors } from "./config/config"; + +// Database structures +import { getDefaultPlayer, UpdatePlayer, Player } from "./database/player"; +import { Missions, MissionKey } from "./database/destinations"; +import { GetGuild, GuildConfig, NitradoConfig } from "./database/guild"; + +const MINUTE = 60000; // 1 minute in milliseconds +const AUTO_RESTART_INTERVAL = 600000; // Set auto-restart interval 10 minutes (600,000ms) +const STARTING_PLAYERS_TICK = 11; + +// TODO: move this interface to LogsHandler when converted to ts +interface PlayerSession { + startTime: Date; + endTime: Date | null; +}; + +// TODO: put this somewhere else +interface CommandOptions { + +}; + +interface InteractionHandler { + run: ( + client: DayZR, + interaction: ChatInputCommandInteraction, + GuildDB: GuildConfig + ) => Promise; +} + +interface Command { + name: string; + debug: boolean; + global: boolean; + description: string; + usage: string; + permissions: { + channel: Array; + member: Array; + }; + options: Array; + SlashCommand: { + run: ( + client: DayZR, + interaction: ChatInputCommandInteraction, + args: any, // Unsure of what this should be, come back to later + GuildDB: GuildConfig, + start?: number + ) => Promise; + }; + Interactions?: { + [InteractoinName: string]: InteractionHandler; + } +} + +export default class DayZR extends Client +{ + + private config: Config; + private logger: Logger; + + private commands: Collection; + private interactionHandlers: Collection; + + private key: string; + private encryptionIV: string; + private db: any; + public dbo: any; + public databaseConnected: boolean; + + private arInterval: number; + private arIntervalIds: Map; + private playerSessions: Map>; + private logHistory: Map; + private alarmPingQueue: Map>>>; + private playerListMsgIds: Map; + + private Ready: boolean; + private activePlayersTick: number; + private timer: number; + + constructor(options: ClientOptions, config: Config) + { + super(options); + + this.config = config; + this.commands = new Collection(); + this.interactionHandlers = new Collection(); + this.logger = new Logger(path.join(__dirname, "..", "logs", "Logs.log")); + this.timer = this.config.Dev == "PROD." ? MINUTE * 5 : MINUTE / 4; + + if ( + this.config.Token === "" || + this.config.SecretKey === "" || + this.config.SecretIv === "" + ) { + throw new TypeError( + "The config.js is not filled out. Please make sure nothing is blank, otherwise the bot will not work properly." + ); + } + + if (!["DEV.", "PROD."].includes(this.config.Dev)) { + throw new TypeError( + "The Dev version in the config.js does not match the allowed cases of \"DEV.\" or \"PROD.\"" + ); + } + + // Generate secret hash with crypto to use for encryption + this.key = crypto + .createHash("sha512") + .update(this.config.SecretKey) + .digest("hex") + .substring(0, 32); + + this.encryptionIV = crypto + .createHash("sha512") + .update(this.config.SecretIv) + .digest("hex") + .substring(0, 16); + + this.databaseConnected = false; + this.arInterval = AUTO_RESTART_INTERVAL; + this.arIntervalIds = new Map(); + this.playerSessions = new Map(); + this.logHistory = new Map(); + this.alarmPingQueue = new Map(); + this.playerListMsgIds = new Map(); + + this.initialize(); + this.LoadCommandsAndInteractionHandlers(); + this.LoadEvents(); + + this.Ready = false; + this.activePlayersTick = STARTING_PLAYERS_TICK; + + this.ws.on( + GatewayDispatchEvents.InteractionCreate, + async (interaction: ChatInputCommandInteraction) => + { + + const start: number = new Date().getTime(); + + if (interaction.type == InteractionType.ApplicationCommand) + { + if (!interaction.guildId) return; + + let GuildDB: GuildConfig = await GetGuild(this, interaction.guildId); + + if (this.exists(GuildDB.Nitrado) && this.exists(GuildDB.Nitrado.Auth)) + { + GuildDB.Nitrado.Auth = decrypt( + GuildDB.Nitrado.Auth, + this.config.EncryptionMethod, + this.key, + this.encryptionIV + ); + } + + const command: string = interaction.commandName; + const args: any = interaction.options; + + // Free unused armbands for related commands + if (["armbands", "claim", "factions"].includes(command)) + { + + for (const [factionID, data] of Object.entries(GuildDB.factionArmbands)) + { + + const guild: Guild | undefined = this.guilds.cache.get(GuildDB.serverID); + + if (!guild) continue; + + const role: Role | undefined = guild.roles.cache.find(role => role.id == factionID); + + if (!role) + { + let query = { + $pull: { "server.usedArmbands": data.armband }, + $unset: { [`server.factionArmbands.${factionID}`]: "" }, + }; + + this.dbo.collection("guilds").updateOne( + { + "server.serverID": GuildDB.serverID + }, + query, + (err: string) => + { + if (err) return this.sendInternalError(interaction, err); + } + ); + } + } + } + + this.log(`Interaction [${interaction.guildId}] - ${command}`); + + const rest = new REST({ version: "10" }).setToken(this.config.Token); + + const normalizeMessage = (message: any) => + { + if (message?.embeds) { + message = { + ...message, + embeds: message.embeds.map((embed: EmbedBuilder | any) => + embed instanceof EmbedBuilder ? embed.toJSON() : embed + ), + }; + } + return message; + }; + + // Easy to send response so ;) + const handleCallback = async (interactionType: InteractionResponseType, message: any): Promise => + { + message = normalizeMessage(message); + + return await rest.post(Routes.interactionCallback(interaction.id, interaction.token), + { + body: { + type: interactionType, + data: message, + } + }); + } + + // Nicely name our custom callback functions and pass correct type because discord is picky with numbers... + interaction.send = async (message: Message): Promise => handleCallback(InteractionResponseType.ChannelMessageWithSource, message); + interaction.deferReply = async (message: Message): Promise => handleCallback(InteractionResponseType.DeferredChannelMessageWithSource, message); + interaction.showModal = async (message: Message): Promise => handleCallback(InteractionResponseType.Modal, message); + + interaction.editReply = async (message: Message): Promise => + { + if (!this.application) return; + return await rest.patch(Routes.webhookMessage(this.application.id, interaction.token), { + body: message, + }); + }; + + if (!this.databaseConnected) + { + const dbFailedEmbed: EmbedBuilder = new EmbedBuilder() + .setDescription(`**Internal Error:**\nUh Oh D: Its not you, its me.\nThe bot has failed to connect to the database 5 times!\nContact the Developers\nhttps://discord.gg/YCXhvy9uZw`) + .setColor(Colors.Red) + + return interaction.send({ embeds: [dbFailedEmbed] }); + } + + let cmd: Command | undefined = this.commands.get(command); + if (!cmd) return; + try { + cmd.SlashCommand.run(this, interaction, args, GuildDB, start); // start is only used in ping / stats command + } catch (err: any) { + this.sendInternalError(interaction, err); + } + } + }); + } + + private log(text: string): void { this.logger.log(text); } + public error(text: string): void { this.logger.error(text); } + + public getDateEST(time: string): Date + { + // TODO: fix magic numbers <------------------------------------------------------------------------------------ + let timeArray: Array = time.split(" ")[0].split(":").map((v) => parseInt(v)); + let t: Date = new Date(); // Get current date & time (UTC) + let f: Date = new Date(t.getTime() - 4 * 3600000); // Convert UTC into EST time to roll back the day as necessary + + f.setUTCHours(timeArray[0], timeArray[1], timeArray[2]); // Apply the supplied EST time to the converted date (EST is the timezone produced from the Nitrado logs). + return new Date(f.getTime() + 4 * 3600000); // Add EST time offset to return timestamp in UTC + } + + private async readLogs(guild: GuildConfig): Promise + { + + if (!guild.Nitrado) return; + + const fileStream: fs.ReadStream = fs.createReadStream(`./logs/${guild.Nitrado.ServerID}-logs.ADM`); + + const rl: readline.Interface = readline.createInterface({ + input: fileStream as Readable, + crlfDelay: Infinity + }); + let lines: Array = []; + for await (const line of rl) { lines.push(line); } + + const lastLog: string | null | undefined = this.logHistory.get(guild.Nitrado.ServerID); + + let logIndex = -1; + if (lastLog != null) + { + logIndex = lines.indexOf(lastLog); + } + + if (this.playerSessions.get(guild.Nitrado.ServerID)?.size === 0) + { + + // Get all players of this server + let players: Array = await this.dbo.collection("players").find( + { + "nitradoServerID": guild.Nitrado.ServerID + } + ).toArray().then((all: Array) => + { + // assume all players who were previously connected are not connected on init only. + return all.filter(p => p.connected).map(p => p.connected = false); + } + ); + + for (let i = 0; i < players.length; i++) + { + await UpdatePlayer(this, players[i]) + } + } + + for (let i = logIndex + 1; i < lines.length; i++) + { + // TODO: tidy this up its just bad looking + + // Handle lines to skip + if (lines[i].includes("| ####")) continue; + if (lines[i].includes("(id=Unknown") || lines[i].includes("Player \"Unknown Entity\"")) continue; + if ((i - 1) >= 0 && lines[i] == lines[i - 1]) continue; // continue if this line is a duplicate of the last line + + // Handle general logs + if (lines[i].includes("connected") || lines[i].includes("pos=<")) await HandlePlayerLogs(guild.Nitrado.ServerID, this, guild, lines[i], guild.combatLogTimer); + if (lines[i].includes("killed by Zmb") || lines[i].includes(">) died.")) await UpdateLastDeathDate(guild.Nitrado.ServerID, this, lines[i]); // Updates users last death date for non PVP deaths. + if (lines[i].includes(") placed Fireplace")) await PlaceFireplaceInAlarm(this, guild, lines[i]); + + // Handle killfeed logs + if ( + (lines[i].includes("killed by with") || lines[i].includes("killed by LandMineTrap")) || // Handle explosive deaths + (!(i + 1 >= lines.length) && lines[i + 1].includes("killed by") && lines[i].includes("TransportHit")) || // Handle vehicle deaths + (!(i + 1 >= lines.length) && lines[i + 1].includes("killed by Player") && lines[i].includes("hit by Player")) || // Handle PVP deaths + (lines[i].includes("killed by Player") && !lines[i - 1].includes("hit by Player")) // Handle deaths missing hit by log + ) await HandleKillfeed(guild.Nitrado.ServerID, this, guild, lines[i]); + } + + // Handle alarm pings + const maxEmbed = 10; + + this.alarmPingQueue.forEach(queue => + { + queue.forEach(async (data, channel_id) => + { + const channel = this.GetChannel(channel_id); + if (!channel) return; + + const NAME = "DayZ.R Zone Alert"; + const webhook = await GetWebhook(this, NAME, channel_id); + + data.forEach(async (embeds, role) => + { + let embedArrays: Array> = []; + while (embeds.length > 0) + { + embedArrays.push(embeds.splice(0, maxEmbed)); + } + + for (let i = 0; i < embedArrays.length; i++) + { + interface AlarmEmbed + { + embeds: any; + content?: string; + }; + + let content: AlarmEmbed = { embeds: embedArrays[i] }; + + if (role != "-no-role-ping-") content.content = `<@&${role}>`; + WebhookSend(this, webhook, content); + + // if (role == "-no-role-ping-") channel.send({ embeds: embedArrays[i] }); + // else channel.send({ content: `<@&${role}>`, embeds: embedArrays[i] }); + } + }); + }); + }); + + this.alarmPingQueue.set(guild.serverID, new Map()); // Clear alarm queue for this guild + + const playerTemplate = /(.*) \| Player \"(.*)\" \(id=(.*) pos=<(.*)>\)/g; + let previouslyConnected = await this.dbo.collection("players").find( + { + "nitradoServerID": guild.Nitrado.ServerID + } + ).toArray().then((players: Array) => { + // All players with connection log captured above and no disconnect log + return players.filter(p => p.connected); + }); + + let lastDetectedTime; + + for (let i = lines.length - 1; i > 0; i--) + { + + if (lines[i].includes("PlayerList log:")) + { + + for (let j = i + 1; j < lines.length; j++) + { + + let line = lines[j]; + if (line.includes("| ####")) break; + + let data = [...line.matchAll(playerTemplate)][0]; + if (!data) continue; + + let info = { + time: data[1], + player: data[2], + playerID: data[3], + }; + + // Skip this player if the player does not exist. + if (!this.exists(info.player) || !this.exists(info.playerID)) continue; + + lastDetectedTime = await this.getDateEST(info.time); + + let playerStat = await this.dbo.collection("players").findOne({ "playerID": info.playerID }); + if (!this.exists(playerStat)) playerStat = getDefaultPlayer(info.player, info.playerID, guild.Nitrado.ServerID); + + // Skip this player if the lastDisconnectionDate time is later than the player log entry. + if (!previouslyConnected.includes(playerStat) && + this.exists(playerStat.lastDisconnectionDate) && + playerStat.lastDisconnectionDate !== null && + playerStat.lastDisconnectionDate.getTime() > lastDetectedTime.getTime() + ) continue; + + + // Track adjusted sessions this instance has handled (e.g. no bot crashes or restarts). + if (this.playerSessions.get(guild.Nitrado.ServerID)?.has(info.playerID)) + { + + // Player is already in a session, update the session"s end time. + let session: PlayerSession | undefined = this.playerSessions.get(guild.Nitrado.ServerID)?.get(info.playerID); + + if (!session) continue; + + session.endTime = lastDetectedTime; // Update end time. + } + else + { + + // Player is not in a session, create a new session. + const newSession = { + startTime: lastDetectedTime, + endTime: null, // Initialize end time as null. + }; + + this.playerSessions.get(guild.Nitrado.ServerID)?.set(info.playerID, newSession); + + // Check if the player has been marked as connected before, but only if a session doesn"t exist + // in the map, indicating the connection was discovered in the logs during this session. + if (!previouslyConnected.includes(playerStat)) + { + playerStat.connected = true; + playerStat.lastConnectionDate = lastDetectedTime; // Update last connection date. + } + } + + await UpdatePlayer(this, playerStat); + } + break; + } + } + + const lastLine = lines[lines.length - 1]; + this.logHistory.set(guild.Nitrado.ServerID, lastLine); + + this.dbo.collection("guilds").updateOne( + { + "server.serverID": guild.serverID + }, + { + $set: { "server.lastLog": lastLine } + }, + (err: string) => + { + if (err) + { + this.error(`Failed to save last log to guild config [${guild.serverID}] for nitrado server [${guild.Nitrado?.ServerID}]`); + } + } + ); + } + + private async logsUpdateTimer(c: DayZR): Promise + { + c.activePlayersTick++; + + c.guilds.cache.forEach(async (guild) => + { + let GuildDB = await GetGuild(c, guild.id); + + /* + Note to self: + return statements do not prematurely exit out of a forEach loop like it does in a for loop. + */ + + // Continue if no nitrado credentials + if (!c.exists(GuildDB.Nitrado)) return; + + // Continue if these credentials are marked as failed + if (GuildDB.Nitrado.Status == NitradoCredentialStatus.FAILED) return; + + const NitradoCred = { + ServerID: GuildDB.Nitrado.ServerID, + UserID: GuildDB.Nitrado.UserID, + Auth: decrypt( + GuildDB.Nitrado.Auth, + c.config.EncryptionMethod, + c.key, + c.encryptionIV + ), + }; + + const response = await FetchServerSettings(NitradoCred, c, "logsUpdateTimer").then(res => res); + + // TODO: fix magic number + if (response == 1) + { + c.dbo.collection("guilds").updateOne( + { + "server.serverID": GuildDB.serverID + }, + { + $set: { "Nitrado.Status": NitradoCredentialStatus.FAILED } + }, + (err: string) => + { + if (err) + { + this.error(`Failed to update Nitrado status to failed. [${GuildDB.serverID}]`); + } + } + ); + + return; + }; + + const settings = response.data.gameserver; + + // Update Nitrado DayZ Mission if change is detected + let missionKey: MissionKey = settings.settings.config.mission; + if (GuildDB.Nitrado.Mission !== Missions[missionKey]) + { + c.dbo.collection("guilds").updateOne( + { + "server.serverID": GuildDB.serverID + }, + { + $set: { "Nitrado.Mission": Missions[missionKey] } + }, + (err: string) => + { + if (err) + { + this.error(`Failed to save mission to guild config [${GuildDB.serverID}] for nitrado server [${GuildDB.Nitrado?.ServerID}]`) + }; + } + ); + } + + GuildDB.Nitrado.Mission = Missions[missionKey]; + + // Ignore if no log files on Nitrado server + if (settings.game_specific.log_files.length == 0) return; + + // Get file with longest file name (target log file) + const filename = settings.game_specific.log_files.sort((a: string, b: string) => a.length - b.length)[0]; + const path = `${settings.game_specific.path.slice(0, -1)}${filename.split(settings.game)[1]}`; + + // Ensure Player List is logged for next update + const playerListEnabled = parseInt(settings.settings.config.adminLogPlayerList) + if (!playerListEnabled) PostServerSettings(NitradoCred, c, "config", "adminLogPlayerList", "1") + + await DownloadNitradoFile(NitradoCred, c, path, `./logs/${NitradoCred.ServerID}-logs.ADM`) + .then(async (status) => + { + if (status == 1) return c.error(`Failed to Download Nitrado Log Files - [${NitradoCred.ServerID}]`); + + await c.readLogs(GuildDB).then(async () => + { + HandleExpiredUAVs(c, GuildDB); + HandleEvents(c, GuildDB) + + // TODO: fix magic number + if (c.activePlayersTick == 12) await HandleActivePlayersList(NitradoCred, c, GuildDB); + }) + }); + }); + } + + async connectMongo(mongoURI: string, dbo: string): Promise + { + + let failed: boolean = false; + let dbLogDir: string = path.join(__dirname, "..", "logs", "database-logs.json"); + + interface DatabaseLog + { + attempts: number; + connected: boolean; + }; + + let databaselogs: DatabaseLog; + + try + { + databaselogs = JSON.parse( + fs.readFileSync(dbLogDir).toString("utf-8") + ); + } + catch (err) + { + databaselogs = { + attempts: 0, + connected: false, + }; + } + + if (databaselogs.attempts >= 5) + { + this.error("Failed to connect to mongodb after multiple attempts"); + return; // prevent further attempts + } + + try + { + // Connect to Mongo database. + this.db = new MongoClient(mongoURI, { connectTimeoutMS: 1000 }); + await this.db.connect(); + + this.dbo = this.db.db(dbo); + this.log("Successfully connected to mongoDB"); + + databaselogs.connected = true; + databaselogs.attempts = 0; // reset attempts + this.databaseConnected = true; + + } + catch (err) + { + databaselogs.attempts++; + databaselogs.connected = false; + + let db = mongoURI.includes("@") ? mongoURI.split("@")[1] : mongoURI.split("//")[1]; + db = db.includes("/") ? db.split("/")[0] : db; + + this.error(`Failed to connect to mongodb (mongodb://${db}/${dbo}): attempt ${databaselogs.attempts} - ${err}`); + + failed = true; + } + + // write JSON string to a file + fs.writeFileSync(dbLogDir, JSON.stringify(databaselogs)); + + if (failed) process.exit(-1); + } + + async initialize(): Promise + { + + // Wait for MongoDB to connect + await this.connectMongo(this.config.MongoURI, this.config.DBO); + + if (!this.databaseConnected) return; + + let guilds = await this.dbo.collection("guilds").find({}).toArray(); + + /* + Initialize auto restart for enabled servers + Initialize last logs + Initialize Player Sessions + */ + for (let i = 0; i < guilds.length; i++) + { + if (!this.exists(guilds[i].Nitrado)) continue; + + if (guilds[i].server.autoRestart) + { + const NitradoCred = { + ServerID: guilds[i].Nitrado.ServerID, + UserID: guilds[i].Nitrado.UserID, + Auth: decrypt( + guilds[i].Nitrado.Auth, + this.config.EncryptionMethod, + this.key, + this.encryptionIV + ) + }; + this.arIntervalIds.set(guilds[i].server.serverID, setInterval(CheckServerStatus, this.arInterval, NitradoCred, this)) + } + this.logHistory.set(guilds[i].Nitrado.ServerID, guilds[i].server.lastLog); // Using Nitrado Server ID over guild ID in case of future support for multiple nitrado servers in a single guild + this.playerSessions.set(guilds[i].Nitrado.ServerID, new Map()); // Same reason here as named above. + this.alarmPingQueue.set(guilds[i].server.serverID, new Map()); // Initialize alarm queue to be empty + this.playerListMsgIds.set(guilds[i].server.serverID, ""); // Initialize player list message ids + this.log(`[${guilds[i].server.serverID}] Initialized existing Nitrado Server: (${guilds[i].Nitrado.ServerID})`); + } + } + + async initNewNitradoServer( + guildId: Snowflake, + Nitrado: NitradoConfig + ): Promise + { + let guild = await GetGuild(this, guildId) + + if (guild.autoRestart) + { + this.arIntervalIds.set(guildId, setInterval(CheckServerStatus, this.arInterval, Nitrado, this)) + } + + this.logHistory.set(Nitrado.ServerID, guild.lastLog); + this.playerSessions.set(Nitrado.ServerID, new Map()); + this.alarmPingQueue.set(guildId, new Map()); + this.playerListMsgIds.set(guildId, ""); + this.log(`[${guildId}] Initialized new Nitrado`); + } + + exists (n: T | null | undefined | "" | number): n is T + { + return typeof n === "number" ? !isNaN(n) : n !== null && n !== undefined && n !== ""; + } + + secondsToDhms(seconds: number): string + { + + // TODO; holy magic numbers, my prof is gonna assasinate me if he sees this + seconds = Number(seconds); + const d = Math.floor(seconds / (3600 * 24)); + const h = Math.floor(seconds % (3600 * 24) / 3600); + const m = Math.floor(seconds % 3600 / 60); + const s = Math.floor(seconds % 60); + + const dDisplay = d > 0 ? d + (d == 1 ? " day, " : " days, ") : ""; + const hDisplay = h > 0 ? h + (h == 1 ? " hour, " : " hours, ") : ""; + const mDisplay = m > 0 ? m + (m == 1 ? " minute, " : " minutes, ") : ""; + const sDisplay = s > 0 ? s + (s == 1 ? " second" : " seconds") : ""; + + return dDisplay + hDisplay + mDisplay + sDisplay; + } + + LoadCommandsAndInteractionHandlers(): void + { + let CommandsDir = path.join(__dirname, "commands"); + + fs.readdir(CommandsDir, (err, files) => + { + if (err) + { + this.error(err.message); + } + else + { + files.forEach((file) => + { + let cmd: Command = require(CommandsDir + "/" + file); + if (!this.exists(cmd.name) || !this.exists(cmd.description)) + { + return this.error( + "Unable to load Command: " + + file.split(".")[0] + + ", Reason: File doesn't had name/desciption" + ); + } + + this.commands.set(file.split(".")[0].toLowerCase(), cmd); + + if (this.exists(cmd.Interactions)) + { + for (let [interaction, handler] of Object.entries(cmd.Interactions)) + { + this.interactionHandlers.set(interaction, handler); + } + } + + this.log("Command Loaded: " + file.split(".")[0]); + }); + } + }); + } + + LoadEvents(): void + { + let EventsDir = path.join(__dirname, "events"); + + fs.readdir(EventsDir, (err, files) => + { + if (err) + { + this.error(err.message); + } + else + { + files.forEach((file) => + { + const event = require(EventsDir + "/" + file); + + if (["interactionCreate", "guildMemberAdd"].includes(file.split(".")[0])) + { + this.on(file.split(".")[0], i => event(this, i)); + } + else + { + this.on(file.split(".")[0], event.bind(null, this)); + } + + this.log("Event Loaded: " + file.split(".")[0]); + }); + } + }); + } + + // Allows shorter lines of code elsewhere + GetChannel(channel_id: Snowflake): Channel | undefined + { + return this.channels.cache.get(channel_id); + } + + sendError(channel: TextBasedChannel, error: string): void + { + this.error(error); + let embed: EmbedBuilder = new EmbedBuilder() + .setColor(Colors.Red) + .setDescription(error); + + + channel.send({ embeds: [embed] }); + } + + // Handles internal errors for slash commands. E.g failed to update database from slash command. + sendInternalError( + interaction: ChatInputCommandInteraction | ButtonInteraction | StringSelectMenuInteraction, + error: string + ): void + { + this.error(error); + const embed: EmbedBuilder = new EmbedBuilder() + .setDescription(`**Internal Error:**\nUh Oh D: Its not you, its me.\nThis command has crashed\nContact the Developers\nhttps://discord.gg/YCXhvy9uZw`) + .setColor(Colors.Red) + + if (interaction.isChatInputCommand()) + { + interaction.send({ embeds: [embed] }); + } + else if (interaction.isButton() || interaction.isStringSelectMenu()) + { + interaction.update({ embeds: [embed], components: [] }); + } + } + + // Calls register for guild and global commands + RegisterSlashCommands(): void + { + RegisterGlobalCommands(this); + + let p = Promise.resolve() + + this.guilds.cache.forEach((guild) => + { + p = p.then(() => + { + RegisterGuildCommands(this, guild.id); + return new Promise((resolve) => + { + setTimeout(resolve, 500); // <<--- TODO: MAGIC NUMBER! + }); + }); + }); + } + + build(): void + { + this.login(this.config.Token); + } +} diff --git a/src/config/config.ts b/src/config/config.ts index 5e9c897..17a0775 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -7,7 +7,8 @@ export const enum DevelopmentBranch { Prod = "PROD.", }; -export const enum Colors { +// switch to const later once removed Colors from config +export enum Colors { Default = "#8a7c72", DarkRed = "#ba0f0f", Red = "#f55c5c", @@ -25,6 +26,7 @@ export interface Config { Dev: DevelopmentBranch; Version: string; Admins: Array; + Colors: typeof Colors; SupportServer: string; Token: string; SecretKey: string; @@ -43,6 +45,7 @@ export const config: Config = { Dev: process.env.Dev || DevelopmentBranch.Dev, Version: version, Admins: ["362791661274660874", "329371697570381824"], // Admins of the bot + Colors: Colors, // only here for backwards compatibility with commands/* SupportServer: "https://discord.gg/KVFJCvvFtK", // Support Server Link Token: process.env.token || "", //Discord Bot Token SecretKey: process.env.key || "01234567891", diff --git a/src/database/armbands.js b/src/database/armbands.js deleted file mode 100644 index c298faf..0000000 --- a/src/database/armbands.js +++ /dev/null @@ -1,164 +0,0 @@ -module.exports = { - Armbands: [ - { - name: "Black", - url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/8/82/ArmbandBlack.png/revision/latest?cb=20161127174754" - }, - { - name: "Blue", - url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/b/bd/ArmbandBlue.png/revision/latest?cb=20161127174803" - }, - { - name: "Green", - url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/c/ce/ArmbandGreen.png/revision/latest?cb=20161127174812" - }, - { - name: "Orange", - url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/e/e6/ArmbandOrange.png/revision/latest?cb=20161127174846" - }, - { - name: "Pink", - url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/f/f7/ArmbandPink.png/revision/latest?cb=20161127174854" - }, - { - name: "Red", - url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/1/14/Armband.png/revision/latest?cb=20161127174901" - }, - { - name: "Yellow", - url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/8/81/ArmbandYellow.png/revision/latest?cb=20161127174918" - }, - { - name: "White", - url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/c/c7/Armband_White.png/revision/latest?cb=20161127174926" - }, - { - name: "Altis", - url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/e/ee/Flag_alti_co.png/revision/latest?cb=20200820222622" - }, - { - name: "Asiain Pacific Alliance (APA)", - url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/c/c2/Flag_apa_co.png/revision/latest?cb=20200820222623" - }, - { - name: "Bear", - url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/e/e1/Flag_bear_co.png/revision/latest?cb=20200820222626" - }, - { - name: "Bohemia Interactive", - url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/e/ee/Flag_bi_co.png/revision/latest?cb=20200820222627" - }, - { - name: "Brain", - url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/7/7d/Flag_brain_co.png/revision/latest?cb=20200820222628" - }, - { - name: "Chernarussian Defence Forces (CDF)", - url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/d/d6/Flag_cdf_co.png/revision/latest?cb=20200820222629" - }, - { - name: "Chedaki (CHED)", - url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/9/96/Flag_ched_co.png/revision/latest?cb=20200820222630" - }, - { - name: "CHEL", - url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/9/98/Flag_chel_co.png/revision/latest?cb=20200820222631" - }, - { - name: "Chernarus", - url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/e/ef/Flag_chern_co.png/revision/latest?cb=20200820222632" - }, - { - name: "Chernarus Mining Corporation (CMC)", - url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/d/da/Flag_cmc_co.png/revision/latest?cb=20200820222634" - }, - { - name: "Rooster", - url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/4/44/Flag_cock_co.png/revision/latest?cb=20200820222635" - }, - { - name: "DayZ", - url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/b/b2/Flag_dayz_co.png/revision/latest?cb=20200820222636" - }, - { - name: "North Sahrani (DROS)", - url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/2/24/Flag_dros_co.png/revision/latest?cb=20200820222637" - }, - { - name: "Fawn", - url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/d/d2/Flag_fawn_co.png/revision/latest/scale-to-width-down/1000?cb=20200820222639" - }, - { - name: "Pirates", - url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/a/ab/Flag_jolly_co.png/revision/latest?cb=20200820222643" - }, - { - name: "Cannibals", - url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/4/42/Flag_jolly_c_co.png/revision/latest?cb=20200820222641" - }, - { - name: "South Sahrani (KOS)", - url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/a/a1/Flag_kos_co.png/revision/latest?cb=20200820222644" - }, - { - name: "Livonia Army (LDF)", - url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/c/c1/Flag_ldf_co.png/revision/latest?cb=20200820222645" - }, - { - name: "Livonia", - url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/e/e6/Flag_livo_co.png/revision/latest?cb=20200820222647" - }, - { - name: "NAPA", - url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/e/e4/Flag_napa_co.png/revision/latest?cb=20200820222648" - }, - { - name: "Livonia Police", - url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/6/63/Flag_police_co.png/revision/latest?cb=20200820222649" - }, - { - name: "TEC", - url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/e/ea/Flag_tec_co.png/revision/latest?cb=20200820222650" - }, - { - name: "United Earth Coalition (UEC)", - url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/c/ca/Flag_uec_co.png/revision/latest?cb=20200820222651" - }, - { - name: "Wolf", - url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/b/b2/Flag_wolf_co.png/revision/latest?cb=20200820222653" - }, - { - name: "Zenit Radio Station", - url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/0/05/Flag_zenit_co.png/revision/latest?cb=20200820222654" - }, - { - name: "Zombie Hunters", - url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/9/97/Flag_zhunters_co.png/revision/latest?cb=20200820222621" - }, - { - name: "RSTA", - url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/2/20/Flag_rsta_co.png/revision/latest/scale-to-width-down/1000?cb=20210216191221" - }, - { - name: "Refuge", - url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/8/8e/Flag_refuge_co.png/revision/latest/scale-to-width-down/1000?cb=20210216191205" - }, - { - name: "Snake", - url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/5/54/Flag_snake_co.png/revision/latest/scale-to-width-down/1000?cb=20210216191234" - }, - { - name: "Zagorky", - url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/7/75/Flag_zagorky_co.png/revision/latest/scale-to-width-down/1000?cb=20230619164704" - }, - { - name: "Crook", - url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/c/c8/Flag_crook_co.png/revision/latest/scale-to-width-down/1000?cb=20230619164705" - }, - { - name: "Rex", - url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/c/c5/Flag_rex_co.png/revision/latest/scale-to-width-down/1000?cb=20230619164706" - }, - ] -} diff --git a/src/database/armbands.ts b/src/database/armbands.ts new file mode 100644 index 0000000..ff5ef1e --- /dev/null +++ b/src/database/armbands.ts @@ -0,0 +1,93 @@ +export enum ArmbandName +{ + Black = "Black", + Blue = "Blue", + Green = "Green", + Orange = "Orange", + Pink = "Pink", + Red = "Red", + Yellow = "Yellow", + White = "White", + Altis = "Altis", + AsiainPacificAllianceAPA = "Asiain Pacific Alliance (APA)", + Bear = "Bear", + BohemiaInteractive = "Bohemia Interactive", + Brain = "Brain", + ChernarussianDefenceForcesCDF = "Chernarussian Defence Forces (CDF)", + ChedakiCHED = "Chedaki (CHED)", + CHEL = "CHEL", + Chernarus = "Chernarus", + ChernarusMiningCorporationCMC = "Chernarus Mining Corporation (CMC)", + Rooster = "Rooster", + DayZ = "DayZ", + NorthSahraniDROS = "North Sahrani (DROS)", + Fawn = "Fawn", + Pirates = "Pirates", + Cannibals = "Cannibals", + SouthSahraniKOS = "South Sahrani (KOS)", + LivoniaArmyLDF = "Livonia Army (LDF)", + Livonia = "Livonia", + NAPA = "NAPA", + LivoniaPolice = "Livonia Police", + TEC = "TEC", + UnitedEarthCoalitionUEC = "United Earth Coalition (UEC)", + Wolf = "Wolf", + ZenitRadioStation = "Zenit Radio Station", + ZombieHunters = "Zombie Hunters", + RSTA = "RSTA", + Refuge = "Refuge", + Snake = "Snake", + Zagorky = "Zagorky", + Crook = "Crook", + Rex = "Rex", +}; + +export interface Armband +{ + name: ArmbandName; + url: string; +}; + +export const Armbands: Armband[] = +[ + { name: ArmbandName.Black, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/8/82/ArmbandBlack.png/revision/latest?cb=20161127174754" }, + { name: ArmbandName.Blue, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/b/bd/ArmbandBlue.png/revision/latest?cb=20161127174803" }, + { name: ArmbandName.Green, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/c/ce/ArmbandGreen.png/revision/latest?cb=20161127174812" }, + { name: ArmbandName.Orange, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/e/e6/ArmbandOrange.png/revision/latest?cb=20161127174846" }, + { name: ArmbandName.Pink, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/f/f7/ArmbandPink.png/revision/latest?cb=20161127174854" }, + { name: ArmbandName.Red, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/1/14/Armband.png/revision/latest?cb=20161127174901" }, + { name: ArmbandName.Yellow, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/8/81/ArmbandYellow.png/revision/latest?cb=20161127174918" }, + { name: ArmbandName.White, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/c/c7/Armband_White.png/revision/latest?cb=20161127174926" }, + { name: ArmbandName.Altis, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/e/ee/Flag_alti_co.png/revision/latest?cb=20200820222622" }, + { name: ArmbandName.AsiainPacificAllianceAPA, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/c/c2/Flag_apa_co.png/revision/latest?cb=20200820222623" }, + { name: ArmbandName.Bear, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/e/e1/Flag_bear_co.png/revision/latest?cb=20200820222626" }, + { name: ArmbandName.BohemiaInteractive, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/e/ee/Flag_bi_co.png/revision/latest?cb=20200820222627" }, + { name: ArmbandName.Brain, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/7/7d/Flag_brain_co.png/revision/latest?cb=20200820222628" }, + { name: ArmbandName.ChernarussianDefenceForcesCDF, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/d/d6/Flag_cdf_co.png/revision/latest?cb=20200820222629" }, + { name: ArmbandName.ChedakiCHED, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/9/96/Flag_ched_co.png/revision/latest?cb=20200820222630" }, + { name: ArmbandName.CHEL, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/9/98/Flag_chel_co.png/revision/latest?cb=20200820222631" }, + { name: ArmbandName.Chernarus, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/e/ef/Flag_chern_co.png/revision/latest?cb=20200820222632" }, + { name: ArmbandName.ChernarusMiningCorporationCMC, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/d/da/Flag_cmc_co.png/revision/latest?cb=20200820222634" }, + { name: ArmbandName.Rooster, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/4/44/Flag_cock_co.png/revision/latest?cb=20200820222635" }, + { name: ArmbandName.DayZ, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/b/b2/Flag_dayz_co.png/revision/latest?cb=20200820222636" }, + { name: ArmbandName.NorthSahraniDROS, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/2/24/Flag_dros_co.png/revision/latest?cb=20200820222637" }, + { name: ArmbandName.Fawn, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/d/d2/Flag_fawn_co.png/revision/latest/scale-to-width-down/1000?cb=20200820222639" }, + { name: ArmbandName.Pirates, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/a/ab/Flag_jolly_co.png/revision/latest?cb=20200820222643" }, + { name: ArmbandName.Cannibals, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/4/42/Flag_jolly_c_co.png/revision/latest?cb=20200820222641" }, + { name: ArmbandName.SouthSahraniKOS, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/a/a1/Flag_kos_co.png/revision/latest?cb=20200820222644" }, + { name: ArmbandName.LivoniaArmyLDF, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/c/c1/Flag_ldf_co.png/revision/latest?cb=20200820222645" }, + { name: ArmbandName.Livonia, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/e/e6/Flag_livo_co.png/revision/latest?cb=20200820222647" }, + { name: ArmbandName.NAPA, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/e/e4/Flag_napa_co.png/revision/latest?cb=20200820222648" }, + { name: ArmbandName.LivoniaPolice, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/6/63/Flag_police_co.png/revision/latest?cb=20200820222649" }, + { name: ArmbandName.TEC, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/e/ea/Flag_tec_co.png/revision/latest?cb=20200820222650" }, + { name: ArmbandName.UnitedEarthCoalitionUEC, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/c/ca/Flag_uec_co.png/revision/latest?cb=20200820222651" }, + { name: ArmbandName.Wolf, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/b/b2/Flag_wolf_co.png/revision/latest?cb=20200820222653" }, + { name: ArmbandName.ZenitRadioStation, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/0/05/Flag_zenit_co.png/revision/latest?cb=20200820222654" }, + { name: ArmbandName.ZombieHunters, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/9/97/Flag_zhunters_co.png/revision/latest?cb=20200820222621" }, + { name: ArmbandName.RSTA, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/2/20/Flag_rsta_co.png/revision/latest/scale-to-width-down/1000?cb=20210216191221" }, + { name: ArmbandName.Refuge, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/8/8e/Flag_refuge_co.png/revision/latest/scale-to-width-down/1000?cb=20210216191205" }, + { name: ArmbandName.Snake, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/5/54/Flag_snake_co.png/revision/latest/scale-to-width-down/1000?cb=20210216191234" }, + { name: ArmbandName.Zagorky, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/7/75/Flag_zagorky_co.png/revision/latest/scale-to-width-down/1000?cb=20230619164704" }, + { name: ArmbandName.Crook, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/c/c8/Flag_crook_co.png/revision/latest/scale-to-width-down/1000?cb=20230619164705" }, + { name: ArmbandName.Rex, url: "https://static.wikia.nocookie.net/dayz_gamepedia/images/c/c5/Flag_rex_co.png/revision/latest/scale-to-width-down/1000?cb=20230619164706" }, +]; diff --git a/src/database/destinations.ts b/src/database/destinations.ts index fd919bb..c9e34dd 100644 --- a/src/database/destinations.ts +++ b/src/database/destinations.ts @@ -1,6 +1,7 @@ -import { calculateVector } from "@util/Vector"; +import { calculateVector } from "../util/Vector"; -export const Missions = { +export const Missions = +{ "dayzOffline.chernarusplus": "Chernarus", "dayzOffline.enoch": "Livonia", "dayzOffline.sakhal": "Sakhal", @@ -9,10 +10,11 @@ export const Missions = { export type MissionKey = keyof typeof Missions; export type MissionName = typeof Missions[MissionKey]; -export type Position = [number, number]; +export type Position = [number, number] | []; // Calculates the nearest location to a given coordinate -export function nearest(pos: Position, mission: MissionName): string { +export function nearest(pos: Position, mission: MissionName): string +{ let tempDest; let lastDist = 1000000; let destination_dir; @@ -28,13 +30,15 @@ export function nearest(pos: Position, mission: MissionName): string { return lastDist > 500 ? `${destination_dir} of ${tempDest}` : `Near ${tempDest}`; }; -interface Destination { +interface Destination +{ name: string; coord: Position; } // A curated list of destinations across DayZ Chernarus and Livonia -const destinations: Record> = { +const destinations: Record> = +{ Chernarus: [ { name: "Sinystok", diff --git a/src/database/guild.ts b/src/database/guild.ts index 33d51a6..7eb405d 100644 --- a/src/database/guild.ts +++ b/src/database/guild.ts @@ -1,29 +1,43 @@ import { Snowflake } from "discord.js"; import DayZR from "../DayZRBot"; -import { NitradoCredentialStatus } from "@util/NitradoAPI"; +import { NitradoCredentialStatus } from "../util/NitradoAPI"; +import { ArmbandName } from "./armbands"; +import { MissionName } from "./destinations"; -export const enum IntegerBoolean { +export const enum IntegerBoolean +{ FALSE, TRUE }; -export interface UAV { +export interface UAV +{ // TODO: fill this out }; -export interface Alarm { +export interface Alarm +{ // TODO: fill this out }; -export interface NitradoConfig { +export interface NitradoConfig +{ ServerID: string; UserID: string; Auth: string; Status: NitradoCredentialStatus; + Mission: MissionName, }; -interface GuildConfigAttributes { - serverID: Snowflake; +export interface FactionArmband +{ + faction: Snowflake; // faction role ID + armband: ArmbandName; +} + +interface GuildConfigAttributes +{ + serverID: Snowflake; // Guild ID lastLog: string | null; serverName: string; autoRestart: IntegerBoolean; @@ -31,40 +45,41 @@ interface GuildConfigAttributes { showKillfeedWeapon: IntegerBoolean; purchaseUAV: IntegerBoolean; purchaseEMP: IntegerBoolean; - allowedChannels: Array; + allowedChannels: Array; // Array of channel IDs customChannelStatus?: boolean; // optional - generated outside of DB hasBotAdmin?: boolean; // optional - generated outside of DB - killfeedChannel: Snowflake; - connectionLogsChannel: Snowflake; - activePlayersChannel: Snowflake; - welcomeChannel: Snowflake; + killfeedChannel: Snowflake; // Channel ID + connectionLogsChannel: Snowflake; // Channel ID + activePlayersChannel: Snowflake; // Channel ID + welcomeChannel: Snowflake; // Channel ID - factionArmbands: any; + factionArmbands: Record; usedArmbands: Array; excludedRoles: Array; - hasExcludedRoles?: boolean; // optional - generated outside of DB - botAdminRoles: Array; + hasExcludedRoles?: boolean; // optional - generated outside of DB + botAdminRoles: Array; // Array of role IDs alarms: Array; events: Array; uavs: Array; - incomeRoles: Array; + incomeRoles: Array; // Array of role IDs incomeLimiter: number; startingBalance: number; uavPrice: number; empPrice: number; - linkedGamertagRole: Snowflake; - memberRole: Snowflake; - adminRole: Snowflake; + linkedGamertagRole: Snowflake; // Role ID + memberRole: Snowflake; // Role ID + adminRole: Snowflake; // Role ID combatLogTimer: number; } -interface GuildConfigDB { +interface GuildConfigDB +{ server: GuildConfigAttributes; Nitrado: NitradoConfig | null; }; @@ -75,7 +90,8 @@ interface GuildConfigDB { // Also this is so stupid, in the DB all server attributes are under server, // while in the code we almost always have the attributes directly in top // level of object, i.e. guild.startingBalance instead of guild.server.startingBalance -export interface GuildConfig extends GuildConfigAttributes { +export interface GuildConfig extends GuildConfigAttributes +{ Nitrado: NitradoConfig | null; } diff --git a/src/database/player.js b/src/database/player.js deleted file mode 100644 index adb3db9..0000000 --- a/src/database/player.js +++ /dev/null @@ -1,131 +0,0 @@ -const { weapons } = require("./weapons"); - -// Creates a copy of an object to prevent mutation of parent (i.e BodyParts, createWeaponsObject) -const copy = (obj) => JSON.parse(JSON.stringify(obj)); - -const BodyParts = { - Head: 0, - Torso: 0, - RightArm: 0, - LeftArm: 0, - RightLeg: 0, - LeftLeg: 0, -}; - -const createWeaponsObject = (value) => { - const defaultWeapons = {}; - for (const [_, weaponNames] of Object.entries(weapons)) { - for (const [name, _] of Object.entries(weaponNames)) { - defaultWeapons[name] = value; - } - } - return copy(defaultWeapons); -}; - -module.exports = { - UpdatePlayer: async (client, player, interaction = null) => { - /* Wrapping this function in a promise solves some bugs */ - return new Promise(resolve => { - client.dbo.collection("players").updateOne( - { "playerID": player.playerID }, - { $set: { ...player } }, - { upsert: true }, // Create player stat document if it does not exist - (err, _) => { - if (err) { - if (interaction == null) return client.error(`UpdatePlayer Error: ${err}`); - else return client.sendInternalError(interaction, `UpdatePlayer Error: ${err}`); - } else resolve(); - } - ); - }); - }, - - getDefaultPlayer(gamertag, playerId, nitradoServerId) { - return { - // Identifiers - gamertag: gamertag, - playerID: playerId, - discordID: "", - nitradoServerID: nitradoServerId, - - // General PVP Stats - KDR: 0.00, - kills: 0, - deaths: 0, - killStreak: 0, - bestKillStreak: 0, - longestKill: 0, - deathStreak: 0, - worstDeathStreak: 0, - - // In depth PVP Stats - shotsLanded: 0, - timesShot: 0, - shotsLandedPerBodyPart: copy(BodyParts), - timesShotPerBodyPart: copy(BodyParts), - weaponStats: createWeaponsObject({ - kills: 0, - deaths: 0, - shotsLanded: 0, - timesShot: 0, - shotsLandedPerBodyPart: copy(BodyParts), - timesShotPerBodyPart: copy(BodyParts), - }), - combatRating: 800, - highestCombatRating: 800, - lowestCombatRating: 800, - combatRatingHistory: [800], - - // General Session Data - lastConnectionDate: null, - lastDisconnectionDate: null, - lastDamageDate: null, - lastDeathDate: null, - lastHitBy: null, - connected: false, - pos: [], - lastPos: [], - time: null, - lastTime: null, - - // Session Stats - totalSessionTime: 0, - lastSessionTime: 0, - longestSessionTime: 0, - connections: 0, - - // Other - bounties: [], - bountiesLength: 0, - } - }, - - insertPVPstats(player) { - player.shotsLanded = 0; - player.timesShot = 0; - player.shotsLandedPerBodyPart = copy(BodyParts); - player.timesShotPerBodyPart = copy(BodyParts); - player.weaponStats = createWeaponsObject({ - kills: 0, - deaths: 0, - shotsLanded: 0, - timesShot: 0, - shotsLandedPerBodyPart: copy(BodyParts), - timesShotPerBodyPart: copy(BodyParts), - }); - return player; - }, - - // If a new weapon is not in the existing weaponStats, this will add it. - createWeaponStats(player, weapon) { - player.weaponStats[weapon] = { - kills: 0, - deaths: 0, - shotsLanded: 0, - timesShot: 0, - shotsLandedPerBodyPart: copy(BodyParts), - timesShotPerBodyPart: copy(BodyParts), - } - return player; - } -} diff --git a/src/database/player.ts b/src/database/player.ts new file mode 100644 index 0000000..ccb26b2 --- /dev/null +++ b/src/database/player.ts @@ -0,0 +1,232 @@ +import { ChatInputCommandInteraction } from "discord.js"; +import { weapons, AllWeapons } from "./weapons"; +import { Snowflake } from "discord.js"; +import { Position } from "./destinations" +import DayZR from "../DayZRBot"; + +// Creates a copy of an object to prevent mutation of parent (i.e BodyParts, createWeaponsObject) +const copy = (obj: T): T => JSON.parse(JSON.stringify(obj)); + +interface BodyPartStats +{ + Head: number; + Torso: number; + RightArm: number; + LeftArm: number; + RightLeg: number; + LeftLeg: number; +} + +interface IndividualWeaponStats +{ + kills: number; + deaths: number; + shotsLanded: number; + timesShot: number; + shotsLandedPerBodyPart: BodyPartStats; + timesShotPerBodyPart: BodyPartStats; +} + +const DefaultBodyPartStats: BodyPartStats = +{ + Head: 0, + Torso: 0, + RightArm: 0, + LeftArm: 0, + RightLeg: 0, + LeftLeg: 0, +}; + +interface WeaponStats +{ + [weaponName: string]: IndividualWeaponStats; +} + +const createWeaponsObject = (defaultWeaponStats: IndividualWeaponStats): WeaponStats => +{ + const defaultWeapons: WeaponStats = {}; + for (const [_, weaponNames] of Object.entries(weapons)) { + for (const [name, _] of Object.entries(weaponNames)) { + defaultWeapons[name] = defaultWeaponStats; + } + } + return copy(defaultWeapons); +}; + +export interface Player +{ + // Identifiers + gamertag: string, + playerID: string, + discordID: Snowflake, + nitradoServerID: string, + + // General PVP Stats + KDR: number, + kills: number, + deaths: number, + killStreak: number, + bestKillStreak: number, + longestKill: number, + deathStreak: number, + worstDeathStreak: number, + + // In depth PVP Stats + shotsLanded: number, + timesShot: number, + shotsLandedPerBodyPart: BodyPartStats, + timesShotPerBodyPart: BodyPartStats, + weaponStats: WeaponStats, + combatRating: number, + highestCombatRating: number, + lowestCombatRating: number, + combatRatingHistory: Array, + + // General Session Data + lastConnectionDate: Date | null, + lastDisconnectionDate: Date | null, + lastDamageDate: Date | null, + lastDeathDate: Date | null, + lastHitBy: string | null, + connected: boolean, + pos: Position, + lastPos: Position, + time: string | null, + lastTime: string | null, + + // Session Stats + totalSessionTime: number, + lastSessionTime: number, + longestSessionTime: number, + connections: number, + + // Other + bounties: Array, // TODO, define bounty object + bountiesLength: number, +} + +export const UpdatePlayer = async( + client: DayZR, + player: Player, + interaction: ChatInputCommandInteraction | null = null +): Promise => +{ + /* Wrapping this function in a promise solves some bugs */ + return new Promise(resolve => { + client.dbo.collection("players").updateOne( + { "playerID": player.playerID }, + { $set: { ...player } }, + { upsert: true }, // Create player stat document if it does not exist + (err: string) => { + if (err) + { + if (interaction == null) return client.error(`UpdatePlayer Error: ${err}`); + else return client.sendInternalError(interaction, `UpdatePlayer Error: ${err}`); + } else resolve(); + } + ); + }); +}; + +// TODO: fix magic NUMBERS!!! +export const getDefaultPlayer = ( + gamertag: string, + playerId: string, + nitradoServerId: string +): Player => +{ + return { + // Identifiers + gamertag: gamertag, + playerID: playerId, + discordID: "", + nitradoServerID: nitradoServerId, + + // General PVP Stats + KDR: 0.00, + kills: 0, + deaths: 0, + killStreak: 0, + bestKillStreak: 0, + longestKill: 0, + deathStreak: 0, + worstDeathStreak: 0, + + // In depth PVP Stats + shotsLanded: 0, + timesShot: 0, + shotsLandedPerBodyPart: copy(DefaultBodyPartStats), + timesShotPerBodyPart: copy(DefaultBodyPartStats), + weaponStats: createWeaponsObject({ + kills: 0, + deaths: 0, + shotsLanded: 0, + timesShot: 0, + shotsLandedPerBodyPart: copy(DefaultBodyPartStats), + timesShotPerBodyPart: copy(DefaultBodyPartStats), + }), + combatRating: 800, + highestCombatRating: 800, + lowestCombatRating: 800, + combatRatingHistory: [ 800 ], + + // General Session Data + lastConnectionDate: null, + lastDisconnectionDate: null, + lastDamageDate: null, + lastDeathDate: null, + lastHitBy: null, + connected: false, + pos: [], + lastPos: [], + time: null, + lastTime: null, + + // Session Stats + totalSessionTime: 0, + lastSessionTime: 0, + longestSessionTime: 0, + connections: 0, + + // Other + bounties: [], + bountiesLength: 0, + } +}; + +export const insertPVPstats = (player: Player): Player => +{ + player.shotsLanded = 0; + player.timesShot = 0; + player.shotsLandedPerBodyPart = copy(DefaultBodyPartStats); + player.timesShotPerBodyPart = copy(DefaultBodyPartStats); + player.weaponStats = createWeaponsObject( + { + kills: 0, + deaths: 0, + shotsLanded: 0, + timesShot: 0, + shotsLandedPerBodyPart: copy(DefaultBodyPartStats), + timesShotPerBodyPart: copy(DefaultBodyPartStats), + }); + + return player; +}; + +// If a new weapon is not in the existing weaponStats, this will add it. +export const createWeaponStats = ( + player: Player, + weapon: AllWeapons +): Player => +{ + player.weaponStats[weapon] = { + kills: 0, + deaths: 0, + shotsLanded: 0, + timesShot: 0, + shotsLandedPerBodyPart: copy(DefaultBodyPartStats), + timesShotPerBodyPart: copy(DefaultBodyPartStats), + } + + return player; +}; diff --git a/src/database/weapons.js b/src/database/weapons.js deleted file mode 100644 index dda8977..0000000 --- a/src/database/weapons.js +++ /dev/null @@ -1,77 +0,0 @@ -module.exports = { - weapons: { - handguns: { - "CR-75": "https://static.wikia.nocookie.net/dayz_gamepedia/images/4/40/CZ75.png/revision/latest/scale-to-width-down/112?cb=20210505021307", - "Deagle": "https://static.wikia.nocookie.net/dayz_gamepedia/images/6/62/Deagle.png/revision/latest/scale-to-width-down/127?cb=20210512003023", - "Derringer": "https://static.wikia.nocookie.net/dayz_gamepedia/images/9/9f/Derringer_Black.png/revision/latest/scale-to-width-down/105?cb=20220521175445", - "FX-45": "https://static.wikia.nocookie.net/dayz_gamepedia/images/f/fd/FNX45.png/revision/latest/scale-to-width-down/104?cb=20210505025055", - "IJ-70": "https://static.wikia.nocookie.net/dayz_gamepedia/images/2/26/MakarovIJ70.png/revision/latest/scale-to-width-down/92?cb=20210209000551", - "Kolt 1911": "https://static.wikia.nocookie.net/dayz_gamepedia/images/f/f9/Colt1911.png/revision/latest/scale-to-width-down/112?cb=20210505030200", - "Longhorn": "https://static.wikia.nocookie.net/dayz_gamepedia/images/7/79/Longhorn.png/revision/latest/scale-to-width-down/222?cb=20220324214533", - "MK II": "https://static.wikia.nocookie.net/dayz_gamepedia/images/0/0d/MKII.png/revision/latest/scale-to-width-down/171?cb=20210210153348", - "Mlock-91": "https://static.wikia.nocookie.net/dayz_gamepedia/images/9/9b/Glock19.png/revision/latest/scale-to-width-down/121?cb=20210505024259", - "P1": "https://static.wikia.nocookie.net/dayz_gamepedia/images/c/cc/P1.png/revision/latest/scale-to-width-down/120?cb=20220518204515", - "Revolver": "https://static.wikia.nocookie.net/dayz_gamepedia/images/6/6d/Revolver.png/revision/latest/scale-to-width-down/148?cb=20210208232303", - "Signal Pistol": "https://static.wikia.nocookie.net/dayz_gamepedia/images/a/a7/Flaregun.png/revision/latest/scale-to-width-down/107?cb=20210501150913", - }, - shotguns: { - "BK-12": "https://static.wikia.nocookie.net/dayz_gamepedia/images/c/cb/Izh18Shotgun.png/revision/latest/scale-to-width-down/256?cb=20220922184507", - "BK-133": "https://static.wikia.nocookie.net/dayz_gamepedia/images/5/5c/MP-133-Shotgun.png/revision/latest/scale-to-width-down/256?cb=20210210190104", - "BK-43": "https://static.wikia.nocookie.net/dayz_gamepedia/images/c/c7/Izh43Shotgun.png/revision/latest/scale-to-width-down/256?cb=20210210185835", - "Vaiga": "https://static.wikia.nocookie.net/dayz_gamepedia/images/c/c8/Vaiga.png/revision/latest/scale-to-width-down/256?cb=20220220185225", - }, - subMachineGuns: { - "Bizon": "https://static.wikia.nocookie.net/dayz_gamepedia/images/a/af/PP19.png/revision/latest/scale-to-width-down/251?cb=20220127132305", - "CR-61 Skorpion": "https://static.wikia.nocookie.net/dayz_gamepedia/images/6/63/VZ61Scorpion.png/revision/latest/scale-to-width-down/222?cb=20220518204508", - "SG5-K": "https://static.wikia.nocookie.net/dayz_gamepedia/images/f/fc/MP5-K.png/revision/latest/scale-to-width-down/158?cb=20220221011343", - "USG-45": "https://static.wikia.nocookie.net/dayz_gamepedia/images/d/d7/UMP45.png/revision/latest/scale-to-width-down/153?cb=20220221002354", - }, - assaultRifles: { - "AUR A1": "https://static.wikia.nocookie.net/dayz_gamepedia/images/e/e6/AugShort.png/revision/latest/scale-to-width-down/173?cb=20211104175243", - "AUR AX": "https://static.wikia.nocookie.net/dayz_gamepedia/images/b/be/Aug.png/revision/latest/scale-to-width-down/233?cb=20211104182427", - "KA-101": "https://static.wikia.nocookie.net/dayz_gamepedia/images/f/f2/AK101.png/revision/latest/scale-to-width-down/251?cb=20210207040122", - "KA-74": "https://static.wikia.nocookie.net/dayz_gamepedia/images/8/8b/AK74.png/revision/latest/scale-to-width-down/253?cb=20210505013141", - "KAS-74U": "https://static.wikia.nocookie.net/dayz_gamepedia/images/0/0b/AKS74U.png/revision/latest/scale-to-width-down/191?cb=20210505014222", - "KA-M": "https://static.wikia.nocookie.net/dayz_gamepedia/images/6/6c/AKM.png/revision/latest/scale-to-width-down/244?cb=20210505011614", - "LE-MAS": "https://static.wikia.nocookie.net/dayz_gamepedia/images/2/21/FAMAS.png/revision/latest/scale-to-width-down/197?cb=20210902183114", - "M16-A2": "https://static.wikia.nocookie.net/dayz_gamepedia/images/b/b3/M16-A2.png/revision/latest/scale-to-width-down/256?cb=20220221002601", - "M4-A1": "https://static.wikia.nocookie.net/dayz_gamepedia/images/a/a1/M4A1.png/revision/latest/scale-to-width-down/223?cb=20220330014851", - "SVAL": "https://static.wikia.nocookie.net/dayz_gamepedia/images/3/39/ASVAL.png/revision/latest/scale-to-width-down/256?cb=20210208015731", - "Vikhr": "https://static.wikia.nocookie.net/dayz_gamepedia/images/0/0d/Vikhr.png/revision/latest/scale-to-width-down/173?cb=20240116163108" - }, - battleRifles: { - "LAR": "https://static.wikia.nocookie.net/dayz_gamepedia/images/e/e9/FAL.png/revision/latest/scale-to-width-down/256?cb=20220221001123", - }, - boltActionRifles: { - "CR-527": "https://static.wikia.nocookie.net/dayz_gamepedia/images/f/f0/CR527Wood.png/revision/latest/scale-to-width-down/256?cb=20220518204503 ", - "CR-550 Savanna": "https://static.wikia.nocookie.net/dayz_gamepedia/images/4/44/CR-550_Savanna.png/revision/latest/scale-to-width-down/256?cb=20220518204410", - "M70 Tundra": "https://static.wikia.nocookie.net/dayz_gamepedia/images/6/62/Winchester70.png/revision/latest/scale-to-width-down/256?cb=20220517152918", - "Mosin 91/30": "https://static.wikia.nocookie.net/dayz_gamepedia/images/a/a8/Mosin9130.png/revision/latest/scale-to-width-down/256?cb=20230126021955", - "Pioneer": "https://static.wikia.nocookie.net/dayz_gamepedia/images/6/69/Scout.png/revision/latest/scale-to-width-down/256?cb=20220518204357", - "SSG 82": "https://static.wikia.nocookie.net/dayz_gamepedia/images/1/10/SSG82.png/revision/latest/scale-to-width-down/256?cb=20220922192455", - "VS-89": "https://static.wikia.nocookie.net/dayz_gamepedia/images/e/ea/SV98.png/revision/latest/scale-to-width-down/256?cb=20240424164607", - }, - breakActionRifles: { - "BK-18": "https://static.wikia.nocookie.net/dayz_gamepedia/images/c/cb/IZH18_Rifle.png/revision/latest/scale-to-width-down/256?cb=20220517154121", - "Blaze": "https://static.wikia.nocookie.net/dayz_gamepedia/images/8/8a/Blaze_95_Double_Rifle_Wood.png/revision/latest/scale-to-width-down/256?cb=20220517154129", - }, - leverActionRifles: { - "Repeater Carbine": "https://static.wikia.nocookie.net/dayz_gamepedia/images/c/ce/Repeater.png/revision/latest/scale-to-width-down/256?cb=20220517154151", - }, - marksmanRifles: { - "VSD": "https://static.wikia.nocookie.net/dayz_gamepedia/images/a/a2/SVD_w._PSO-1.png/revision/latest/scale-to-width-down/256?cb=20220220235826", - "VSS": "https://static.wikia.nocookie.net/dayz_gamepedia/images/8/83/VSSVintorez.png/revision/latest/scale-to-width-down/256?cb=20210208202042", - }, - semiAutomaticRifles: { - "DMR": "https://static.wikia.nocookie.net/dayz_gamepedia/images/b/b4/M14.png/revision/latest/scale-to-width-down/350?cb=20231005142636", - "SK 59/66": "https://static.wikia.nocookie.net/dayz_gamepedia/images/f/fe/SKS.png/revision/latest/scale-to-width-down/256?cb=20220517154633", - "Sporter 22": "https://static.wikia.nocookie.net/dayz_gamepedia/images/5/5b/Sporter_22_Wood.png/revision/latest/scale-to-width-down/256?cb=20220518204154", - }, - other: { - "Crossbow": "https://static.wikia.nocookie.net/dayz_gamepedia/images/7/79/Crossbow.png/revision/latest/scale-to-width-down/212?cb=20180121164101", - "M79": "https://static.wikia.nocookie.net/dayz_gamepedia/images/b/b7/M79.png/revision/latest/scale-to-width-down/256?cb=20220521184052", - }, - }, - - weaponClassOf: (weapon) => Object.keys(module.exports.weapons).filter(c => weapon in module.exports.weapons[c])[0], -} diff --git a/src/database/weapons.ts b/src/database/weapons.ts new file mode 100644 index 0000000..a42426c --- /dev/null +++ b/src/database/weapons.ts @@ -0,0 +1,149 @@ +export type WeaponCategory = + | "handguns" + | "shotguns" + | "subMachineGuns" + | "assaultRifles" + | "battleRifles" + | "boltActionRifles" + | "breakActionRifles" + | "leverActionRifles" + | "marksmanRifles" + | "semiAutomaticRifles" + | "other"; + +export type HandgunWeapon = + | "CR-75" | "Deagle" | "Derringer" | "FX-45" | "IJ-70" | "Kolt 1911" + | "Longhorn" | "MK II" | "Mlock-91" | "P1" | "Revolver" | "Signal Pistol"; + +export type ShotgunWeapon = + | "BK-12" | "BK-133" | "BK-43" | "Vaiga"; + +export type SubMachineGunWeapon = + | "Bizon" | "CR-61 Skorpion" | "SG5-K" | "USG-45"; + +export type AssaultRifleWeapon = + | "AUR A1" | "AUR AX" | "KA-101" | "KA-74" | "KAS-74U" | "KA-M" + | "LE-MAS" | "M16-A2" | "M4-A1" | "SVAL" | "Vikhr"; + +export type BattleRifleWeapon = "LAR"; + +export type BoltActionRifleWeapon = + | "CR-527" | "CR-550 Savanna" | "M70 Tundra" | "Mosin 91/30" + | "Pioneer" | "SSG 82" | "VS-89"; + +export type BreakActionRifleWeapon = "BK-18" | "Blaze"; + +export type LeverActionRifleWeapon = "Repeater Carbine"; + +export type MarksmanRifleWeapon = "VSD" | "VSS"; + +export type SemiAutomaticRifleWeapon = "DMR" | "SK 59/66" | "Sporter 22"; + +export type OtherWeapon = "Crossbow" | "M79"; + +export interface Weapons +{ + handguns: Record; + shotguns: Record; + subMachineGuns: Record; + assaultRifles: Record; + battleRifles: Record; + boltActionRifles: Record; + breakActionRifles: Record; + leverActionRifles: Record; + marksmanRifles: Record; + semiAutomaticRifles: Record; + other: Record; +} + +export type AllWeapons = + | HandgunWeapon + | ShotgunWeapon + | SubMachineGunWeapon + | AssaultRifleWeapon + | BattleRifleWeapon + | BoltActionRifleWeapon + | BreakActionRifleWeapon + | LeverActionRifleWeapon + | MarksmanRifleWeapon + | SemiAutomaticRifleWeapon + | OtherWeapon; + +export const weapons: Weapons = +{ + handguns: { + "CR-75": "https://static.wikia.nocookie.net/dayz_gamepedia/images/4/40/CZ75.png/revision/latest/scale-to-width-down/112?cb=20210505021307", + "Deagle": "https://static.wikia.nocookie.net/dayz_gamepedia/images/6/62/Deagle.png/revision/latest/scale-to-width-down/127?cb=20210512003023", + "Derringer": "https://static.wikia.nocookie.net/dayz_gamepedia/images/9/9f/Derringer_Black.png/revision/latest/scale-to-width-down/105?cb=20220521175445", + "FX-45": "https://static.wikia.nocookie.net/dayz_gamepedia/images/f/fd/FNX45.png/revision/latest/scale-to-width-down/104?cb=20210505025055", + "IJ-70": "https://static.wikia.nocookie.net/dayz_gamepedia/images/2/26/MakarovIJ70.png/revision/latest/scale-to-width-down/92?cb=20210209000551", + "Kolt 1911": "https://static.wikia.nocookie.net/dayz_gamepedia/images/f/f9/Colt1911.png/revision/latest/scale-to-width-down/112?cb=20210505030200", + "Longhorn": "https://static.wikia.nocookie.net/dayz_gamepedia/images/7/79/Longhorn.png/revision/latest/scale-to-width-down/222?cb=20220324214533", + "MK II": "https://static.wikia.nocookie.net/dayz_gamepedia/images/0/0d/MKII.png/revision/latest/scale-to-width-down/171?cb=20210210153348", + "Mlock-91": "https://static.wikia.nocookie.net/dayz_gamepedia/images/9/9b/Glock19.png/revision/latest/scale-to-width-down/121?cb=20210505024259", + "P1": "https://static.wikia.nocookie.net/dayz_gamepedia/images/c/cc/P1.png/revision/latest/scale-to-width-down/120?cb=20220518204515", + "Revolver": "https://static.wikia.nocookie.net/dayz_gamepedia/images/6/6d/Revolver.png/revision/latest/scale-to-width-down/148?cb=20210208232303", + "Signal Pistol": "https://static.wikia.nocookie.net/dayz_gamepedia/images/a/a7/Flaregun.png/revision/latest/scale-to-width-down/107?cb=20210501150913", + }, + shotguns: { + "BK-12": "https://static.wikia.nocookie.net/dayz_gamepedia/images/c/cb/Izh18Shotgun.png/revision/latest/scale-to-width-down/256?cb=20220922184507", + "BK-133": "https://static.wikia.nocookie.net/dayz_gamepedia/images/5/5c/MP-133-Shotgun.png/revision/latest/scale-to-width-down/256?cb=20210210190104", + "BK-43": "https://static.wikia.nocookie.net/dayz_gamepedia/images/c/c7/Izh43Shotgun.png/revision/latest/scale-to-width-down/256?cb=20210210185835", + "Vaiga": "https://static.wikia.nocookie.net/dayz_gamepedia/images/c/c8/Vaiga.png/revision/latest/scale-to-width-down/256?cb=20220220185225", + }, + subMachineGuns: { + "Bizon": "https://static.wikia.nocookie.net/dayz_gamepedia/images/a/af/PP19.png/revision/latest/scale-to-width-down/251?cb=20220127132305", + "CR-61 Skorpion": "https://static.wikia.nocookie.net/dayz_gamepedia/images/6/63/VZ61Scorpion.png/revision/latest/scale-to-width-down/222?cb=20220518204508", + "SG5-K": "https://static.wikia.nocookie.net/dayz_gamepedia/images/f/fc/MP5-K.png/revision/latest/scale-to-width-down/158?cb=20220221011343", + "USG-45": "https://static.wikia.nocookie.net/dayz_gamepedia/images/d/d7/UMP45.png/revision/latest/scale-to-width-down/153?cb=20220221002354", + }, + assaultRifles: { + "AUR A1": "https://static.wikia.nocookie.net/dayz_gamepedia/images/e/e6/AugShort.png/revision/latest/scale-to-width-down/173?cb=20211104175243", + "AUR AX": "https://static.wikia.nocookie.net/dayz_gamepedia/images/b/be/Aug.png/revision/latest/scale-to-width-down/233?cb=20211104182427", + "KA-101": "https://static.wikia.nocookie.net/dayz_gamepedia/images/f/f2/AK101.png/revision/latest/scale-to-width-down/251?cb=20210207040122", + "KA-74": "https://static.wikia.nocookie.net/dayz_gamepedia/images/8/8b/AK74.png/revision/latest/scale-to-width-down/253?cb=20210505013141", + "KAS-74U": "https://static.wikia.nocookie.net/dayz_gamepedia/images/0/0b/AKS74U.png/revision/latest/scale-to-width-down/191?cb=20210505014222", + "KA-M": "https://static.wikia.nocookie.net/dayz_gamepedia/images/6/6c/AKM.png/revision/latest/scale-to-width-down/244?cb=20210505011614", + "LE-MAS": "https://static.wikia.nocookie.net/dayz_gamepedia/images/2/21/FAMAS.png/revision/latest/scale-to-width-down/197?cb=20210902183114", + "M16-A2": "https://static.wikia.nocookie.net/dayz_gamepedia/images/b/b3/M16-A2.png/revision/latest/scale-to-width-down/256?cb=20220221002601", + "M4-A1": "https://static.wikia.nocookie.net/dayz_gamepedia/images/a/a1/M4A1.png/revision/latest/scale-to-width-down/223?cb=20220330014851", + "SVAL": "https://static.wikia.nocookie.net/dayz_gamepedia/images/3/39/ASVAL.png/revision/latest/scale-to-width-down/256?cb=20210208015731", + "Vikhr": "https://static.wikia.nocookie.net/dayz_gamepedia/images/0/0d/Vikhr.png/revision/latest/scale-to-width-down/173?cb=20240116163108", + }, + battleRifles: { + "LAR": "https://static.wikia.nocookie.net/dayz_gamepedia/images/e/e9/FAL.png/revision/latest/scale-to-width-down/256?cb=20220221001123", + }, + boltActionRifles: { + "CR-527": "https://static.wikia.nocookie.net/dayz_gamepedia/images/f/f0/CR527Wood.png/revision/latest/scale-to-width-down/256?cb=20220518204503", + "CR-550 Savanna": "https://static.wikia.nocookie.net/dayz_gamepedia/images/4/44/CR-550_Savanna.png/revision/latest/scale-to-width-down/256?cb=20220518204410", + "M70 Tundra": "https://static.wikia.nocookie.net/dayz_gamepedia/images/6/62/Winchester70.png/revision/latest/scale-to-width-down/256?cb=20220517152918", + "Mosin 91/30": "https://static.wikia.nocookie.net/dayz_gamepedia/images/a/a8/Mosin9130.png/revision/latest/scale-to-width-down/256?cb=20230126021955", + "Pioneer": "https://static.wikia.nocookie.net/dayz_gamepedia/images/6/69/Scout.png/revision/latest/scale-to-width-down/256?cb=20220518204357", + "SSG 82": "https://static.wikia.nocookie.net/dayz_gamepedia/images/1/10/SSG82.png/revision/latest/scale-to-width-down/256?cb=20220922192455", + "VS-89": "https://static.wikia.nocookie.net/dayz_gamepedia/images/e/ea/SV98.png/revision/latest/scale-to-width-down/256?cb=20240424164607", + }, + breakActionRifles: { + "BK-18": "https://static.wikia.nocookie.net/dayz_gamepedia/images/c/cb/IZH18_Rifle.png/revision/latest/scale-to-width-down/256?cb=20220517154121", + "Blaze": "https://static.wikia.nocookie.net/dayz_gamepedia/images/8/8a/Blaze_95_Double_Rifle_Wood.png/revision/latest/scale-to-width-down/256?cb=20220517154129", + }, + leverActionRifles: { + "Repeater Carbine": "https://static.wikia.nocookie.net/dayz_gamepedia/images/c/ce/Repeater.png/revision/latest/scale-to-width-down/256?cb=20220517154151", + }, + marksmanRifles: { + "VSD": "https://static.wikia.nocookie.net/dayz_gamepedia/images/a/a2/SVD_w._PSO-1.png/revision/latest/scale-to-width-down/256?cb=20220220235826", + "VSS": "https://static.wikia.nocookie.net/dayz_gamepedia/images/8/83/VSSVintorez.png/revision/latest/scale-to-width-down/256?cb=20210208202042", + }, + semiAutomaticRifles: { + "DMR": "https://static.wikia.nocookie.net/dayz_gamepedia/images/b/b4/M14.png/revision/latest/scale-to-width-down/350?cb=20231005142636", + "SK 59/66": "https://static.wikia.nocookie.net/dayz_gamepedia/images/f/fe/SKS.png/revision/latest/scale-to-width-down/256?cb=20220517154633", + "Sporter 22": "https://static.wikia.nocookie.net/dayz_gamepedia/images/5/5b/Sporter_22_Wood.png/revision/latest/scale-to-width-down/256?cb=20220518204154", + }, + other: { + "Crossbow": "https://static.wikia.nocookie.net/dayz_gamepedia/images/7/79/Crossbow.png/revision/latest/scale-to-width-down/212?cb=20180121164101", + "M79": "https://static.wikia.nocookie.net/dayz_gamepedia/images/b/b7/M79.png/revision/latest/scale-to-width-down/256?cb=20220521184052", + }, +}; + +export const weaponClassOf = (weapon: AllWeapons): WeaponCategory | undefined => { + return (Object.keys(weapons) as WeaponCategory[]).find((category) => weapon in weapons[category]); +} diff --git a/src/index.ts b/src/index.ts index 2767ca3..72aca4a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ import { ShardingManager } from "discord.js"; -import config from "./config/config"; +import { config } from "./config/config"; // ShardingManager spawns node instances and we need to explicitely point to dist/bot.js const manager: ShardingManager = new ShardingManager("./dist/bot.js", { token: config.Token }); diff --git a/src/types/discord.d.ts b/src/types/discord.d.ts new file mode 100644 index 0000000..4127c69 --- /dev/null +++ b/src/types/discord.d.ts @@ -0,0 +1,23 @@ +import { bgYellow } from "colors"; +import "discord.js"; + +/** + * Type augmentation allows us to add custom functions to the + * ChatInputCommandInteraction without typescript complaining + */ +declare module "discord.js" { + interface ChatInputCommandInteraction { + send( + content: string | { embeds?: (EmbedBuilder | APIEmbed)[]; [key: string]: any } + ): Promise; + deferReply( + content?: string | { embeds?: (EmbedBuilder | APIEmbed)[]; [key: string]: any } + ): Promise; + showModal( + content: string | { embeds?: (EmbedBuilder | APIEmbed)[]; [key: string]: any } + ): Promise; + editReply( + content: string | { embeds?: (EmbedBuilder | APIEmbed)[]; [key: string]: any } + ): Promise; + } +} diff --git a/src/types/readline.d.ts b/src/types/readline.d.ts new file mode 100644 index 0000000..4e3feb6 --- /dev/null +++ b/src/types/readline.d.ts @@ -0,0 +1,12 @@ +import { Readable } from "stream"; + +// cannot get a warning to go away, this is like the only solution +declare module "node:readline" { + import { Interface, ReadLineOptions } from "readline"; + + interface ReadLineOptionsFixed extends ReadLineOptions { + input: Readable; // 👈 force Node stream + } + + export function createInterface(options: ReadLineOptionsFixed): Interface; +} \ No newline at end of file diff --git a/src/util/Logger.js b/src/util/Logger.ts similarity index 62% rename from src/util/Logger.js rename to src/util/Logger.ts index 49adb16..6becc75 100644 --- a/src/util/Logger.js +++ b/src/util/Logger.ts @@ -1,40 +1,44 @@ -const winston = require("winston"); -const colors = require("colors"); +import winston from "winston"; +import colors from "colors"; -class Logger { - constructor(LoggingFile) { +export default class Logger { + + public logger: winston.Logger; + + constructor(loggingFile: string) + { this.logger = winston.createLogger({ - transports: [new winston.transports.File({ filename: LoggingFile })], + transports: [new winston.transports.File({ filename: loggingFile })], }); } - log(Text) { + log(text: string): void + { let d = new Date(); this.logger.log({ level: "info", message: - `${d.getHours()}:${d.getMinutes()} - ${d.getMonth() + 1}:${d.getDate()}:${d.getFullYear()} | Info: ` + Text + `${d.getHours()}:${d.getMinutes()} - ${d.getMonth() + 1}:${d.getDate()}:${d.getFullYear()} | Info: ` + text }); console.log( colors.green( `${d.getMonth() + 1}:${d.getDate()}:${d.getFullYear()} - ${d.getHours()}:${d.getMinutes()}` - ) + colors.yellow(" | Info: " + Text) + ) + colors.yellow(" | Info: " + text) ); } - error(Text) { + error(text: string): void + { let d = new Date(); this.logger.log({ level: "error", message: - `${d.getHours()}:${d.getMinutes()} - ${d.getMonth() + 1}:${d.getDate()}:${d.getFullYear()} | Error: ` + Text + `${d.getHours()}:${d.getMinutes()} - ${d.getMonth() + 1}:${d.getDate()}:${d.getFullYear()} | Error: ` + text }); console.log( colors.green( `${d.getMonth() + 1}:${d.getDate()}:${d.getFullYear()} - ${d.getHours()}:${d.getMinutes()}` - ) + colors.yellow(" | Error: ") + colors.red(Text) + ) + colors.yellow(" | Error: ") + colors.red(text) ); } } - -module.exports = Logger; \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index b9f2d3b..24b2531 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,12 +10,15 @@ "resolveJsonModule": true, "forceConsistentCasingInFileNames": true, "allowJs": true, - "baseUrl": "./src", + "lib": ["es2020"], + "types": ["node"], + "moduleResolution": "node", + "baseUrl": ".", "paths": { - "@util/*": ["util/*"], - "@db/*": ["database/*"] + "@util/*": ["src/util/*"], + "@db/*": ["src/database/*"] } }, - "include": ["src"], + "include": ["src", "src/types"], "exclude": ["node_modules"] } From 2889ff978a35851eb4075e3168f823aacadcea91 Mon Sep 17 00:00:00 2001 From: SowinskiBraeden Date: Tue, 30 Sep 2025 12:13:12 -0700 Subject: [PATCH 2/6] fix/command types --- src/commands/admin.js | 18 ++++----- src/commands/alarm.js | 32 +++++++-------- src/commands/bank.js | 6 +-- src/commands/bounty.js | 8 ++-- src/commands/config.js | 78 ++++++++++++++++++------------------ src/commands/event.js | 4 +- src/commands/help.js | 8 ++-- src/commands/lookup.js | 4 +- src/commands/purchase-uav.js | 4 +- src/commands/server.js | 20 ++++----- 10 files changed, 91 insertions(+), 91 deletions(-) diff --git a/src/commands/admin.js b/src/commands/admin.js index 7aecee4..7f24cbf 100644 --- a/src/commands/admin.js +++ b/src/commands/admin.js @@ -19,7 +19,7 @@ module.exports = { name: "gamertag-link", description: "Link a gamertag for a user", value: "gamertag-link", - type: ApplicationCommandOptionType.SubCommand, + type: ApplicationCommandOptionType.Subcommand, options: [{ name: "user", description: "User to link gamertag to", @@ -38,7 +38,7 @@ module.exports = { name: "gamertag-unlink", description: "Unlink a gamertag for a user", value: "gamertag-unlink", - type: ApplicationCommandOptionType.SubCommand, + type: ApplicationCommandOptionType.Subcommand, options: [{ name: "user", description: "User to link gamertag to", @@ -50,7 +50,7 @@ module.exports = { name: "claim-armband", description: "Claim an armband for a faction", value: "claim-armband", - type: ApplicationCommandOptionType.SubCommand, + type: ApplicationCommandOptionType.Subcommand, options: [{ name: "faction_role", description: "Claim an armband for this faction role.", @@ -62,7 +62,7 @@ module.exports = { name: "bounty-clear", description: "Clear a bounty off a player", value: "bounty-clear", - type: ApplicationCommandOptionType.SubCommand, + type: ApplicationCommandOptionType.Subcommand, options: [{ name: "gamertag", description: "Gamertag of player", @@ -75,17 +75,17 @@ module.exports = { name: "money", description: "Add/Remove money to a user", value: "money", - type: ApplicationCommandOptionType.SubCommandGroup, + type: ApplicationCommandOptionType.SubcommandGroup, options: [{ name: "add", description: "Add money to user", value: "add", - type: ApplicationCommandOptionType.SubCommand, + type: ApplicationCommandOptionType.Subcommand, options: [{ name: "amount", description: "The amount to add to balance", value: "amount", - type: ApplicationCommandOptionType.Float, + type: ApplicationCommandOptionType.Number, min_value: 0.01, required: true, }, { @@ -99,12 +99,12 @@ module.exports = { name: "remove", description: "Remove money from a user", value: "remove", - type: ApplicationCommandOptionType.SubCommand, + type: ApplicationCommandOptionType.Subcommand, options: [{ name: "amount", description: "The amount to remove from balance", value: "amount", - type: ApplicationCommandOptionType.Float, + type: ApplicationCommandOptionType.Number, min_value: 0.01, required: true, }, { diff --git a/src/commands/alarm.js b/src/commands/alarm.js index db700fe..90ff882 100644 --- a/src/commands/alarm.js +++ b/src/commands/alarm.js @@ -40,13 +40,13 @@ module.exports = { name: "create", description: "Create a new Zone Ping Alarm", value: "create", - type: ApplicationCommandOptionType.SubCommand, + type: ApplicationCommandOptionType.Subcommand, options: [ { name: "x-coord", description: "X Coordinate of the origin", value: "x-coord", - type: ApplicationCommandOptionType.Float, + type: ApplicationCommandOptionType.Number, min_value: 0.01, required: true, }, @@ -54,7 +54,7 @@ module.exports = { name: "y-coord", description: "Y Coordinate of the origin", value: "y-coord", - type: ApplicationCommandOptionType.Float, + type: ApplicationCommandOptionType.Number, min_value: 0.01, required: true, }, @@ -62,7 +62,7 @@ module.exports = { name: "radius", description: "Radius of Alarm", value: "radius", - type: ApplicationCommandOptionType.Float, + type: ApplicationCommandOptionType.Number, min_value: 25.00, required: true, }, @@ -108,13 +108,13 @@ module.exports = { name: "delete", description: "Delete an Alarm", value: "delete", - type: ApplicationCommandOptionType.SubCommand, + type: ApplicationCommandOptionType.Subcommand, }, { name: "add-player", description: "Add player to be ignored list of an Alarm", value: "add-player", - type: ApplicationCommandOptionType.SubCommand, + type: ApplicationCommandOptionType.Subcommand, options: [{ name: "gamertag", description: "Gamertag of player to ignore", @@ -127,7 +127,7 @@ module.exports = { name: "remove-player", description: "Remove a player from the ignored list of an Alarm", value: "remove-player", - type: ApplicationCommandOptionType.SubCommand, + type: ApplicationCommandOptionType.Subcommand, options: [{ name: "gamertag", description: "Gamertag of player to ignore", @@ -140,19 +140,19 @@ module.exports = { name: "disable", description: "Disable an Alarm", value: "disable", - type: ApplicationCommandOptionType.SubCommand, + type: ApplicationCommandOptionType.Subcommand, }, { name: "enable", description: "Enable an Alarm", value: "enable", - type: ApplicationCommandOptionType.SubCommand, + type: ApplicationCommandOptionType.Subcommand, }, { name: "mute", description: "Mute the role ping of an Alarm", value: "mute", - type: ApplicationCommandOptionType.SubCommand, + type: ApplicationCommandOptionType.Subcommand, options: [{ name: "toggle", description: "Turn on/off role pings for this alarm", @@ -165,7 +165,7 @@ module.exports = { name: "set-rule", description: "Add a Rule to an Alarm", value: "set-rule", - type: ApplicationCommandOptionType.SubCommand, + type: ApplicationCommandOptionType.Subcommand, options: [{ name: "rule", description: "Select a rule to add to an Alarm", @@ -183,13 +183,13 @@ module.exports = { name: "remove-rule", description: "Remove a rule from an Alarm", value: "remove-rule", - type: ApplicationCommandOptionType.SubCommand, + type: ApplicationCommandOptionType.Subcommand, }, { name: "rename", description: "Rename an Alarm", value: "rename", - type: ApplicationCommandOptionType.SubCommand, + type: ApplicationCommandOptionType.Subcommand, options: [{ name: "name", description: "New Alarm Name", @@ -202,12 +202,12 @@ module.exports = { name: "move-origin", description: "Move the origin of an Alarm", value: "move-origin", - type: ApplicationCommandOptionType.SubCommand, + type: ApplicationCommandOptionType.Subcommand, options: [{ name: "x-coord", description: "X Coordinate of the new origin", value: "x-coord", - type: ApplicationCommandOptionType.Float, + type: ApplicationCommandOptionType.Number, min_value: 0.01, required: true, }, @@ -215,7 +215,7 @@ module.exports = { name: "y-coord", description: "Y Coordinate of the new origin", value: "y-coord", - type: ApplicationCommandOptionType.Float, + type: ApplicationCommandOptionType.Number, min_value: 0.01, required: true, }] diff --git a/src/commands/bank.js b/src/commands/bank.js index 0b74e0b..28ceec1 100644 --- a/src/commands/bank.js +++ b/src/commands/bank.js @@ -17,7 +17,7 @@ module.exports = { name: "balance", description: "View your bank balance", value: "balance", - type: ApplicationCommandOptionType.SubCommand, + type: ApplicationCommandOptionType.Subcommand, options: [{ name: "user", description: "User to view ballance", @@ -30,7 +30,7 @@ module.exports = { name: "transfer", description: "Transfer money to another user", value: "transfer", - type: ApplicationCommandOptionType.SubCommand, + type: ApplicationCommandOptionType.Subcommand, options: [ { name: "user", @@ -43,7 +43,7 @@ module.exports = { name: "amount", description: "The amount to transfer", value: "amount", - type: ApplicationCommandOptionType.Float, + type: ApplicationCommandOptionType.Number, min_value: 0.01, required: true, }, diff --git a/src/commands/bounty.js b/src/commands/bounty.js index 098fd5f..115574d 100644 --- a/src/commands/bounty.js +++ b/src/commands/bounty.js @@ -17,7 +17,7 @@ module.exports = { name: "set", description: "Set a bounty on a player", value: "set", - type: ApplicationCommandOptionType.SubCommand, + type: ApplicationCommandOptionType.Subcommand, options: [{ name: "gamertag", description: "Gamertag of player for bounty", @@ -28,7 +28,7 @@ module.exports = { name: "value", description: "Amount of the bounty", value: "value", - type: ApplicationCommandOptionType.Float, + type: ApplicationCommandOptionType.Number, min_value: 0.01, required: true }, { @@ -42,12 +42,12 @@ module.exports = { name: "pay", description: "Pay off your bounty", value: "pay", - type: ApplicationCommandOptionType.SubCommand, + type: ApplicationCommandOptionType.Subcommand, }, { name: "view", description: "View all active bounties", value: "view", - type: ApplicationCommandOptionType.SubCommand, + type: ApplicationCommandOptionType.Subcommand, }], SlashCommand: { /** diff --git a/src/commands/config.js b/src/commands/config.js index f9212a1..3388647 100644 --- a/src/commands/config.js +++ b/src/commands/config.js @@ -18,13 +18,13 @@ module.exports = { name: "killfeed", description: "Configure the killfeed", value: "killfeed", - type: ApplicationCommandOptionType.SubCommandGroup, + type: ApplicationCommandOptionType.SubcommandGroup, options: [ { name: "channel", description: "Configure the killfeed channel", value: "channel", - type: ApplicationCommandOptionType.SubCommand, + type: ApplicationCommandOptionType.Subcommand, options: [{ name: "channel", description: "The channel to configure", @@ -37,7 +37,7 @@ module.exports = { name: "show_coords", description: "Show the coordinates of the victim in the killfeed channel.", value: "show_coords", - type: ApplicationCommandOptionType.SubCommand, + type: ApplicationCommandOptionType.Subcommand, options: [{ name: "configuration", description: "True or False", @@ -50,7 +50,7 @@ module.exports = { name: "show_weapon", description: "Show the image of the weapon in the killfeed", value: "show_weapon", - type: ApplicationCommandOptionType.SubCommand, + type: ApplicationCommandOptionType.Subcommand, options: [{ name: "configuration", description: "True or False", @@ -65,13 +65,13 @@ module.exports = { name: "allowed_channels", description: "Set channels you're allowed to use the bot in", value: "allowed_channels", - type: ApplicationCommandOptionType.SubCommandGroup, + type: ApplicationCommandOptionType.SubcommandGroup, options: [ { name: "add", description: "Add channel", value: "add", - type: ApplicationCommandOptionType.SubCommand, + type: ApplicationCommandOptionType.Subcommand, options: [{ name: "channel", description: "The channel to configure", @@ -85,7 +85,7 @@ module.exports = { name: "remove", description: "Remove channel", value: "remove", - type: ApplicationCommandOptionType.SubCommand, + type: ApplicationCommandOptionType.Subcommand, options: [{ name: "channel", description: "The channel to configure", @@ -99,13 +99,13 @@ module.exports = { name: "clear", description: "Clears all configured channels", value: "clear", - type: ApplicationCommandOptionType.SubCommand, + type: ApplicationCommandOptionType.Subcommand, }, { name: "view", description: "View configured allowed channels", value: "view", - type: ApplicationCommandOptionType.SubCommand, + type: ApplicationCommandOptionType.Subcommand, } ] }, @@ -113,7 +113,7 @@ module.exports = { name: "set_channel", description: "Configure a channel", value: "set_channel", - type: ApplicationCommandOptionType.SubCommand, + type: ApplicationCommandOptionType.Subcommand, options: [ { name: "channel_type", @@ -140,7 +140,7 @@ module.exports = { name: "linked_gt_role", description: "Role for users with linked gamertags", value: "linked_gt_role", - type: ApplicationCommandOptionType.SubCommand, + type: ApplicationCommandOptionType.Subcommand, options: [{ name: "role", description: "Role to configure", @@ -153,7 +153,7 @@ module.exports = { name: "member_role", description: "Role for users who join the server", value: "member_role", - type: ApplicationCommandOptionType.SubCommand, + type: ApplicationCommandOptionType.Subcommand, options: [{ name: "role", description: "Role to configure", @@ -166,13 +166,13 @@ module.exports = { name: "bot_admin_role", description: "Set/remove bot admin role", value: "bot_admin_role", - type: ApplicationCommandOptionType.SubCommandGroup, + type: ApplicationCommandOptionType.SubcommandGroup, options: [ { name: "add", description: "Configure role to be bot admin", value: "add", - type: ApplicationCommandOptionType.SubCommand, + type: ApplicationCommandOptionType.Subcommand, options: [{ name: "role", description: "Role to confiure", @@ -185,7 +185,7 @@ module.exports = { name: "remove", description: "Remove configured role as bot admin", value: "remove", - type: ApplicationCommandOptionType.SubCommand, + type: ApplicationCommandOptionType.Subcommand, options: [{ name: "role", description: "Role to remove", @@ -198,7 +198,7 @@ module.exports = { name: "view", description: "View the configured bot admin roles", value: "view", - type: ApplicationCommandOptionType.SubCommand, + type: ApplicationCommandOptionType.Subcommand, } ] }, @@ -206,7 +206,7 @@ module.exports = { name: "admin_ping_role", description: "Admin role to ping in admin logs channel", value: "admin_ping_role", - type: ApplicationCommandOptionType.SubCommand, + type: ApplicationCommandOptionType.Subcommand, options: [{ name: "role", description: "Role to configure", @@ -219,13 +219,13 @@ module.exports = { name: "exclude", description: "Exclude roles that can be used to claim armbands", value: "exclude", - type: ApplicationCommandOptionType.SubCommandGroup, + type: ApplicationCommandOptionType.SubcommandGroup, options: [ { name: "add", description: "Configure role to be excluded", value: "add", - type: ApplicationCommandOptionType.SubCommand, + type: ApplicationCommandOptionType.Subcommand, options: [{ name: "role", description: "Role to confiure", @@ -238,7 +238,7 @@ module.exports = { name: "remove", description: "Remove configured role thats excluded", value: "remove", - type: ApplicationCommandOptionType.SubCommand, + type: ApplicationCommandOptionType.Subcommand, options: [{ name: "role", description: "Role to remove", @@ -251,7 +251,7 @@ module.exports = { name: "view", description: "View the configured excluded roles", value: "view", - type: ApplicationCommandOptionType.SubCommand, + type: ApplicationCommandOptionType.Subcommand, }, ] }, @@ -259,24 +259,24 @@ module.exports = { name: "reset", description: "Restore all settings to default configurations", value: "reset", - type: ApplicationCommandOptionType.SubCommand, + type: ApplicationCommandOptionType.Subcommand, }, { name: "view", description: "View current settings configuration", value: "view", - type: ApplicationCommandOptionType.SubCommand, + type: ApplicationCommandOptionType.Subcommand, }, { name: "starting_balance", description: "Set the starting balance of a new user", value: "starting_balance", - type: ApplicationCommandOptionType.SubCommand, + type: ApplicationCommandOptionType.Subcommand, options: [{ name: "amount", description: "The amount to set the starting balance", value: "amount", - type: ApplicationCommandOptionType.Float, + type: ApplicationCommandOptionType.Number, min_value: 1.00, required: true, }] @@ -285,12 +285,12 @@ module.exports = { name: "uav-price", description: "Configure the price of a UAV", value: "uav-price", - type: ApplicationCommandOptionType.SubCommand, + type: ApplicationCommandOptionType.Subcommand, options: [{ name: "amount", description: "The amount to set the UAV price", value: "amount", - type: ApplicationCommandOptionType.Float, + type: ApplicationCommandOptionType.Number, min_value: 0.01, required: true, }] @@ -299,12 +299,12 @@ module.exports = { name: "emp-price", description: "Configure the price of an EMP", value: "emp-price", - type: ApplicationCommandOptionType.SubCommand, + type: ApplicationCommandOptionType.Subcommand, options: [{ name: "amount", description: "The amount to set the EMP price", value: "amount", - type: ApplicationCommandOptionType.Float, + type: ApplicationCommandOptionType.Number, min_value: 0.01, required: true, }] @@ -313,13 +313,13 @@ module.exports = { name: "income_role", description: "Set/remove roles to recieve income", value: "set_income_role", - type: ApplicationCommandOptionType.SubCommandGroup, + type: ApplicationCommandOptionType.SubcommandGroup, options: [ { name: "set", description: "Set role", value: "set", - type: ApplicationCommandOptionType.SubCommand, + type: ApplicationCommandOptionType.Subcommand, options: [ { name: "role", @@ -332,7 +332,7 @@ module.exports = { name: "amount", description: "The amount to collect", value: 120.00, - type: ApplicationCommandOptionType.Float, + type: ApplicationCommandOptionType.Number, min_value: 0.01, required: true, } @@ -342,7 +342,7 @@ module.exports = { name: "remove", description: "Remove role", value: "remove", - type: ApplicationCommandOptionType.SubCommand, + type: ApplicationCommandOptionType.Subcommand, options: [{ name: "role", description: "Role to remove", @@ -357,12 +357,12 @@ module.exports = { name: "income_limiter", description: "Change the number of hours to wait before collecting next income", value: "income_limiter", - type: ApplicationCommandOptionType.SubCommand, + type: ApplicationCommandOptionType.Subcommand, options: [{ name: "hours", description: "Number of hours till income can be collected", value: 168.00, // 1 week - type: ApplicationCommandOptionType.Float, + type: ApplicationCommandOptionType.Number, min_value: 1.00, required: true, }] @@ -371,7 +371,7 @@ module.exports = { name: "combat-log-timer", description: "Adjust number of minutes to detect combat logs (0 disables combat log)", value: "combat-log-timer", - type: ApplicationCommandOptionType.SubCommand, + type: ApplicationCommandOptionType.Subcommand, options: [{ name: "minutes", description: "Minutes to qualify combat log", @@ -384,7 +384,7 @@ module.exports = { name: "toggle-uav-purchase", description: "Allow/Disallow UAV purchases", value: "toggle-uav-purchase", - type: ApplicationCommandOptionType.SubCommand, + type: ApplicationCommandOptionType.Subcommand, options: [{ name: "configuration", description: "True or False", @@ -397,7 +397,7 @@ module.exports = { name: "toggle-emp-purchase", description: "Allow/Disallow EMP purchases", value: "toggle-uav-purchase", - type: ApplicationCommandOptionType.SubCommand, + type: ApplicationCommandOptionType.Subcommand, options: [{ name: "configuration", description: "True or False", @@ -410,7 +410,7 @@ module.exports = { name: "welcome_message_server_name", description: "Configure the server name in the welcome message", value: "welcome_message_server_name", - type: ApplicationCommandOptionType.SubCommand, + type: ApplicationCommandOptionType.Subcommand, options: [{ name: "name", description: "Server name to include in welcome message", diff --git a/src/commands/event.js b/src/commands/event.js index eb02008..a1c581f 100644 --- a/src/commands/event.js +++ b/src/commands/event.js @@ -16,7 +16,7 @@ module.exports = { name: "player-track", description: "Track a player and announce location", value: "player-track", - type: ApplicationCommandOptionType.SubCommand, + type: ApplicationCommandOptionType.Subcommand, options: [{ name: "gamertag", description: "Gamertag of player", @@ -60,7 +60,7 @@ module.exports = { name: "delete", description: "Delete an active event", value: "delete", - type: ApplicationCommandOptionType.SubCommand + type: ApplicationCommandOptionType.Subcommand }], SlashCommand: { /** diff --git a/src/commands/help.js b/src/commands/help.js index 8d39915..194b79a 100644 --- a/src/commands/help.js +++ b/src/commands/help.js @@ -17,7 +17,7 @@ module.exports = { name: "commands", description: "List all commands", value: "commands", - type: ApplicationCommandOptionType.SubCommand, + type: ApplicationCommandOptionType.Subcommand, options: [{ name: "command", description: "Get information on a specific command", @@ -30,19 +30,19 @@ module.exports = { name: "support", description: "Get support for Application", value: "support", - type: ApplicationCommandOptionType.SubCommand, + type: ApplicationCommandOptionType.Subcommand, }, { name: "credits", description: "DayZ.R Bot Credits", value: "credits", - type: ApplicationCommandOptionType.SubCommand, + type: ApplicationCommandOptionType.Subcommand, }, { name: "stats", description: "Current Bot Statistics", value: "stats", - type: ApplicationCommandOptionType.SubCommand, + type: ApplicationCommandOptionType.Subcommand, } ], SlashCommand: { diff --git a/src/commands/lookup.js b/src/commands/lookup.js index f79f21d..4da0f07 100644 --- a/src/commands/lookup.js +++ b/src/commands/lookup.js @@ -15,7 +15,7 @@ module.exports = { name: "discord", description: "Find a Discord user from a Gamertag", value: "discord", - type: ApplicationCommandOptionType.SubCommand, + type: ApplicationCommandOptionType.Subcommand, options: [{ name: "gamertag", description: "Gamertag of player", @@ -27,7 +27,7 @@ module.exports = { name: "gamertag", description: "Find a Gamertag from a Discord user", value: "gamertag", - type: ApplicationCommandOptionType.SubCommand, + type: ApplicationCommandOptionType.Subcommand, options: [{ name: "user", description: "Discord User", diff --git a/src/commands/purchase-uav.js b/src/commands/purchase-uav.js index bb5c17f..344c53c 100644 --- a/src/commands/purchase-uav.js +++ b/src/commands/purchase-uav.js @@ -17,7 +17,7 @@ module.exports = { name: "x-coord", description: "X Coordinate of the origin", value: "x-coord", - type: ApplicationCommandOptionType.Float, + type: ApplicationCommandOptionType.Number, min_value: 0.01, required: true, }, @@ -25,7 +25,7 @@ module.exports = { name: "y-coord", description: "Y Coordinate of the origin", value: "y-coord", - type: ApplicationCommandOptionType.Float, + type: ApplicationCommandOptionType.Number, min_value: 0.01, required: true, }, diff --git a/src/commands/server.js b/src/commands/server.js index d42e774..614b3b7 100644 --- a/src/commands/server.js +++ b/src/commands/server.js @@ -18,31 +18,31 @@ module.exports = { name: "initialize", description: "Connect your Nitrado server to the bot", value: "initialize", - type: ApplicationCommandOptionType.SubCommand, + type: ApplicationCommandOptionType.Subcommand, }, { name: "disconnect", description: "Delete your Nitrado server from the bot database", value: "disconnect", - type: ApplicationCommandOptionType.SubCommand, + type: ApplicationCommandOptionType.Subcommand, }, { name: "credentials-status", description: "Check the status of your Nitrado Credentials", value: "credentials-status", - type: ApplicationCommandOptionType.SubCommand, + type: ApplicationCommandOptionType.Subcommand, }, { name: "retry-credentials", description: "If your credentials are marked as FAILED, try retreiving Nitrado logs again.", value: "retry-credentials", - type: ApplicationCommandOptionType.SubCommand, + type: ApplicationCommandOptionType.Subcommand, }, { name: "ban-player", description: "Ban a player from the DayZ server", value: "ban-player", - type: ApplicationCommandOptionType.SubCommand, + type: ApplicationCommandOptionType.Subcommand, options: [{ name: "gamertag", description: "gamertag of the player to ban.", @@ -54,7 +54,7 @@ module.exports = { name: "unban-player", description: "Unban a player from the DayZ server", value: "unban-player", - type: ApplicationCommandOptionType.SubCommand, + type: ApplicationCommandOptionType.Subcommand, options: [{ name: "gamertag", description: "gamertag of the player to unban.", @@ -67,17 +67,17 @@ module.exports = { name: "restart", description: "Restart the DayZ Server", value: "restart", - type: ApplicationCommandOptionType.SubCommand, + type: ApplicationCommandOptionType.Subcommand, }, { name: "auto-restart", description: "Enable/Disable periodic server checks and restart if stopped", value: "auto-restart", - type: ApplicationCommandOptionType.SubCommand, + type: ApplicationCommandOptionType.Subcommand, }, { name: "disable-base-damage", description: "Disable/Enable base damage", value: "disable-base-damage", - type: ApplicationCommandOptionType.SubCommand, + type: ApplicationCommandOptionType.Subcommand, options: [{ name: "preference", description: "DisableBaseDamage Preference", @@ -89,7 +89,7 @@ module.exports = { name: "disable-container-damage", description: "Disable/Enable container damage", value: "disable-container-damage", - type: ApplicationCommandOptionType.SubCommand, + type: ApplicationCommandOptionType.Subcommand, options: [{ name: "preference", description: "disableContainerDamage Preference", From 24fa1ead3be8dcf9fd0d49db4897fd16362277bb Mon Sep 17 00:00:00 2001 From: SowinskiBraeden Date: Tue, 30 Sep 2025 12:18:22 -0700 Subject: [PATCH 3/6] refactor/clean imports --- src/DayZRBot.ts | 72 ++++++++++++++++++++++++------------------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/src/DayZRBot.ts b/src/DayZRBot.ts index 635999d..5439556 100644 --- a/src/DayZRBot.ts +++ b/src/DayZRBot.ts @@ -1,38 +1,39 @@ -// Discord.js & REST API -import { - Collection, - Client, - EmbedBuilder, - Routes, - InteractionResponseType, - InteractionType, - GatewayDispatchEvents, - ClientOptions, - Guild, - ChatInputCommandInteraction, - Snowflake, - Role, - Message, - TextBasedChannel, - ButtonInteraction, - StringSelectMenuInteraction, - Channel, -} from "discord.js"; -import { REST } from "@discordjs/rest"; - -// MongoDB -import { MongoClient } from "mongodb"; - // Node.js built-ins -import * as path from "path"; -import * as fs from "node:fs"; -import * as readline from "node:readline"; import * as crypto from "crypto"; +import * as fs from "node:fs"; +import * as path from "path"; +import * as readline from "node:readline"; import { Readable } from "node:stream"; -// Custom utilities -import Logger from "./util/Logger"; -import { RegisterGlobalCommands, RegisterGuildCommands } from "./util/RegisterSlashCommands"; +// Third-party libraries +import { + ButtonInteraction, + Channel, + ChatInputCommandInteraction, + Client, + ClientOptions, + Collection, + EmbedBuilder, + GatewayDispatchEvents, + Guild, + InteractionResponseType, + InteractionType, + Message, + Role, + Routes, + Snowflake, + StringSelectMenuInteraction, + TextBasedChannel, +} from "discord.js"; +import { REST } from "@discordjs/rest"; +import { MongoClient } from "mongodb"; + +// Config +import { Colors, Config } from "./config/config"; + +// Utilities +import Logger from "./util/Logger"; +import { decrypt } from "./util/Cryptic"; import { DownloadNitradoFile, CheckServerStatus, @@ -40,17 +41,16 @@ import { PostServerSettings, NitradoCredentialStatus, } from "./util/NitradoAPI"; +import { RegisterGlobalCommands, RegisterGuildCommands } from "./util/RegisterSlashCommands"; import { HandlePlayerLogs, HandleActivePlayersList } from "./util/LogsHandler"; import { HandleKillfeed, UpdateLastDeathDate } from "./util/KillfeedHandler"; import { HandleExpiredUAVs, HandleEvents, PlaceFireplaceInAlarm } from "./util/AlarmsHandler"; -import { decrypt } from "./util/Cryptic"; import { GetWebhook, WebhookSend } from "./util/WebhookHandler"; -import { Config, Colors } from "./config/config"; -// Database structures -import { getDefaultPlayer, UpdatePlayer, Player } from "./database/player"; +// Database +import { Player, UpdatePlayer, getDefaultPlayer } from "./database/player"; import { Missions, MissionKey } from "./database/destinations"; -import { GetGuild, GuildConfig, NitradoConfig } from "./database/guild"; +import { GuildConfig, NitradoConfig, GetGuild } from "./database/guild"; const MINUTE = 60000; // 1 minute in milliseconds const AUTO_RESTART_INTERVAL = 600000; // Set auto-restart interval 10 minutes (600,000ms) From f7ff7898099d0a96e6993594e00f1e2614c2533e Mon Sep 17 00:00:00 2001 From: SowinskiBraeden Date: Tue, 30 Sep 2025 13:32:29 -0700 Subject: [PATCH 4/6] refactor/doc comments + types where possible --- src/DayZRBot.ts | 541 ++++++++++++++++++++++++++++-------------- src/database/guild.ts | 9 +- 2 files changed, 376 insertions(+), 174 deletions(-) diff --git a/src/DayZRBot.ts b/src/DayZRBot.ts index 5439556..22e3888 100644 --- a/src/DayZRBot.ts +++ b/src/DayZRBot.ts @@ -50,24 +50,27 @@ import { GetWebhook, WebhookSend } from "./util/WebhookHandler"; // Database import { Player, UpdatePlayer, getDefaultPlayer } from "./database/player"; import { Missions, MissionKey } from "./database/destinations"; -import { GuildConfig, NitradoConfig, GetGuild } from "./database/guild"; +import { GuildConfig, NitradoConfig, GetGuild, NitradoCredentials, IntegerBoolean, GuildConfigDB } from "./database/guild"; -const MINUTE = 60000; // 1 minute in milliseconds +const MINUTE = 60000; // 1 minute in milliseconds const AUTO_RESTART_INTERVAL = 600000; // Set auto-restart interval 10 minutes (600,000ms) const STARTING_PLAYERS_TICK = 11; // TODO: move this interface to LogsHandler when converted to ts -interface PlayerSession { +interface PlayerSession +{ startTime: Date; - endTime: Date | null; + endTime: Date | null; }; // TODO: put this somewhere else -interface CommandOptions { +interface CommandOptions +{ }; -interface InteractionHandler { +interface InteractionHandler +{ run: ( client: DayZR, interaction: ChatInputCommandInteraction, @@ -75,31 +78,48 @@ interface InteractionHandler { ) => Promise; } -interface Command { - name: string; - debug: boolean; - global: boolean; +interface Command +{ + name: string; + debug: boolean; + global: boolean; description: string; - usage: string; - permissions: { + usage: string; + permissions: + { channel: Array; - member: Array; + member: Array; }; - options: Array; - SlashCommand: { + options: Array; + SlashCommand: + { run: ( - client: DayZR, + client: DayZR, interaction: ChatInputCommandInteraction, - args: any, // Unsure of what this should be, come back to later - GuildDB: GuildConfig, - start?: number + args: any, // Unsure of what this should be, come back to later + GuildDB: GuildConfig, + start?: number ) => Promise; }; - Interactions?: { + Interactions?: + { [InteractoinName: string]: InteractionHandler; } } +/** + * DayZR extends discord.js Client and contains many + * important methods that handles concurrent log + * checking, handling logs, calling utility methods + * to handle logs, update database, send queued alarms, + * clean claimed armbands per guild, store player sessions, + * commands, handlers, interaction handlers, keeping track + * of where logs have been scanned, server automatic restarts, + * etc. + * + * @author Braeden Sowinski + * @version 14.0.0 + */ export default class DayZR extends Client { @@ -126,7 +146,17 @@ export default class DayZR extends Client private activePlayersTick: number; private timer: number; - constructor(options: ClientOptions, config: Config) + /** + * DayZR constructor to create discord Client + * and initialize maps, determine which guilds + * have valid Nitrado credentials, setup + * concurrent log checking for guilds with valid + * Nitrado credentials. + * + * @param options for discord.js Client + * @param config general config for DayZR bot + */ + public constructor(options: ClientOptions, config: Config) { super(options); @@ -180,6 +210,7 @@ export default class DayZR extends Client this.Ready = false; this.activePlayersTick = STARTING_PLAYERS_TICK; + // Handle Interactions via websocket this.ws.on( GatewayDispatchEvents.InteractionCreate, async (interaction: ChatInputCommandInteraction) => @@ -187,126 +218,149 @@ export default class DayZR extends Client const start: number = new Date().getTime(); - if (interaction.type == InteractionType.ApplicationCommand) + if (interaction.type != InteractionType.ApplicationCommand) return + if (!interaction.guildId) return; + + let GuildDB: GuildConfig = await GetGuild(this, interaction.guildId); + + if (this.exists(GuildDB.Nitrado) && this.exists(GuildDB.Nitrado.Auth)) { - if (!interaction.guildId) return; + GuildDB.Nitrado.Auth = decrypt( + GuildDB.Nitrado.Auth, + this.config.EncryptionMethod, + this.key, + this.encryptionIV + ); + } - let GuildDB: GuildConfig = await GetGuild(this, interaction.guildId); + const command: string = interaction.commandName; + const args: any = interaction.options; - if (this.exists(GuildDB.Nitrado) && this.exists(GuildDB.Nitrado.Auth)) + // TODO: maybe move this to another method + // Free unused armbands for related commands + if (["armbands", "claim", "factions"].includes(command)) + { + + for (const [factionID, data] of Object.entries(GuildDB.factionArmbands)) { - GuildDB.Nitrado.Auth = decrypt( - GuildDB.Nitrado.Auth, - this.config.EncryptionMethod, - this.key, - this.encryptionIV - ); - } - const command: string = interaction.commandName; - const args: any = interaction.options; + const guild: Guild | undefined = this.guilds.cache.get(GuildDB.serverID); - // Free unused armbands for related commands - if (["armbands", "claim", "factions"].includes(command)) - { - - for (const [factionID, data] of Object.entries(GuildDB.factionArmbands)) + if (!guild) continue; + + const role: Role | undefined = guild.roles.cache.find(role => role.id == factionID); + + if (!role) { - - const guild: Guild | undefined = this.guilds.cache.get(GuildDB.serverID); - - if (!guild) continue; - - const role: Role | undefined = guild.roles.cache.find(role => role.id == factionID); - - if (!role) - { - let query = { - $pull: { "server.usedArmbands": data.armband }, - $unset: { [`server.factionArmbands.${factionID}`]: "" }, - }; - - this.dbo.collection("guilds").updateOne( - { - "server.serverID": GuildDB.serverID - }, - query, - (err: string) => - { - if (err) return this.sendInternalError(interaction, err); - } - ); - } - } - } - - this.log(`Interaction [${interaction.guildId}] - ${command}`); - - const rest = new REST({ version: "10" }).setToken(this.config.Token); - - const normalizeMessage = (message: any) => - { - if (message?.embeds) { - message = { - ...message, - embeds: message.embeds.map((embed: EmbedBuilder | any) => - embed instanceof EmbedBuilder ? embed.toJSON() : embed - ), + let query = { + $pull: { "server.usedArmbands": data.armband }, + $unset: { [`server.factionArmbands.${factionID}`]: "" }, }; + + this.dbo.collection("guilds").updateOne( + { + "server.serverID": GuildDB.serverID + }, + query, + (err: string) => + { + if (err) return this.sendInternalError(interaction, err); + } + ); } - return message; - }; - - // Easy to send response so ;) - const handleCallback = async (interactionType: InteractionResponseType, message: any): Promise => - { - message = normalizeMessage(message); - - return await rest.post(Routes.interactionCallback(interaction.id, interaction.token), - { - body: { - type: interactionType, - data: message, - } - }); } + } - // Nicely name our custom callback functions and pass correct type because discord is picky with numbers... - interaction.send = async (message: Message): Promise => handleCallback(InteractionResponseType.ChannelMessageWithSource, message); - interaction.deferReply = async (message: Message): Promise => handleCallback(InteractionResponseType.DeferredChannelMessageWithSource, message); - interaction.showModal = async (message: Message): Promise => handleCallback(InteractionResponseType.Modal, message); + this.log(`Interaction [${interaction.guildId}] - ${command}`); - interaction.editReply = async (message: Message): Promise => - { - if (!this.application) return; - return await rest.patch(Routes.webhookMessage(this.application.id, interaction.token), { - body: message, - }); - }; + const rest: REST = new REST({ version: "10" }).setToken(this.config.Token); - if (!this.databaseConnected) - { - const dbFailedEmbed: EmbedBuilder = new EmbedBuilder() - .setDescription(`**Internal Error:**\nUh Oh D: Its not you, its me.\nThe bot has failed to connect to the database 5 times!\nContact the Developers\nhttps://discord.gg/YCXhvy9uZw`) - .setColor(Colors.Red) - - return interaction.send({ embeds: [dbFailedEmbed] }); + // Ensure embeds are properly formatted to send + const normalizeMessage = (message: any) => + { + if (message?.embeds) { + message = { + ...message, + embeds: message.embeds.map((embed: EmbedBuilder | any) => + embed instanceof EmbedBuilder ? embed.toJSON() : embed + ), + }; } + return message; + }; - let cmd: Command | undefined = this.commands.get(command); - if (!cmd) return; - try { - cmd.SlashCommand.run(this, interaction, args, GuildDB, start); // start is only used in ping / stats command - } catch (err: any) { - this.sendInternalError(interaction, err); - } + // Easy to send response so ;) + const handleCallback = async ( + interactionType: InteractionResponseType, + message: any + ): Promise => + { + message = normalizeMessage(message); + + return await rest.post(Routes.interactionCallback(interaction.id, interaction.token), + { + body: { + type: interactionType, + data: message, + } + }); + } + + // Nicely name our custom callback functions and pass correct type because discord is picky with numbers... + interaction.send = async (message: Message): Promise => handleCallback(InteractionResponseType.ChannelMessageWithSource, message); + interaction.deferReply = async (message: Message): Promise => handleCallback(InteractionResponseType.DeferredChannelMessageWithSource, message); + interaction.showModal = async (message: Message): Promise => handleCallback(InteractionResponseType.Modal, message); + + interaction.editReply = async (message: Message): Promise => + { + if (!this.application) return; + return await rest.patch(Routes.webhookMessage(this.application.id, interaction.token), { + body: message, + }); + }; + + if (!this.databaseConnected) + { + const dbFailedEmbed: EmbedBuilder = new EmbedBuilder() + .setDescription(`**Internal Error:**\nUh Oh D: Its not you, its me.\nThe bot has failed to connect to the database 5 times!\nContact the Developers\nhttps://discord.gg/YCXhvy9uZw`) + .setColor(Colors.Red) + + return interaction.send({ embeds: [dbFailedEmbed] }); + } + + let cmd: Command | undefined = this.commands.get(command); + if (!cmd) return; + + try + { + cmd.SlashCommand.run(this, interaction, args, GuildDB, start); // start is only used in ping / stats command + } + catch (err: any) + { + this.sendInternalError(interaction, err); } }); } + /** + * log sends given log messages to the logger + * to print to console and write to file. + * @param text to send to logger. + */ private log(text: string): void { this.logger.log(text); } + + /** + * error send a given error message to the logger + * to print to console and write to file. + * @param text to send to logger as error. + */ public error(text: string): void { this.logger.error(text); } + /** + * getDateEST gets proper Date from given nitrado EST time + * @param time in string format hh:mm:ss + * @returns Date object at the given time + */ public getDateEST(time: string): Date { // TODO: fix magic numbers <------------------------------------------------------------------------------------ @@ -318,6 +372,32 @@ export default class DayZR extends Client return new Date(f.getTime() + 4 * 3600000); // Add EST time offset to return timestamp in UTC } + /** + * readLogs of nitrado server from given guild configuration, + * ensures logs arent read twice. Checks contents of logs and + * send to appropriate handler in util/ e.g. HandlePlayerLog, + * HandleKillfeed, etc. + * + * After all logs are read, and handlers update db as needed, + * send any queued alarm pings to configured discord webhooks. + * Then clear alarm embed queue of given guild. + * + * After alarm queue embeds are handled, any players who are + * still connected, are checked against logs latest logs + * player list to ensure they are still connected. If players + * are no longer connected even while being marked as connected. + * Mark players as disconnected. + * + * Any players who have been determined to be still connected to + * server, update their calculated session times. Update players + * database for connected and previously connected to updated + * session stats. + * + * Update guild config lastLog read to ensure we continue from + * where we last left off in the logs when we call readLogs next. + * + * @param guild config to read Nitrado logs + */ private async readLogs(guild: GuildConfig): Promise { @@ -334,7 +414,7 @@ export default class DayZR extends Client const lastLog: string | null | undefined = this.logHistory.get(guild.Nitrado.ServerID); - let logIndex = -1; + let logIndex: number = -1; if (lastLog != null) { logIndex = lines.indexOf(lastLog); @@ -361,6 +441,7 @@ export default class DayZR extends Client } } + // Read logs for (let i = logIndex + 1; i < lines.length; i++) { // TODO: tidy this up its just bad looking @@ -391,11 +472,11 @@ export default class DayZR extends Client { queue.forEach(async (data, channel_id) => { - const channel = this.GetChannel(channel_id); + const channel: Channel | undefined = this.GetChannel(channel_id); if (!channel) return; - const NAME = "DayZ.R Zone Alert"; - const webhook = await GetWebhook(this, NAME, channel_id); + const NAME: string = "DayZ.R Zone Alert"; + const webhook: any = await GetWebhook(this, NAME, channel_id); data.forEach(async (embeds, role) => { @@ -425,8 +506,10 @@ export default class DayZR extends Client }); }); - this.alarmPingQueue.set(guild.serverID, new Map()); // Clear alarm queue for this guild + // Clear alarm queue for this guild + this.alarmPingQueue.set(guild.serverID, new Map()); + // Get all previously connected players from database. const playerTemplate = /(.*) \| Player \"(.*)\" \(id=(.*) pos=<(.*)>\)/g; let previouslyConnected = await this.dbo.collection("players").find( { @@ -437,8 +520,9 @@ export default class DayZR extends Client return players.filter(p => p.connected); }); - let lastDetectedTime; + let lastDetectedTime: Date; + // Get latest player list from logs for (let i = lines.length - 1; i > 0; i--) { @@ -448,7 +532,7 @@ export default class DayZR extends Client for (let j = i + 1; j < lines.length; j++) { - let line = lines[j]; + let line: string = lines[j]; if (line.includes("| ####")) break; let data = [...line.matchAll(playerTemplate)][0]; @@ -463,9 +547,9 @@ export default class DayZR extends Client // Skip this player if the player does not exist. if (!this.exists(info.player) || !this.exists(info.playerID)) continue; - lastDetectedTime = await this.getDateEST(info.time); + lastDetectedTime = this.getDateEST(info.time); - let playerStat = await this.dbo.collection("players").findOne({ "playerID": info.playerID }); + let playerStat: Player = await this.dbo.collection("players").findOne({ "playerID": info.playerID }); if (!this.exists(playerStat)) playerStat = getDefaultPlayer(info.player, info.playerID, guild.Nitrado.ServerID); // Skip this player if the lastDisconnectionDate time is later than the player log entry. @@ -480,7 +564,7 @@ export default class DayZR extends Client if (this.playerSessions.get(guild.Nitrado.ServerID)?.has(info.playerID)) { - // Player is already in a session, update the session"s end time. + // Player is already in a session, update the session's end time. let session: PlayerSession | undefined = this.playerSessions.get(guild.Nitrado.ServerID)?.get(info.playerID); if (!session) continue; @@ -491,14 +575,14 @@ export default class DayZR extends Client { // Player is not in a session, create a new session. - const newSession = { + const newSession: PlayerSession = { startTime: lastDetectedTime, endTime: null, // Initialize end time as null. }; this.playerSessions.get(guild.Nitrado.ServerID)?.set(info.playerID, newSession); - // Check if the player has been marked as connected before, but only if a session doesn"t exist + // Check if the player has been marked as connected before, but only if a session doesn't exist // in the map, indicating the connection was discovered in the logs during this session. if (!previouslyConnected.includes(playerStat)) { @@ -513,7 +597,8 @@ export default class DayZR extends Client } } - const lastLine = lines[lines.length - 1]; + // Update lastLog read to keep track of progress in logs for next log update + const lastLine: string = lines[lines.length - 1]; this.logHistory.set(guild.Nitrado.ServerID, lastLine); this.dbo.collection("guilds").updateOne( @@ -533,13 +618,30 @@ export default class DayZR extends Client ); } + /** + * logsUpdateTimer handles concurrent log checking and ensures + * correct log files are downloaded to read. + * + * Go over each guild, for each guild with valid Nitrado credentials, + * check DayZ mission from Nitrado server settings. Ensure guild config + * database is up to date for the mission of the server. + * + * Download the correct log file for nitrado server. + * + * After log files are downloaded, call readLogs to read the log file. + * Once the log files are done being read, call other utility methods + * to handle expired things such as UAVs, and events. + * @param c is the DayZR client, passed in from external source + * as this function is called externally, and sometimes + * "this." just doesnt work. + */ private async logsUpdateTimer(c: DayZR): Promise { c.activePlayersTick++; c.guilds.cache.forEach(async (guild) => { - let GuildDB = await GetGuild(c, guild.id); + let GuildDB: GuildConfig = await GetGuild(c, guild.id); /* Note to self: @@ -552,7 +654,7 @@ export default class DayZR extends Client // Continue if these credentials are marked as failed if (GuildDB.Nitrado.Status == NitradoCredentialStatus.FAILED) return; - const NitradoCred = { + const NitradoCred: NitradoCredentials = { ServerID: GuildDB.Nitrado.ServerID, UserID: GuildDB.Nitrado.UserID, Auth: decrypt( @@ -563,6 +665,7 @@ export default class DayZR extends Client ), }; + // TODO: can we type this? will have to read Nitrado API docs const response = await FetchServerSettings(NitradoCred, c, "logsUpdateTimer").then(res => res); // TODO: fix magic number @@ -616,16 +719,17 @@ export default class DayZR extends Client if (settings.game_specific.log_files.length == 0) return; // Get file with longest file name (target log file) - const filename = settings.game_specific.log_files.sort((a: string, b: string) => a.length - b.length)[0]; - const path = `${settings.game_specific.path.slice(0, -1)}${filename.split(settings.game)[1]}`; + const filename: string = settings.game_specific.log_files.sort((a: string, b: string) => a.length - b.length)[0]; + const path: string = `${settings.game_specific.path.slice(0, -1)}${filename.split(settings.game)[1]}`; // Ensure Player List is logged for next update - const playerListEnabled = parseInt(settings.settings.config.adminLogPlayerList) + const playerListEnabled: IntegerBoolean = parseInt(settings.settings.config.adminLogPlayerList) if (!playerListEnabled) PostServerSettings(NitradoCred, c, "config", "adminLogPlayerList", "1") await DownloadNitradoFile(NitradoCred, c, path, `./logs/${NitradoCred.ServerID}-logs.ADM`) .then(async (status) => { + // TODO: magic number if (status == 1) return c.error(`Failed to Download Nitrado Log Files - [${NitradoCred.ServerID}]`); await c.readLogs(GuildDB).then(async () => @@ -640,7 +744,18 @@ export default class DayZR extends Client }); } - async connectMongo(mongoURI: string, dbo: string): Promise + /** + * connectMongo connects to mongo database instance, retries on failure + * up to a maximum number of times. After failed database connection attempts, + * it marks databaseConnected as false, and allows bot to run. + * + * Any commands called will return error to end-user alerting them + * the bot has failed to connect to databsae and to contact administrator. + * + * @param mongoURI to connect to + * @param dbo name of database + */ + private async connectMongo(mongoURI: string, dbo: string): Promise { let failed: boolean = false; @@ -684,7 +799,7 @@ export default class DayZR extends Client this.log("Successfully connected to mongoDB"); databaselogs.connected = true; - databaselogs.attempts = 0; // reset attempts + databaselogs.attempts = 0; // reset attempts this.databaseConnected = true; } @@ -693,7 +808,7 @@ export default class DayZR extends Client databaselogs.attempts++; databaselogs.connected = false; - let db = mongoURI.includes("@") ? mongoURI.split("@")[1] : mongoURI.split("//")[1]; + let db: string = mongoURI.includes("@") ? mongoURI.split("@")[1] : mongoURI.split("//")[1]; db = db.includes("/") ? db.split("/")[0] : db; this.error(`Failed to connect to mongodb (mongodb://${db}/${dbo}): attempt ${databaselogs.attempts} - ${err}`); @@ -707,7 +822,25 @@ export default class DayZR extends Client if (failed) process.exit(-1); } - async initialize(): Promise + /** + * initialize is called once upon the bot being constructed. + * It attempts to connect the bot to the database, gets guilds + * from the database to initialize maps. + * + * Checks guilds configurations to see if they should be added + * to auto-restart map. + * + * Adds each guild with valid Nitrado config to a lastLogs map + * to keep a local copy of where logs have been read last. + * + * Adds each guild with valid Nitrado config to player sessions + * map to keep track of active players per guilds nitrado server. + * + * Adds each guild with valid Nitrado config to alarm queue map + * to keep track of which guilds have embeds to send to select + * channels. + */ + private async initialize(): Promise { // Wait for MongoDB to connect @@ -715,7 +848,7 @@ export default class DayZR extends Client if (!this.databaseConnected) return; - let guilds = await this.dbo.collection("guilds").find({}).toArray(); + let guilds: Array = await this.dbo.collection("guilds").find({}).toArray(); /* Initialize auto restart for enabled servers @@ -724,15 +857,16 @@ export default class DayZR extends Client */ for (let i = 0; i < guilds.length; i++) { + // // Ignore guilds with no Nitrado configuration if (!this.exists(guilds[i].Nitrado)) continue; if (guilds[i].server.autoRestart) { - const NitradoCred = { - ServerID: guilds[i].Nitrado.ServerID, - UserID: guilds[i].Nitrado.UserID, + const NitradoCred: NitradoCredentials = { + ServerID: guilds[i].Nitrado!.ServerID, + UserID: guilds[i].Nitrado!.UserID, Auth: decrypt( - guilds[i].Nitrado.Auth, + guilds[i].Nitrado!.Auth, this.config.EncryptionMethod, this.key, this.encryptionIV @@ -740,20 +874,28 @@ export default class DayZR extends Client }; this.arIntervalIds.set(guilds[i].server.serverID, setInterval(CheckServerStatus, this.arInterval, NitradoCred, this)) } - this.logHistory.set(guilds[i].Nitrado.ServerID, guilds[i].server.lastLog); // Using Nitrado Server ID over guild ID in case of future support for multiple nitrado servers in a single guild - this.playerSessions.set(guilds[i].Nitrado.ServerID, new Map()); // Same reason here as named above. + this.logHistory.set(guilds[i].Nitrado!.ServerID, guilds[i].server.lastLog); // Using Nitrado Server ID over guild ID in case of future support for multiple nitrado servers in a single guild + this.playerSessions.set(guilds[i].Nitrado!.ServerID, new Map()); // Same reason here as named above. this.alarmPingQueue.set(guilds[i].server.serverID, new Map()); // Initialize alarm queue to be empty this.playerListMsgIds.set(guilds[i].server.serverID, ""); // Initialize player list message ids - this.log(`[${guilds[i].server.serverID}] Initialized existing Nitrado Server: (${guilds[i].Nitrado.ServerID})`); + this.log(`[${guilds[i].server.serverID}] Initialized existing Nitrado Server: (${guilds[i].Nitrado!.ServerID})`); } } - async initNewNitradoServer( + /** + * initNewNitradoServer takes required Nitrado credentials + * such as the unique server ID to add guild to required maps + * such as log history, player sessions, alarm queue, etc. + * + * @param guildId of the guild the nitrado server is assigned in. + * @param Nitrado configuration provided for this guild. + */ + private async initNewNitradoServer( guildId: Snowflake, Nitrado: NitradoConfig ): Promise { - let guild = await GetGuild(this, guildId) + let guild: GuildConfig = await GetGuild(this, guildId) if (guild.autoRestart) { @@ -767,32 +909,52 @@ export default class DayZR extends Client this.log(`[${guildId}] Initialized new Nitrado`); } - exists (n: T | null | undefined | "" | number): n is T + /** + * exists simply ensures a given input is not + * null, undefined, an empty string or NaN. + * + * @param n to validate + * @returns if this object exists (boolean) + */ + public exists (n: T | null | undefined | "" | number): n is T { return typeof n === "number" ? !isNaN(n) : n !== null && n !== undefined && n !== ""; } - secondsToDhms(seconds: number): string + /** + * secondsToDhms converts a given number of seconds + * to its equivilent in days, hours, minutes and seconds. + * + * @param seconds to convert to dhms + * @returns string of days x, hours y, minutes z, ... + */ + public secondsToDhms(seconds: number): string { // TODO; holy magic numbers, my prof is gonna assasinate me if he sees this seconds = Number(seconds); - const d = Math.floor(seconds / (3600 * 24)); - const h = Math.floor(seconds % (3600 * 24) / 3600); - const m = Math.floor(seconds % 3600 / 60); - const s = Math.floor(seconds % 60); + const d: number = Math.floor(seconds / (3600 * 24)); + const h: number = Math.floor(seconds % (3600 * 24) / 3600); + const m: number = Math.floor(seconds % 3600 / 60); + const s: number = Math.floor(seconds % 60); - const dDisplay = d > 0 ? d + (d == 1 ? " day, " : " days, ") : ""; - const hDisplay = h > 0 ? h + (h == 1 ? " hour, " : " hours, ") : ""; - const mDisplay = m > 0 ? m + (m == 1 ? " minute, " : " minutes, ") : ""; - const sDisplay = s > 0 ? s + (s == 1 ? " second" : " seconds") : ""; + const dDisplay: string = d > 0 ? d + (d == 1 ? " day, " : " days, ") : ""; + const hDisplay: string = h > 0 ? h + (h == 1 ? " hour, " : " hours, ") : ""; + const mDisplay: string = m > 0 ? m + (m == 1 ? " minute, " : " minutes, ") : ""; + const sDisplay: string = s > 0 ? s + (s == 1 ? " second" : " seconds") : ""; return dDisplay + hDisplay + mDisplay + sDisplay; } - LoadCommandsAndInteractionHandlers(): void + /** + * LoadCommandAndInteractoinHandlers checks the commands/ + * directory for each slash command, and maps their provided + * functions to their given name. As well as interaction + * handlers for button menues, select menus, etc. + */ + private LoadCommandsAndInteractionHandlers(): void { - let CommandsDir = path.join(__dirname, "commands"); + let CommandsDir: string = path.join(__dirname, "commands"); fs.readdir(CommandsDir, (err, files) => { @@ -830,9 +992,14 @@ export default class DayZR extends Client }); } - LoadEvents(): void + /** + * LoadEvents read events from the events/ directory + * and maps each event to a webhook listener to ensure + * we are listening to events we want to keep track of. + */ + private LoadEvents(): void { - let EventsDir = path.join(__dirname, "events"); + let EventsDir: string = path.join(__dirname, "events"); fs.readdir(EventsDir, (err, files) => { @@ -861,13 +1028,27 @@ export default class DayZR extends Client }); } - // Allows shorter lines of code elsewhere - GetChannel(channel_id: Snowflake): Channel | undefined + /** + * GetChannel takes a channel id and returns the channel + * object from discord's client. + * + * @param channel_id to get + * @returns Channel if found or undefined if not + */ + public GetChannel(channel_id: Snowflake): Channel | undefined { return this.channels.cache.get(channel_id); } - sendError(channel: TextBasedChannel, error: string): void + /** + * sendError sends an error embed with the given error + * message to a discord channel to alert end-users of + * an issue. + * + * @param channel to send error to + * @param error to include in embed + */ + public sendError(channel: TextBasedChannel, error: string): void { this.error(error); let embed: EmbedBuilder = new EmbedBuilder() @@ -878,8 +1059,15 @@ export default class DayZR extends Client channel.send({ embeds: [embed] }); } - // Handles internal errors for slash commands. E.g failed to update database from slash command. - sendInternalError( + /** + * sendInternalError should send errors if an internal error + * has occuered such as failed database operations, failed + * interaction handlers, etc. + * + * @param interaction to reply to with error warning + * @param error to log to internal logs + */ + public sendInternalError( interaction: ChatInputCommandInteraction | ButtonInteraction | StringSelectMenuInteraction, error: string ): void @@ -899,8 +1087,12 @@ export default class DayZR extends Client } } - // Calls register for guild and global commands - RegisterSlashCommands(): void + /** + * RegisterSlashCommands registers slash commands + * loaded earlier from commands/ directory with + * discords REST API. + */ + private RegisterSlashCommands(): void { RegisterGlobalCommands(this); @@ -919,7 +1111,10 @@ export default class DayZR extends Client }); } - build(): void + /** + * build logs into client once ready + */ + public build(): void { this.login(this.config.Token); } diff --git a/src/database/guild.ts b/src/database/guild.ts index 7eb405d..d391469 100644 --- a/src/database/guild.ts +++ b/src/database/guild.ts @@ -20,6 +20,13 @@ export interface Alarm // TODO: fill this out }; +export interface NitradoCredentials +{ + ServerID: string; + UserID: string; + Auth: string; +}; + export interface NitradoConfig { ServerID: string; @@ -78,7 +85,7 @@ interface GuildConfigAttributes combatLogTimer: number; } -interface GuildConfigDB +export interface GuildConfigDB { server: GuildConfigAttributes; Nitrado: NitradoConfig | null; From 1dd74f527527cf37703c9cdfa6b90dfa2accf692 Mon Sep 17 00:00:00 2001 From: SowinskiBraeden Date: Wed, 1 Oct 2025 09:31:08 -0700 Subject: [PATCH 5/6] refactor/project structure --- src/DayZRBot.ts | 43 +++++++---------- src/commands/admin.js | 29 ++++++------ src/commands/alarm.js | 9 ++-- src/commands/bank.js | 13 ++--- src/commands/bounty.js | 11 +++-- src/commands/collect-income.js | 7 +-- src/commands/compare-rating.js | 11 +++-- src/commands/config.js | 13 ++--- src/commands/event.js | 5 +- src/commands/gamertag-link.js | 15 +++--- src/commands/gamertag-unlink.js | 5 +- src/commands/leaderboard.js | 3 +- src/commands/location.js | 7 +-- src/commands/lookup.js | 5 +- src/commands/player-list.js | 5 +- src/commands/player-stats.js | 13 ++--- src/commands/purchase-emp.js | 9 ++-- src/commands/purchase-uav.js | 11 +++-- src/commands/reset.js | 3 +- src/commands/server.js | 7 +-- src/commands/weapon-stats.js | 9 ++-- src/database/guild.ts | 2 +- src/events/guildCreate.js | 2 +- src/events/guildMemberAdd.js | 2 +- src/{util => handlers}/AdminLogsHandler.js | 13 ++--- src/{util => handlers}/AlarmsHandler.js | 9 ++-- src/{util => handlers}/KillfeedHandler.js | 47 ++++++++++--------- src/{util => handlers}/LogsHandler.js | 37 ++++++++------- src/{util => services}/Logger.ts | 0 src/{util => services}/NitradoAPI.ts | 3 +- .../RegisterSlashCommands.js | 0 .../WebhookService.js} | 0 src/services/index.ts | 0 ...CombatRatingHandler.js => CombatRating.js} | 0 src/util/Validation.ts | 11 +++++ 35 files changed, 192 insertions(+), 167 deletions(-) rename src/{util => handlers}/AdminLogsHandler.js (86%) rename src/{util => handlers}/AlarmsHandler.js (95%) rename src/{util => handlers}/KillfeedHandler.js (83%) rename src/{util => handlers}/LogsHandler.js (85%) rename src/{util => services}/Logger.ts (100%) rename src/{util => services}/NitradoAPI.ts (99%) rename src/{util => services}/RegisterSlashCommands.js (100%) rename src/{util/WebhookHandler.js => services/WebhookService.js} (100%) create mode 100644 src/services/index.ts rename src/util/{CombatRatingHandler.js => CombatRating.js} (100%) create mode 100644 src/util/Validation.ts diff --git a/src/DayZRBot.ts b/src/DayZRBot.ts index 22e3888..e2c6895 100644 --- a/src/DayZRBot.ts +++ b/src/DayZRBot.ts @@ -32,7 +32,7 @@ import { MongoClient } from "mongodb"; import { Colors, Config } from "./config/config"; // Utilities -import Logger from "./util/Logger"; +import Logger from "./services/Logger"; import { decrypt } from "./util/Cryptic"; import { DownloadNitradoFile, @@ -40,12 +40,13 @@ import { FetchServerSettings, PostServerSettings, NitradoCredentialStatus, -} from "./util/NitradoAPI"; -import { RegisterGlobalCommands, RegisterGuildCommands } from "./util/RegisterSlashCommands"; -import { HandlePlayerLogs, HandleActivePlayersList } from "./util/LogsHandler"; -import { HandleKillfeed, UpdateLastDeathDate } from "./util/KillfeedHandler"; -import { HandleExpiredUAVs, HandleEvents, PlaceFireplaceInAlarm } from "./util/AlarmsHandler"; -import { GetWebhook, WebhookSend } from "./util/WebhookHandler"; +} from "./services/NitradoAPI"; +import { RegisterGlobalCommands, RegisterGuildCommands } from "./services/RegisterSlashCommands"; +import { HandlePlayerLogs, HandleActivePlayersList } from "./handlers/LogsHandler"; +import { HandleKillfeed, UpdateLastDeathDate } from "./handlers/KillfeedHandler"; +import { HandleExpiredUAVs, HandleEvents, PlaceFireplaceInAlarm } from "./handlers/AlarmsHandler"; +import { GetWebhook, WebhookSend } from "./services/WebhookService"; +import isDefined from "./util/Validation"; // Database import { Player, UpdatePlayer, getDefaultPlayer } from "./database/player"; @@ -223,7 +224,7 @@ export default class DayZR extends Client let GuildDB: GuildConfig = await GetGuild(this, interaction.guildId); - if (this.exists(GuildDB.Nitrado) && this.exists(GuildDB.Nitrado.Auth)) + if (isDefined(GuildDB.Nitrado) && isDefined(GuildDB.Nitrado.Auth)) { GuildDB.Nitrado.Auth = decrypt( GuildDB.Nitrado.Auth, @@ -545,16 +546,16 @@ export default class DayZR extends Client }; // Skip this player if the player does not exist. - if (!this.exists(info.player) || !this.exists(info.playerID)) continue; + if (!isDefined(info.player) || !isDefined(info.playerID)) continue; lastDetectedTime = this.getDateEST(info.time); let playerStat: Player = await this.dbo.collection("players").findOne({ "playerID": info.playerID }); - if (!this.exists(playerStat)) playerStat = getDefaultPlayer(info.player, info.playerID, guild.Nitrado.ServerID); + if (!isDefined(playerStat)) playerStat = getDefaultPlayer(info.player, info.playerID, guild.Nitrado.ServerID); // Skip this player if the lastDisconnectionDate time is later than the player log entry. if (!previouslyConnected.includes(playerStat) && - this.exists(playerStat.lastDisconnectionDate) && + isDefined(playerStat.lastDisconnectionDate) && playerStat.lastDisconnectionDate !== null && playerStat.lastDisconnectionDate.getTime() > lastDetectedTime.getTime() ) continue; @@ -649,7 +650,7 @@ export default class DayZR extends Client */ // Continue if no nitrado credentials - if (!c.exists(GuildDB.Nitrado)) return; + if (!isDefined(GuildDB.Nitrado)) return; // Continue if these credentials are marked as failed if (GuildDB.Nitrado.Status == NitradoCredentialStatus.FAILED) return; @@ -858,7 +859,7 @@ export default class DayZR extends Client for (let i = 0; i < guilds.length; i++) { // // Ignore guilds with no Nitrado configuration - if (!this.exists(guilds[i].Nitrado)) continue; + if (!isDefined(guilds[i].Nitrado)) continue; if (guilds[i].server.autoRestart) { @@ -909,18 +910,6 @@ export default class DayZR extends Client this.log(`[${guildId}] Initialized new Nitrado`); } - /** - * exists simply ensures a given input is not - * null, undefined, an empty string or NaN. - * - * @param n to validate - * @returns if this object exists (boolean) - */ - public exists (n: T | null | undefined | "" | number): n is T - { - return typeof n === "number" ? !isNaN(n) : n !== null && n !== undefined && n !== ""; - } - /** * secondsToDhms converts a given number of seconds * to its equivilent in days, hours, minutes and seconds. @@ -967,7 +956,7 @@ export default class DayZR extends Client files.forEach((file) => { let cmd: Command = require(CommandsDir + "/" + file); - if (!this.exists(cmd.name) || !this.exists(cmd.description)) + if (!isDefined(cmd.name) || !isDefined(cmd.description)) { return this.error( "Unable to load Command: " + @@ -978,7 +967,7 @@ export default class DayZR extends Client this.commands.set(file.split(".")[0].toLowerCase(), cmd); - if (this.exists(cmd.Interactions)) + if (isDefined(cmd.Interactions)) { for (let [interaction, handler] of Object.entries(cmd.Interactions)) { diff --git a/src/commands/admin.js b/src/commands/admin.js index 7f24cbf..ebd51f7 100644 --- a/src/commands/admin.js +++ b/src/commands/admin.js @@ -4,6 +4,7 @@ const bitfieldCalculator = require("discord-bitfield-calculator"); const { Armbands } = require("../database/armbands.js"); const { createUser, addUser } = require("../database/user"); const { UpdatePlayer } = require("../database/player"); +const isDefined = require("../util/Validation.js"); module.exports = { name: "admin", @@ -134,7 +135,7 @@ module.exports = { if (args[0].name == "gamertag-link") { - if (!client.exists(GuildDB.Nitrado) || !client.exists(GuildDB.Nitrado.ServerID) || !client.exists(GuildDB.Nitrado.UserID) || !client.exists(GuildDB.Nitrado.Auth)) { + if (!isDefined(GuildDB.Nitrado) || !isDefined(GuildDB.Nitrado.ServerID) || !isDefined(GuildDB.Nitrado.UserID) || !isDefined(GuildDB.Nitrado.Auth)) { const warnNitradoNotInitialized = new EmbedBuilder() .setColor(client.config.Colors.Yellow) .setDescription("**WARNING:** The DayZ Nitrado Server has not been configured for this guild yet. This command or feature is currently unavailable."); @@ -143,9 +144,9 @@ module.exports = { } let playerStat = await client.dbo.collection("players").findOne({ "gamertag": args[0].options[1].value }); - if (!client.exists(playerStat)) return interaction.send({ embeds: [new EmbedBuilder().setColor(client.config.Colors.Yellow).setDescription(`**Not Found** This gamertag \` ${args[0].options[1].value} \` cannot be found, the gamertag may be incorrect or this player has not logged onto the server before for at least \` 5 minutes \`.`)] }); + if (!isDefined(playerStat)) return interaction.send({ embeds: [new EmbedBuilder().setColor(client.config.Colors.Yellow).setDescription(`**Not Found** This gamertag \` ${args[0].options[1].value} \` cannot be found, the gamertag may be incorrect or this player has not logged onto the server before for at least \` 5 minutes \`.`)] }); - if (client.exists(playerStat.discordID)) { + if (isDefined(playerStat.discordID)) { const warnGTOverwrite = new EmbedBuilder() .setColor(client.config.Colors.Yellow) .setDescription(`**Notice:**\n> The gamertag has previously been linked to <@${playerStat.discordID}>. Are you sure you would like to change this?`) @@ -170,12 +171,12 @@ module.exports = { await UpdatePlayer(client, playerStat, interaction); let member = interaction.guild.members.cache.get(args[0].options[0].value); - if (client.exists(GuildDB.linkedGamertagRole)) { + if (isDefined(GuildDB.linkedGamertagRole)) { let role = interaction.guild.roles.cache.get(GuildDB.linkedGamertagRole); member.roles.add(role); } - if (client.exists(GuildDB.memberRole)) { + if (isDefined(GuildDB.memberRole)) { let role = interaction.guild.roles.cache.get(GuildDB.memberRole); member.roles.add(role); } @@ -188,7 +189,7 @@ module.exports = { } else if (args[0].name == "gamertag-unlink") { - if (!client.exists(GuildDB.Nitrado) || !client.exists(GuildDB.Nitrado.ServerID) || !client.exists(GuildDB.Nitrado.UserID) || !client.exists(GuildDB.Nitrado.Auth)) { + if (!isDefined(GuildDB.Nitrado) || !isDefined(GuildDB.Nitrado.ServerID) || !isDefined(GuildDB.Nitrado.UserID) || !isDefined(GuildDB.Nitrado.Auth)) { const warnNitradoNotInitialized = new EmbedBuilder() .setColor(client.config.Colors.Yellow) .setDescription("**WARNING:** The DayZ Nitrado Server has not been configured for this guild yet. This command or feature is currently unavailable."); @@ -197,7 +198,7 @@ module.exports = { } let playerStat = await client.dbo.collection("players").findOne({ "discordID": args[0].options[0].value }); - if (!client.exists(playerStat)) return interaction.send({ embeds: [new EmbedBuilder().setColor(client.config.Colors.Yellow).setDescription(`**Not Found** <@${args[0].options[0].value}> has no gamertag linked.`)] }); + if (!isDefined(playerStat)) return interaction.send({ embeds: [new EmbedBuilder().setColor(client.config.Colors.Yellow).setDescription(`**Not Found** <@${args[0].options[0].value}> has no gamertag linked.`)] }); const warnGTOverwrite = new EmbedBuilder() .setColor(client.config.Colors.Yellow) @@ -281,7 +282,7 @@ module.exports = { } else if (args[0].name == "bounty-clear") { - if (!client.exists(GuildDB.Nitrado) || !client.exists(GuildDB.Nitrado.ServerID) || !client.exists(GuildDB.Nitrado.UserID) || !client.exists(GuildDB.Nitrado.Auth)) { + if (!isDefined(GuildDB.Nitrado) || !isDefined(GuildDB.Nitrado.ServerID) || !isDefined(GuildDB.Nitrado.UserID) || !isDefined(GuildDB.Nitrado.Auth)) { const warnNitradoNotInitialized = new EmbedBuilder() .setColor(client.config.Colors.Yellow) .setDescription("**WARNING:** The DayZ Nitrado Server has not been configured for this guild yet. This command or feature is currently unavailable."); @@ -290,7 +291,7 @@ module.exports = { } let playerStat = await client.dbo.collection("players").findOne({ "gamertag": args[0].options[0].value }); - if (!client.exists(playerStat)) return interaction.send({ embeds: [new EmbedBuilder().setColor(client.config.Colors.Yellow).setDescription("**Not Found** This player cannot be found, the gamertag may be incorrect or this player has not logged onto the server before for at least ` 5 minutes `.")] }); + if (!isDefined(playerStat)) return interaction.send({ embeds: [new EmbedBuilder().setColor(client.config.Colors.Yellow).setDescription("**Not Found** This player cannot be found, the gamertag may be incorrect or this player has not logged onto the server before for at least ` 5 minutes `.")] }); playerStat.bounties = []; @@ -309,16 +310,16 @@ module.exports = { if (!banking) { banking = await createUser(targetUserID, GuildDB.serverID, GuildDB.startingBalance, client) - if (!client.exists(banking)) return client.sendInternalError(interaction, err); + if (!isDefined(banking)) return client.sendInternalError(interaction, err); } banking = banking.user; - if (!client.exists(banking.guilds[GuildDB.serverID])) { + if (!isDefined(banking.guilds[GuildDB.serverID])) { const success = addUser(banking.guilds, GuildDB.serverID, targetUserID, client, GuildDB.startingBalance); if (!success) return client.sendInternalError(interaction, "Failed to add bank"); } - if (!client.exists(banking.guilds[GuildDB.serverID].balance)) banking.guilds[GuildDB.serverID].balance = GuildDB.startingBalance; + if (!isDefined(banking.guilds[GuildDB.serverID].balance)) banking.guilds[GuildDB.serverID].balance = GuildDB.startingBalance; const add = args[0].options[0].name == "add"; let newBalance = add @@ -354,12 +355,12 @@ module.exports = { await UpdatePlayer(client, playerStat); let member = interaction.guild.members.cache.get(interaction.member.user.id); - if (client.exists(GuildDB.linkedGamertagRole)) { + if (isDefined(GuildDB.linkedGamertagRole)) { let role = interaction.guild.roles.cache.get(GuildDB.linkedGamertagRole); member.roles.add(role); } - if (client.exists(GuildDB.memberRole)) { + if (isDefined(GuildDB.memberRole)) { let role = interaction.guild.roles.cache.get(GuildDB.memberRole); member.roles.add(role); } diff --git a/src/commands/alarm.js b/src/commands/alarm.js index 90ff882..f8fce64 100644 --- a/src/commands/alarm.js +++ b/src/commands/alarm.js @@ -1,6 +1,7 @@ const { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, StringSelectMenuBuilder } = require("discord.js"); const { ApplicationCommandOptionType } = require("discord.js"); const bitfieldCalculator = require("discord-bitfield-calculator"); +const isDefined = require("../util/Validation.js"); const generateAlarmMenus = (alarms, customId, placeholder, description) => { let alarmComponents = []; @@ -237,7 +238,7 @@ module.exports = { if (GuildDB.hasBotAdmin && interaction.member.roles.filter(e => GuildDB.botAdminRoles.indexOf(e) !== -1).length > 0) canUseCommand = true; if (!canUseCommand) return interaction.send({ content: "You don\"t have the permissions to use this command." }); - if (!client.exists(GuildDB.Nitrado) || !client.exists(GuildDB.Nitrado.ServerID) || !client.exists(GuildDB.Nitrado.UserID) || !client.exists(GuildDB.Nitrado.Auth)) { + if (!isDefined(GuildDB.Nitrado) || !isDefined(GuildDB.Nitrado.ServerID) || !isDefined(GuildDB.Nitrado.UserID) || !isDefined(GuildDB.Nitrado.Auth)) { const warnNitradoNotInitialized = new EmbedBuilder() .setColor(client.config.Colors.Yellow) .setDescription("**WARNING:** The DayZ Nitrado Server has not been configured for this guild yet. This command or feature is currently unavailable."); @@ -259,8 +260,8 @@ module.exports = { role: args[0].options[5].value, ignoredPlayers: [], rules: [], - empExempt: client.exists(args[0].options[6]) ? args[0].options[6].value : false, - showPlayerCoord: client.exists(args[0].options[7]) ? args[0].options[7].value : true, + empExempt: isDefined(args[0].options[6]) ? args[0].options[6].value : false, + showPlayerCoord: isDefined(args[0].options[7]) ? args[0].options[7].value : true, disabled: false, empExpire: null, }; @@ -451,7 +452,7 @@ module.exports = { let alarmIndex = GuildDB.alarms.indexOf(alarm); let playerStat = await client.dbo.collection("players").findOne({ "gamertag": interaction.customId.split("-")[2] }); - if (!client.exists(playerStat)) return interaction.update({ embeds: [new EmbedBuilder().setColor(client.config.Colors.Yellow).setDescription("**Not Found** This player cannot be found, the gamertag may be incorrect or this player has not logged onto the server before.")], components: [] }); + if (!isDefined(playerStat)) return interaction.update({ embeds: [new EmbedBuilder().setColor(client.config.Colors.Yellow).setDescription("**Not Found** This player cannot be found, the gamertag may be incorrect or this player has not logged onto the server before.")], components: [] }); let add = interaction.customId.split("-")[1] == "add"; diff --git a/src/commands/bank.js b/src/commands/bank.js index 28ceec1..e3b17db 100644 --- a/src/commands/bank.js +++ b/src/commands/bank.js @@ -1,6 +1,7 @@ const { EmbedBuilder } = require("discord.js"); const { ApplicationCommandOptionType } = require("discord.js"); const { createUser, addUser } = require("../database/user"); +const isDefined = require("../util/Validation.js"); module.exports = { name: "bank", @@ -67,11 +68,11 @@ module.exports = { if (!banking) { banking = await createUser(interaction.member.user.id, GuildDB.serverID, GuildDB.startingBalance, client) - if (!client.exists(banking)) return client.sendInternalError(interaction, err); + if (!isDefined(banking)) return client.sendInternalError(interaction, err); } banking = banking.user; - if (!client.exists(banking.guilds[GuildDB.serverID])) { + if (!isDefined(banking.guilds[GuildDB.serverID])) { const success = addUser(banking.guilds, GuildDB.serverID, interaction.member.user.id, client, GuildDB.startingBalance); if (!success) return client.sendInternalError(interaction, "Failed to add bank"); } @@ -88,11 +89,11 @@ module.exports = { if (!targetUserBanking) { targetUserBanking = await createUser(targetUserID, GuildDB.serverID, GuildDB.startingBalance, client) - if (!client.exists(banking)) return client.sendInternalError(interaction, err); + if (!isDefined(banking)) return client.sendInternalError(interaction, err); } targetUserBanking = targetUserBanking.user; - if (!client.exists(targetUserBanking.guilds[GuildDB.serverID])) { + if (!isDefined(targetUserBanking.guilds[GuildDB.serverID])) { const success = addUser(banking.guilds, GuildDB.serverID, targetUserID, client, GuildDB.startingBalance); if (!success) return client.sendInternalError(interaction, "Failed to add bank"); } @@ -138,11 +139,11 @@ module.exports = { if (!targetUserBanking) { targetUserBanking = await createUser(targetUserID, GuildDB.serverID, GuildDB.startingBalance, client) - if (!client.exists(banking)) return client.sendInternalError(interaction, err); + if (!isDefined(banking)) return client.sendInternalError(interaction, err); } targetUserBanking = targetUserBanking.user; - if (!client.exists(targetUserBanking.guilds[GuildDB.serverID])) { + if (!isDefined(targetUserBanking.guilds[GuildDB.serverID])) { const success = addUser(banking.guilds, GuildDB.serverID, targetUserID, client, GuildDB.startingBalance); if (!success) return client.sendInternalError(interaction, "Failed to add bank"); } diff --git a/src/commands/bounty.js b/src/commands/bounty.js index 115574d..53bb854 100644 --- a/src/commands/bounty.js +++ b/src/commands/bounty.js @@ -2,6 +2,7 @@ const { EmbedBuilder } = require("discord.js"); const { ApplicationCommandOptionType } = require("discord.js"); const { createUser, addUser } = require("../database/user"); const { UpdatePlayer } = require("../database/player"); +const isDefined = require("../util/Validation.js"); module.exports = { name: "bounty", @@ -59,7 +60,7 @@ module.exports = { */ run: async (client, interaction, args, { GuildDB }) => { - if (!client.exists(GuildDB.Nitrado) || !client.exists(GuildDB.Nitrado.ServerID) || !client.exists(GuildDB.Nitrado.UserID) || !client.exists(GuildDB.Nitrado.Auth)) { + if (!isDefined(GuildDB.Nitrado) || !isDefined(GuildDB.Nitrado.ServerID) || !isDefined(GuildDB.Nitrado.UserID) || !isDefined(GuildDB.Nitrado.Auth)) { const warnNitradoNotInitialized = new EmbedBuilder() .setColor(client.config.Colors.Yellow) .setDescription("**WARNING:** The DayZ Nitrado Server has not been configured for this guild yet. This command or feature is currently unavailable."); @@ -73,11 +74,11 @@ module.exports = { if (!banking) { banking = await createUser(interaction.member.user.id, GuildDB.serverID, GuildDB.startingBalance, client) - if (!client.exists(banking)) return client.sendInternalError(interaction, err); + if (!isDefined(banking)) return client.sendInternalError(interaction, err); } banking = banking.user; - if (!client.exists(banking.guilds[GuildDB.serverID])) { + if (!isDefined(banking.guilds[GuildDB.serverID])) { const success = addUser(banking.guilds, GuildDB.serverID, interaction.member.user.id, client, GuildDB.startingBalance); if (!success) return client.sendInternalError(interaction, "Failed to add bank"); } @@ -86,7 +87,7 @@ module.exports = { if (args[0].name == "set") { let playerStat = await client.dbo.collection("players").findOne({ "gamertag": args[0].options[0].value }); - if (!client.exists(playerStat)) return interaction.send({ embeds: [new EmbedBuilder().setColor(client.config.Colors.Yellow).setDescription("**Not Found** This player cannot be found, the gamertag may be incorrect or this player has not logged onto the server before for at least ` 5 minutes `.")] }); + if (!isDefined(playerStat)) return interaction.send({ embeds: [new EmbedBuilder().setColor(client.config.Colors.Yellow).setDescription("**Not Found** This player cannot be found, the gamertag may be incorrect or this player has not logged onto the server before for at least ` 5 minutes `.")] }); if (args[0].options[1].value > banking.guilds[GuildDB.serverID].balance) { let nsf = new EmbedBuilder() @@ -126,7 +127,7 @@ module.exports = { } else if (args[0].name == "pay") { let playerStat = await client.dbo.collection("players").findOne({ "discordID": interaction.member.user.id }); - if (!client.exists(playerStat)) return interaction.send({ embeds: [new EmbedBuilder().setColor(client.config.Colors.Yellow).setDescription("**Not Found** Your user ID could not be found, contact an Admin.")] }); + if (!isDefined(playerStat)) return interaction.send({ embeds: [new EmbedBuilder().setColor(client.config.Colors.Yellow).setDescription("**Not Found** Your user ID could not be found, contact an Admin.")] }); if (playerStat.bounties.length == 0) { const noBounty = new EmbedBuilder() diff --git a/src/commands/collect-income.js b/src/commands/collect-income.js index f41b883..01976ce 100644 --- a/src/commands/collect-income.js +++ b/src/commands/collect-income.js @@ -1,5 +1,6 @@ const { EmbedBuilder, } = require("discord.js"); const { createUser, addUser } = require("../database/user"); +const isDefined = require("../util/Validation.js"); module.exports = { name: "collect-income", @@ -44,16 +45,16 @@ module.exports = { if (!banking) { banking = await createUser(interaction.member.user.id, GuildDB.serverID, GuildDB.startingBalance, client) - if (!client.exists(banking)) return client.sendInternalError(interaction, err); + if (!isDefined(banking)) return client.sendInternalError(interaction, err); } banking = banking.user; - if (!client.exists(banking.guilds[GuildDB.serverID])) { + if (!isDefined(banking.guilds[GuildDB.serverID])) { const success = addUser(banking.guilds, GuildDB.serverID, interaction.member.user.id, client, GuildDB.startingBalance); if (!success) return client.sendInternalError(interaction, "Failed to add bank"); } - if (!client.exists(banking.guilds[GuildDB.serverID].lastIncome)) banking.guilds[GuildDB.serverID].lastIncome = new Date("2000-01-01T00:00:00"); + if (!isDefined(banking.guilds[GuildDB.serverID].lastIncome)) banking.guilds[GuildDB.serverID].lastIncome = new Date("2000-01-01T00:00:00"); let now = new Date(); let diff = (now - banking.guilds[GuildDB.serverID].lastIncome) / 1000; diff --git a/src/commands/compare-rating.js b/src/commands/compare-rating.js index 8c4d077..d19b46e 100644 --- a/src/commands/compare-rating.js +++ b/src/commands/compare-rating.js @@ -1,6 +1,7 @@ const { EmbedBuilder } = require("discord.js"); const { ApplicationCommandOptionType } = require("discord.js"); const { insertPVPstats } = require("../database/player"); +const isDefined = require("../util/Validation.js"); module.exports = { name: "compare-rating", @@ -34,7 +35,7 @@ module.exports = { */ run: async (client, interaction, args, { GuildDB }) => { - if (!client.exists(GuildDB.Nitrado) || !client.exists(GuildDB.Nitrado.ServerID) || !client.exists(GuildDB.Nitrado.UserID) || !client.exists(GuildDB.Nitrado.Auth)) { + if (!isDefined(GuildDB.Nitrado) || !isDefined(GuildDB.Nitrado.ServerID) || !isDefined(GuildDB.Nitrado.UserID) || !isDefined(GuildDB.Nitrado.Auth)) { const warnNitradoNotInitialized = new EmbedBuilder() .setColor(client.config.Colors.Yellow) .setDescription("**WARNING:** The DayZ Nitrado Server has not been configured for this guild yet. This command or feature is currently unavailable."); @@ -56,8 +57,8 @@ module.exports = { if (gamertag) comp = leaderboard.find(s => s.gamertag == gamertag); let self = leaderboard.find(s => s.discordID == interaction.member.user.id); - if (!client.exists(comp)) return interaction.send({ embeds: [new EmbedBuilder().setColor(client.config.Colors.Yellow).setDescription(`**Not Found** Unable to find any records with the gamertag or user provided.`)] }); - if (!client.exists(self)) return interaction.send({ embeds: [new EmbedBuilder().setColor(client.config.Colors.Yellow).setDescription(`**Not Found** You haven"t linked your gamertag and your stats cannot be found.`)] }); + if (!isDefined(comp)) return interaction.send({ embeds: [new EmbedBuilder().setColor(client.config.Colors.Yellow).setDescription(`**Not Found** Unable to find any records with the gamertag or user provided.`)] }); + if (!isDefined(self)) return interaction.send({ embeds: [new EmbedBuilder().setColor(client.config.Colors.Yellow).setDescription(`**Not Found** You haven"t linked your gamertag and your stats cannot be found.`)] }); let lbPosSelf = leaderboard.indexOf(self) + 1; let lbPosComp = leaderboard.indexOf(comp) + 1; @@ -69,8 +70,8 @@ module.exports = { let selfDataMax = Math.max(...selfData); let compDataMax = Math.max(...compData); - if (!client.exists(self.highestCombatRating) || self.highestCombatRating < selfDataMax) self.highestCombatRating = selfDataMax; - if (!client.exists(comp.highestCombatRating) || comp.highestCombatRating < compDataMax) comp.highestCombatRating = compDataMax; + if (!isDefined(self.highestCombatRating) || self.highestCombatRating < selfDataMax) self.highestCombatRating = selfDataMax; + if (!isDefined(comp.highestCombatRating) || comp.highestCombatRating < compDataMax) comp.highestCombatRating = compDataMax; let tag = comp.discordID != "" ? `<@${comp.discordID}>` : comp.gamertag; diff --git a/src/commands/config.js b/src/commands/config.js index 3388647..7d010c2 100644 --- a/src/commands/config.js +++ b/src/commands/config.js @@ -2,6 +2,7 @@ const { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require(" const { ApplicationCommandOptionType } = require("discord.js"); const bitfieldCalculator = require("discord-bitfield-calculator"); const { getDefaultSettings } = require("../database/guild"); +const isDefined = require("../util/Validation.js"); module.exports = { name: "config", @@ -712,12 +713,12 @@ module.exports = { // Role / channel display const NONE = `${w}${m}none${w}`; const channelsInfo = GuildDB.customChannelStatus ? "\`\` to view" : NONE; - const killfeedChannel = client.exists(GuildDB.killfeedChannel) ? `<#${GuildDB.killfeedChannel}>` : NONE; - const connectionLogs = client.exists(GuildDB.connectionLogsChannel) ? `<#${GuildDB.connectionLogsChannel}>` : NONE; - const activePlayers = client.exists(GuildDB.activePlayersChannel) ? `<#${GuildDB.activePlayersChannel}>` : NONE; - const welcomeChannel = client.exists(GuildDB.welcomeChannel) ? `<#${GuildDB.welcomeChannel}>` : NONE; - const linkedGTRole = client.exists(GuildDB.linkedGamertagRole) ? `<@&${GuildDB.linkedGamertagRole}>` : NONE; - const memberRole = client.exists(GuildDB.memberRole) ? `<@&${GuildDB.memberRole}>` : NONE; + const killfeedChannel = isDefined(GuildDB.killfeedChannel) ? `<#${GuildDB.killfeedChannel}>` : NONE; + const connectionLogs = isDefined(GuildDB.connectionLogsChannel) ? `<#${GuildDB.connectionLogsChannel}>` : NONE; + const activePlayers = isDefined(GuildDB.activePlayersChannel) ? `<#${GuildDB.activePlayersChannel}>` : NONE; + const welcomeChannel = isDefined(GuildDB.welcomeChannel) ? `<#${GuildDB.welcomeChannel}>` : NONE; + const linkedGTRole = isDefined(GuildDB.linkedGamertagRole) ? `<@&${GuildDB.linkedGamertagRole}>` : NONE; + const memberRole = isDefined(GuildDB.memberRole) ? `<@&${GuildDB.memberRole}>` : NONE; // value display const incomeLimiter = `${GuildDB.incomeLimiter} hours`; diff --git a/src/commands/event.js b/src/commands/event.js index a1c581f..7965d4a 100644 --- a/src/commands/event.js +++ b/src/commands/event.js @@ -1,6 +1,7 @@ const { EmbedBuilder, ActionRowBuilder, StringSelectMenuBuilder } = require("discord.js"); const { ApplicationCommandOptionType } = require("discord.js"); const bitfieldCalculator = require("discord-bitfield-calculator"); +const isDefined = require("../util/Validation.js"); module.exports = { name: "event", @@ -72,7 +73,7 @@ module.exports = { */ run: async (client, interaction, args, { GuildDB }) => { - if (!client.exists(GuildDB.Nitrado) || !client.exists(GuildDB.Nitrado.ServerID) || !client.exists(GuildDB.Nitrado.UserID) || !client.exists(GuildDB.Nitrado.Auth)) { + if (!isDefined(GuildDB.Nitrado) || !isDefined(GuildDB.Nitrado.ServerID) || !isDefined(GuildDB.Nitrado.UserID) || !isDefined(GuildDB.Nitrado.Auth)) { const warnNitradoNotInitialized = new EmbedBuilder() .setColor(client.config.Colors.Yellow) .setDescription("**WARNING:** The DayZ Nitrado Server has not been configured for this guild yet. This command or feature is currently unavailable."); @@ -92,7 +93,7 @@ module.exports = { if (args[0].name == "player-track") { let playerStat = await client.dbo.collection("players").findOne({ "gamertag": args[0].options[0].value }); - if (!client.exists(playerStat)) return interaction.send({ embeds: [new EmbedBuilder().setColor(client.config.Colors.Yellow).setDescription(`**Not Found** This gamertag \` ${args[0].options[0].value} \` cannot be found, the gamertag may be incorrect or this player has not logged onto the server before for at least \` 5 minutes \`.`)] }); + if (!isDefined(playerStat)) return interaction.send({ embeds: [new EmbedBuilder().setColor(client.config.Colors.Yellow).setDescription(`**Not Found** This gamertag \` ${args[0].options[0].value} \` cannot be found, the gamertag may be incorrect or this player has not logged onto the server before for at least \` 5 minutes \`.`)] }); let event = { type: args[0].name, diff --git a/src/commands/gamertag-link.js b/src/commands/gamertag-link.js index 9753995..87b074a 100644 --- a/src/commands/gamertag-link.js +++ b/src/commands/gamertag-link.js @@ -1,6 +1,7 @@ const { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require("discord.js"); const { ApplicationCommandOptionType } = require("discord.js"); const { UpdatePlayer } = require("../database/player"); +const isDefined = require("../util/Validation.js"); module.exports = { name: "gamertag-link", @@ -29,7 +30,7 @@ module.exports = { */ run: async (client, interaction, args, { GuildDB }) => { - if (!client.exists(GuildDB.Nitrado) || !client.exists(GuildDB.Nitrado.ServerID) || !client.exists(GuildDB.Nitrado.UserID) || !client.exists(GuildDB.Nitrado.Auth)) { + if (!isDefined(GuildDB.Nitrado) || !isDefined(GuildDB.Nitrado.ServerID) || !isDefined(GuildDB.Nitrado.UserID) || !isDefined(GuildDB.Nitrado.Auth)) { const warnNitradoNotInitialized = new EmbedBuilder() .setColor(client.config.Colors.Yellow) .setDescription("**WARNING:** The DayZ Nitrado Server has not been configured for this guild yet. This command or feature is currently unavailable."); @@ -38,9 +39,9 @@ module.exports = { } let playerStat = await client.dbo.collection("players").findOne({ "gamertag": args[0].value }); - if (!client.exists(playerStat)) return interaction.send({ embeds: [new EmbedBuilder().setColor(client.config.Colors.Yellow).setDescription(`**Not Found** This gamertag \` ${args[0].value} \` cannot be found, the gamertag may be incorrect or this player has not logged onto the server before for at least \` 5 minutes \`.`)] }); + if (!isDefined(playerStat)) return interaction.send({ embeds: [new EmbedBuilder().setColor(client.config.Colors.Yellow).setDescription(`**Not Found** This gamertag \` ${args[0].value} \` cannot be found, the gamertag may be incorrect or this player has not logged onto the server before for at least \` 5 minutes \`.`)] }); - if (client.exists(playerStat.discordID)) { + if (isDefined(playerStat.discordID)) { const warnGTOverwrite = new EmbedBuilder() .setColor(client.config.Colors.Yellow) .setDescription(`**Notice:**\n> The gamertag has previously been linked to <@${playerStat.discordID}>. Are you sure you would like to change this?`) @@ -65,12 +66,12 @@ module.exports = { await UpdatePlayer(client, playerStat, interaction); let member = interaction.guild.members.cache.get(interaction.member.user.id); - if (client.exists(GuildDB.linkedGamertagRole)) { + if (isDefined(GuildDB.linkedGamertagRole)) { let role = interaction.guild.roles.cache.get(GuildDB.linkedGamertagRole); member.roles.add(role); } - if (client.exists(GuildDB.memberRole)) { + if (isDefined(GuildDB.memberRole)) { let role = interaction.guild.roles.cache.get(GuildDB.memberRole); member.roles.add(role); } @@ -98,12 +99,12 @@ module.exports = { await UpdatePlayer(client, playerStat, interaction); let member = interaction.guild.members.cache.get(interaction.member.user.id); - if (client.exists(GuildDB.linkedGamertagRole)) { + if (isDefined(GuildDB.linkedGamertagRole)) { let role = interaction.guild.roles.cache.get(GuildDB.linkedGamertagRole); member.roles.add(role); } - if (client.exists(GuildDB.memberRole)) { + if (isDefined(GuildDB.memberRole)) { let role = interaction.guild.roles.cache.get(GuildDB.memberRole); member.roles.add(role); } diff --git a/src/commands/gamertag-unlink.js b/src/commands/gamertag-unlink.js index 80ab4c0..41fd99a 100644 --- a/src/commands/gamertag-unlink.js +++ b/src/commands/gamertag-unlink.js @@ -1,5 +1,6 @@ const { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require("discord.js"); const { UpdatePlayer } = require("../database/player"); +const isDefined = require("../util/Validation.js"); module.exports = { name: "gamertag-unlink", @@ -21,7 +22,7 @@ module.exports = { */ run: async (client, interaction, args, { GuildDB }) => { - if (!client.exists(GuildDB.Nitrado) || !client.exists(GuildDB.Nitrado.ServerID) || !client.exists(GuildDB.Nitrado.UserID) || !client.exists(GuildDB.Nitrado.Auth)) { + if (!isDefined(GuildDB.Nitrado) || !isDefined(GuildDB.Nitrado.ServerID) || !isDefined(GuildDB.Nitrado.UserID) || !isDefined(GuildDB.Nitrado.Auth)) { const warnNitradoNotInitialized = new EmbedBuilder() .setColor(client.config.Colors.Yellow) .setDescription("**WARNING:** The DayZ Nitrado Server has not been configured for this guild yet. This command or feature is currently unavailable."); @@ -30,7 +31,7 @@ module.exports = { } let playerStat = await client.dbo.collection("players").findOne({ "discordID": interaction.member.user.id }); - if (!client.exists(playerStat)) return interaction.send({ embeds: [new EmbedBuilder().setColor(client.config.Colors.Yellow).setDescription(`**No Gamertag Linked** It Appears your don"t have a gamertag linked to your account.`)] }); + if (!isDefined(playerStat)) return interaction.send({ embeds: [new EmbedBuilder().setColor(client.config.Colors.Yellow).setDescription(`**No Gamertag Linked** It Appears your don"t have a gamertag linked to your account.`)] }); const warnGTOverwrite = new EmbedBuilder() .setColor(client.config.Colors.Yellow) diff --git a/src/commands/leaderboard.js b/src/commands/leaderboard.js index 0d60417..61bf430 100644 --- a/src/commands/leaderboard.js +++ b/src/commands/leaderboard.js @@ -1,5 +1,6 @@ const { EmbedBuilder } = require("discord.js"); const { ApplicationCommandOptionType } = require("discord.js"); +const isDefined = require("../util/Validation.js"); module.exports = { name: "leaderboard", @@ -53,7 +54,7 @@ module.exports = { */ run: async (client, interaction, args, { GuildDB }) => { - if (!client.exists(GuildDB.Nitrado) || !client.exists(GuildDB.Nitrado.ServerID) || !client.exists(GuildDB.Nitrado.UserID) || !client.exists(GuildDB.Nitrado.Auth)) { + if (!isDefined(GuildDB.Nitrado) || !isDefined(GuildDB.Nitrado.ServerID) || !isDefined(GuildDB.Nitrado.UserID) || !isDefined(GuildDB.Nitrado.Auth)) { const warnNitradoNotInitialized = new EmbedBuilder() .setColor(client.config.Colors.Yellow) .setDescription("**WARNING:** The DayZ Nitrado Server has not been configured for this guild yet. This command or feature is currently unavailable."); diff --git a/src/commands/location.js b/src/commands/location.js index dc2bec6..ad483b5 100644 --- a/src/commands/location.js +++ b/src/commands/location.js @@ -1,5 +1,6 @@ const { EmbedBuilder } = require("discord.js"); const { nearest } = require("../database/destinations"); +const isDefined = require("../util/Validation.js"); module.exports = { name: "location", @@ -21,7 +22,7 @@ module.exports = { */ run: async (client, interaction, args, { GuildDB }) => { - if (!client.exists(GuildDB.Nitrado) || !client.exists(GuildDB.Nitrado.ServerID) || !client.exists(GuildDB.Nitrado.UserID) || !client.exists(GuildDB.Nitrado.Auth) || !client.exists(GuildDB.Nitrado.Mission)) { + if (!isDefined(GuildDB.Nitrado) || !isDefined(GuildDB.Nitrado.ServerID) || !isDefined(GuildDB.Nitrado.UserID) || !isDefined(GuildDB.Nitrado.Auth) || !isDefined(GuildDB.Nitrado.Mission)) { const warnNitradoNotInitialized = new EmbedBuilder() .setColor(client.config.Colors.Yellow) .setDescription("**WARNING:** The DayZ Nitrado Server has not been configured for this guild yet. This command or feature is currently unavailable."); @@ -30,8 +31,8 @@ module.exports = { } let playerStat = await client.dbo.collection("players").findOne({ "discordID": interaction.member.user.id }); - if (!client.exists(playerStat)) return interaction.send({ embeds: [new EmbedBuilder().setColor(client.config.Colors.Yellow).setDescription(`**Not Found** You haven"t linked your gamertag and are unable to use this command.`)], flags: (1 << 6) }); - if (!client.exists(playerStat.time)) return interaction.send({ embeds: [new EmbedBuilder().setColor(client.config.Colors.Yellow).setDescription(`**Not Found** There is no location saved to your gamertag yet. Make sure you"ve logged into the server for more than **5 minutes.**`)], flags: (1 << 6) }); + if (!isDefined(playerStat)) return interaction.send({ embeds: [new EmbedBuilder().setColor(client.config.Colors.Yellow).setDescription(`**Not Found** You haven"t linked your gamertag and are unable to use this command.`)], flags: (1 << 6) }); + if (!isDefined(playerStat.time)) return interaction.send({ embeds: [new EmbedBuilder().setColor(client.config.Colors.Yellow).setDescription(`**Not Found** There is no location saved to your gamertag yet. Make sure you"ve logged into the server for more than **5 minutes.**`)], flags: (1 << 6) }); console.log(true); diff --git a/src/commands/lookup.js b/src/commands/lookup.js index 4da0f07..c3ad359 100644 --- a/src/commands/lookup.js +++ b/src/commands/lookup.js @@ -1,5 +1,6 @@ const { EmbedBuilder } = require("discord.js"); const { ApplicationCommandOptionType } = require("discord.js"); +const isDefined = require("../util/Validation.js"); module.exports = { name: "lookup", @@ -46,7 +47,7 @@ module.exports = { */ run: async (client, interaction, args, { GuildDB }) => { - if (!client.exists(GuildDB.Nitrado) || !client.exists(GuildDB.Nitrado.ServerID) || !client.exists(GuildDB.Nitrado.UserID) || !client.exists(GuildDB.Nitrado.Auth)) { + if (!isDefined(GuildDB.Nitrado) || !isDefined(GuildDB.Nitrado.ServerID) || !isDefined(GuildDB.Nitrado.UserID) || !isDefined(GuildDB.Nitrado.Auth)) { const warnNitradoNotInitialized = new EmbedBuilder() .setColor(client.config.Colors.Yellow) .setDescription("**WARNING:** The DayZ Nitrado Server has not been configured for this guild yet. This command or feature is currently unavailable."); @@ -59,7 +60,7 @@ module.exports = { let playerStat = await client.dbo.collection("players").findOne({ "gamertag": args[0].options[0].value }); if (playerStat == undefined) return interaction.send({ embeds: [new EmbedBuilder().setColor(client.config.Colors.Yellow).setDescription(`**Not Found** This gamertag \` ${args[0].options[0].value} \` cannot be found, the gamertag may be incorrect or this player has not logged onto the server before for at least \` 5 minutes \`.`)] }); - if (client.exists(playerStat.discordID)) { + if (isDefined(playerStat.discordID)) { const found = new EmbedBuilder() .setColor(client.config.Colors.Yellow) .setDescription(`**Record Found**\n> The gamertag \` ${playerStat.gamertag} \` is currently linked to <@${playerStat.discordID}>.`) diff --git a/src/commands/player-list.js b/src/commands/player-list.js index de0b2af..700da90 100644 --- a/src/commands/player-list.js +++ b/src/commands/player-list.js @@ -1,6 +1,7 @@ -const { FetchServerSettings } = require("../util/NitradoAPI"); +const { FetchServerSettings } = require("../services/NitradoAPI"); const { Missions } = require("../database/destinations"); const { EmbedBuilder } = require("discord.js"); +const isDefined = require("../util/Validation.js"); module.exports = { name: "player-list", @@ -23,7 +24,7 @@ module.exports = { */ run: async (client, interaction, args, { GuildDB }, start) => { - if (!client.exists(GuildDB.Nitrado) || !client.exists(GuildDB.Nitrado.ServerID) || !client.exists(GuildDB.Nitrado.UserID) || !client.exists(GuildDB.Nitrado.Auth)) { + if (!isDefined(GuildDB.Nitrado) || !isDefined(GuildDB.Nitrado.ServerID) || !isDefined(GuildDB.Nitrado.UserID) || !isDefined(GuildDB.Nitrado.Auth)) { const warnNitradoNotInitialized = new EmbedBuilder() .setColor(client.config.Colors.Yellow) .setDescription("**WARNING:** The DayZ Nitrado Server has not been configured for this guild yet. This command or feature is currently unavailable."); diff --git a/src/commands/player-stats.js b/src/commands/player-stats.js index c0a1119..d027d57 100644 --- a/src/commands/player-stats.js +++ b/src/commands/player-stats.js @@ -1,6 +1,7 @@ const { EmbedBuilder } = require("discord.js"); const { ApplicationCommandOptionType } = require("discord.js"); const { insertPVPstats } = require("../database/player"); +const isDefined = require("../util/Validation.js"); module.exports = { name: "player-stats", @@ -57,7 +58,7 @@ module.exports = { */ run: async (client, interaction, args, { GuildDB }, start) => { - if (!client.exists(GuildDB.Nitrado) || !client.exists(GuildDB.Nitrado.ServerID) || !client.exists(GuildDB.Nitrado.UserID) || !client.exists(GuildDB.Nitrado.Auth)) { + if (!isDefined(GuildDB.Nitrado) || !isDefined(GuildDB.Nitrado.ServerID) || !isDefined(GuildDB.Nitrado.UserID) || !isDefined(GuildDB.Nitrado.Auth)) { const warnNitradoNotInitialized = new EmbedBuilder() .setColor(client.config.Colors.Yellow) .setDescription("**WARNING:** The DayZ Nitrado Server has not been configured for this guild yet. This command or feature is currently unavailable."); @@ -84,7 +85,7 @@ module.exports = { if (gamertag) query = leaderboard.find(u => u.user.userID == playerStat.discordID); // Searching by gamertag if (self) query = leaderboard.find(u => u.user.userID == interaction.member.user.id); // Searching for self - if (!client.exists(query)) return interaction.send({ embeds: [new EmbedBuilder().setColor(client.config.Colors.Yellow).setDescription(`**Not Found** Unable to find any records with the gamertag or user provided.`)] }); + if (!isDefined(query)) return interaction.send({ embeds: [new EmbedBuilder().setColor(client.config.Colors.Yellow).setDescription(`**Not Found** Unable to find any records with the gamertag or user provided.`)] }); leaderboardPos = leaderboard.indexOf(query); } else { @@ -97,7 +98,7 @@ module.exports = { if (gamertag) query = leaderboard.find(s => s.gamertag == gamertag); // Searching by gamertag if (self) query = leaderboard.find(s => s.discordID == interaction.member.user.id); // Searching for self - if (!client.exists(query)) return interaction.send({ embeds: [new EmbedBuilder().setColor(client.config.Colors.Yellow).setDescription(`**Not Found** Unable to find any records with the gamertag or user provided.`)] }); + if (!isDefined(query)) return interaction.send({ embeds: [new EmbedBuilder().setColor(client.config.Colors.Yellow).setDescription(`**Not Found** Unable to find any records with the gamertag or user provided.`)] }); leaderboardPos = leaderboard.indexOf(query); } @@ -142,7 +143,7 @@ module.exports = { statsEmbed.addFields({ name: "Leaderboard Position", value: `# ${leaderboardPos}`, inline: true }); - if ((category == "shotsLanded" || category == "timesShot") && !client.exists(query.shotsLanded)) query = insertPVPstats(query); + if ((category == "shotsLanded" || category == "timesShot") && !isDefined(query.shotsLanded)) query = insertPVPstats(query); if (category == "totalSessionTime") { statsEmbed.addFields( @@ -242,8 +243,8 @@ module.exports = { let dataMax = Math.max(...query.combatRatingHistory); let dataMin = Math.min(...query.combatRatingHistory); - if (!client.exists(query.highestCombatRating) || query.highestCombatRating < dataMax) query.highestCombatRating = dataMax; - if (!client.exists(query.lowestCombatRating) || query.lowestCombatRating > dataMin) query.lowestCombatRating = dataMin; + if (!isDefined(query.highestCombatRating) || query.highestCombatRating < dataMax) query.highestCombatRating = dataMax; + if (!isDefined(query.lowestCombatRating) || query.lowestCombatRating > dataMin) query.lowestCombatRating = dataMin; statsEmbed.addFields( { name: "Combat Rating", value: `${query.combatRating}`, inline: true }, diff --git a/src/commands/purchase-emp.js b/src/commands/purchase-emp.js index 319292f..8fe0994 100644 --- a/src/commands/purchase-emp.js +++ b/src/commands/purchase-emp.js @@ -1,6 +1,7 @@ const { EmbedBuilder, ActionRowBuilder, StringSelectMenuBuilder } = require("discord.js"); const { createUser, addUser } = require("../database/user"); const { ApplicationCommandOptionType } = require("discord.js"); +const isDefined = require("../util/Validation.js"); module.exports = { name: "purchase-emp", @@ -33,7 +34,7 @@ module.exports = { */ run: async (client, interaction, args, { GuildDB }) => { - if (!client.exists(GuildDB.Nitrado) || !client.exists(GuildDB.Nitrado.ServerID) || !client.exists(GuildDB.Nitrado.UserID) || !client.exists(GuildDB.Nitrado.Auth)) { + if (!isDefined(GuildDB.Nitrado) || !isDefined(GuildDB.Nitrado.ServerID) || !isDefined(GuildDB.Nitrado.UserID) || !isDefined(GuildDB.Nitrado.Auth)) { const warnNitradoNotInitialized = new EmbedBuilder() .setColor(client.config.Colors.Yellow) .setDescription("**WARNING:** The DayZ Nitrado Server has not been configured for this guild yet. This command or feature is currently unavailable."); @@ -41,18 +42,18 @@ module.exports = { return interaction.send({ embeds: [warnNitradoNotInitialized], flags: (1 << 6) }); } - if (client.exists(GuildDB.purchaseEMP) && !GuildDB.purchaseEMP) return interaction.send({ embeds: [new EmbedBuilder().setColor(client.config.Colors.Yellow).setDescription("**Notice:** The admins have disabled this feature")] }); + if (isDefined(GuildDB.purchaseEMP) && !GuildDB.purchaseEMP) return interaction.send({ embeds: [new EmbedBuilder().setColor(client.config.Colors.Yellow).setDescription("**Notice:** The admins have disabled this feature")] }); const duration = args[0].value; let banking = await client.dbo.collection("users").findOne({ "user.userID": interaction.member.user.id }).then(banking => banking); if (!banking) { banking = await createUser(interaction.member.user.id, GuildDB.serverID, GuildDB.startingBalance, client) - if (!client.exists(banking)) return client.sendInternalError(interaction, err); + if (!isDefined(banking)) return client.sendInternalError(interaction, err); } banking = banking.user; - if (!client.exists(banking.guilds[GuildDB.serverID])) { + if (!isDefined(banking.guilds[GuildDB.serverID])) { const success = addUser(banking.guilds, GuildDB.serverID, interaction.member.user.id, client, GuildDB.startingBalance); if (!success) return client.sendInternalError(interaction, "Failed to add bank"); } diff --git a/src/commands/purchase-uav.js b/src/commands/purchase-uav.js index 344c53c..77d64db 100644 --- a/src/commands/purchase-uav.js +++ b/src/commands/purchase-uav.js @@ -1,6 +1,7 @@ const { EmbedBuilder } = require("discord.js"); const { ApplicationCommandOptionType } = require("discord.js"); -const { createUser, addUser } = require("../database/user") +const { createUser, addUser } = require("../database/user"); +const isDefined = require("../util/Validation.js"); module.exports = { name: "purchase-uav", @@ -40,7 +41,7 @@ module.exports = { */ run: async (client, interaction, args, { GuildDB }) => { - if (!client.exists(GuildDB.Nitrado) || !client.exists(GuildDB.Nitrado.ServerID) || !client.exists(GuildDB.Nitrado.UserID) || !client.exists(GuildDB.Nitrado.Auth)) { + if (!isDefined(GuildDB.Nitrado) || !isDefined(GuildDB.Nitrado.ServerID) || !isDefined(GuildDB.Nitrado.UserID) || !isDefined(GuildDB.Nitrado.Auth)) { const warnNitradoNotInitialized = new EmbedBuilder() .setColor(client.config.Colors.Yellow) .setDescription("**WARNING:** The DayZ Nitrado Server has not been configured for this guild yet. This command or feature is currently unavailable."); @@ -48,17 +49,17 @@ module.exports = { return interaction.send({ embeds: [warnNitradoNotInitialized], flags: (1 << 6) }); } - if (client.exists(GuildDB.purchaseUAV) && !GuildDB.purchaseUAV) return interaction.send({ embeds: [new EmbedBuilder().setColor(client.config.Colors.Yellow).setDescription("**Notice:** The admins have disabled this feature")] }); + if (isDefined(GuildDB.purchaseUAV) && !GuildDB.purchaseUAV) return interaction.send({ embeds: [new EmbedBuilder().setColor(client.config.Colors.Yellow).setDescription("**Notice:** The admins have disabled this feature")] }); let banking = await client.dbo.collection("users").findOne({ "user.userID": interaction.member.user.id }).then(banking => banking); if (!banking) { banking = await createUser(interaction.member.user.id, GuildDB.serverID, GuildDB.startingBalance, client) - if (!client.exists(banking)) return client.sendInternalError(interaction, err); + if (!isDefined(banking)) return client.sendInternalError(interaction, err); } banking = banking.user; - if (!client.exists(banking.guilds[GuildDB.serverID])) { + if (!isDefined(banking.guilds[GuildDB.serverID])) { const success = addUser(banking.guilds, GuildDB.serverID, interaction.member.user.id, client, GuildDB.startingBalance); if (!success) return client.sendInternalError(interaction, "Failed to add bank"); } diff --git a/src/commands/reset.js b/src/commands/reset.js index dc5a1c4..b717fb2 100644 --- a/src/commands/reset.js +++ b/src/commands/reset.js @@ -2,6 +2,7 @@ const { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require(" const { ApplicationCommandOptionType } = require("discord.js"); const { addUser } = require("../database/user"); const bitfieldCalculator = require("discord-bitfield-calculator"); +const isDefined = require("../util/Validation.js"); module.exports = { name: "reset", @@ -33,7 +34,7 @@ module.exports = { let canUseCommand = false; if (permissions.includes("MANAGE_GUILD")) canUseCommand = true; - if (client.exists(GuildDB.botAdmin) && interaction.member.roles.includes(GuildDB.botAdmin)) canUseCommand = true; + if (isDefined(GuildDB.botAdmin) && interaction.member.roles.includes(GuildDB.botAdmin)) canUseCommand = true; if (!canUseCommand) return interaction.send({ content: "You don\"t have the permissions to use this command." }); const targetUserID = args[0].value.replace("<@!", "").replace(">", ""); diff --git a/src/commands/server.js b/src/commands/server.js index 614b3b7..1e401f6 100644 --- a/src/commands/server.js +++ b/src/commands/server.js @@ -1,8 +1,9 @@ const { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, ModalBuilder, TextInputBuilder, TextInputStyle } = require("discord.js"); const { ApplicationCommandOptionType } = require("discord.js"); const bitfieldCalculator = require("discord-bitfield-calculator"); -const { BanPlayer, UnbanPlayer, RestartServer, CheckServerStatus, DisableBaseDamage, DisableContainerDamage, NitradoCredentialStatus } = require("../util/NitradoAPI"); +const { BanPlayer, UnbanPlayer, RestartServer, CheckServerStatus, DisableBaseDamage, DisableContainerDamage, NitradoCredentialStatus } = require("../services/NitradoAPI"); const { encrypt, decrypt } = require("../util/Cryptic"); +const isDefined = require("../util/Validation.js"); module.exports = { name: "server", @@ -117,7 +118,7 @@ module.exports = { if (args[0].name == "initialize") { - if (client.exists(GuildDB.Nitrado)) { + if (isDefined(GuildDB.Nitrado)) { const prompt = new EmbedBuilder() .setTitle(`Nitrado Server Information Already Configured!`) .setDescription("**Notice:** This will overwrite your previously configured Nitrado Server Information") @@ -190,7 +191,7 @@ module.exports = { return interaction.send({ embeds: [prompt], components: [opt], flags: (1 << 6) }); } - if (!client.exists(GuildDB.Nitrado) || !client.exists(GuildDB.Nitrado.ServerID)) return interaction.send({ embeds: [new EmbedBuilder().setColor(client.config.Colors.Red).setDescription(`**Notice:**\nThis Discord guild has not been configured with a Nitrado DayZ server. To configure your guild, use `)] }); + if (!isDefined(GuildDB.Nitrado) || !isDefined(GuildDB.Nitrado.ServerID)) return interaction.send({ embeds: [new EmbedBuilder().setColor(client.config.Colors.Red).setDescription(`**Notice:**\nThis Discord guild has not been configured with a Nitrado DayZ server. To configure your guild, use `)] }); if (args[0].name == "credentials-status") { diff --git a/src/commands/weapon-stats.js b/src/commands/weapon-stats.js index 24dcdbb..313dd1b 100644 --- a/src/commands/weapon-stats.js +++ b/src/commands/weapon-stats.js @@ -2,6 +2,7 @@ const { EmbedBuilder, ActionRowBuilder, StringSelectMenuBuilder } = require("dis const { ApplicationCommandOptionType } = require("discord.js"); const { weapons } = require("../database/weapons"); const { insertPVPstats, createWeaponStats } = require("../database/player"); +const isDefined = require("../util/Validation.js"); module.exports = { name: "weapon-stats", @@ -54,7 +55,7 @@ module.exports = { */ run: async (client, interaction, args, { GuildDB }) => { - if (!client.exists(GuildDB.Nitrado) || !client.exists(GuildDB.Nitrado.ServerID) || !client.exists(GuildDB.Nitrado.UserID) || !client.exists(GuildDB.Nitrado.Auth)) { + if (!isDefined(GuildDB.Nitrado) || !isDefined(GuildDB.Nitrado.ServerID) || !isDefined(GuildDB.Nitrado.UserID) || !isDefined(GuildDB.Nitrado.Auth)) { const warnNitradoNotInitialized = new EmbedBuilder() .setColor(client.config.Colors.Yellow) .setDescription("**WARNING:** The DayZ Nitrado Server has not been configured for this guild yet. This command or feature is currently unavailable."); @@ -78,7 +79,7 @@ module.exports = { // Searching for self if (self) query = await client.dbo.collection("players").findOne({ "discordID": interaction.member.user.id }); - if (!client.exists(query)) return interaction.send({ embeds: [new EmbedBuilder().setColor(client.config.Colors.Yellow).setDescription(`**Not Found** Unable to find any records with the gamertag or user provided.`)] }); + if (!isDefined(query)) return interaction.send({ embeds: [new EmbedBuilder().setColor(client.config.Colors.Yellow).setDescription(`**Not Found** Unable to find any records with the gamertag or user provided.`)] }); let weaponSelect = new StringSelectMenuBuilder() .setCustomId(`ViewWeaponStats-${query.playerID}-${interaction.member.user.id}`) @@ -110,8 +111,8 @@ module.exports = { let player = await client.dbo.collection("players").findOne({ "playerID": playerID }); const tag = player.discordID != "" ? `<@${player.discordID}>"s` : `**${player.gamertag}"s**`; - if (!client.exists(player.shotsLanded)) player = insertPVPstats(player); - if (!client.exists(player.weaponStats[weapon])) player = createWeaponStats(player, weapon); + if (!isDefined(player.shotsLanded)) player = insertPVPstats(player); + if (!isDefined(player.weaponStats[weapon])) player = createWeaponStats(player, weapon); let stats = new EmbedBuilder() .setColor(client.config.Colors.Default) diff --git a/src/database/guild.ts b/src/database/guild.ts index d391469..2174958 100644 --- a/src/database/guild.ts +++ b/src/database/guild.ts @@ -1,6 +1,6 @@ import { Snowflake } from "discord.js"; import DayZR from "../DayZRBot"; -import { NitradoCredentialStatus } from "../util/NitradoAPI"; +import { NitradoCredentialStatus } from "../services/NitradoAPI"; import { ArmbandName } from "./armbands"; import { MissionName } from "./destinations"; diff --git a/src/events/guildCreate.js b/src/events/guildCreate.js index 1e97d97..164834e 100644 --- a/src/events/guildCreate.js +++ b/src/events/guildCreate.js @@ -1,3 +1,3 @@ module.exports = (client, guild) => { - require("../util/RegisterSlashCommands").RegisterGuildCommands(client, guild.id); + require("../services/RegisterSlashCommands").RegisterGuildCommands(client, guild.id); }; \ No newline at end of file diff --git a/src/events/guildMemberAdd.js b/src/events/guildMemberAdd.js index 574c6bb..45873eb 100644 --- a/src/events/guildMemberAdd.js +++ b/src/events/guildMemberAdd.js @@ -4,7 +4,7 @@ const { GetGuild } = require("../database/guild"); module.exports = async (client, member) => { let GuildDB = await GetGuild(client, member.guild.id); - if (!client.exists(GuildDB.welcomeChannel)) return; + if (!isDefined(GuildDB.welcomeChannel)) return; const channel = client.GetChannel(GuildDB.welcomeChannel); if (GuildDB.serverName == "") GuildDB.serverName = "our server!" diff --git a/src/util/AdminLogsHandler.js b/src/handlers/AdminLogsHandler.js similarity index 86% rename from src/util/AdminLogsHandler.js rename to src/handlers/AdminLogsHandler.js index 83d4bfd..7868e61 100644 --- a/src/util/AdminLogsHandler.js +++ b/src/handlers/AdminLogsHandler.js @@ -1,11 +1,12 @@ const { EmbedBuilder } = require("discord.js"); const { nearest } = require("../database/destinations"); -const { GetWebhook, WebhookSend } = require("./WebhookHandler"); +const { GetWebhook, WebhookSend } = require("../services/WebhookService"); +const isDefined = require("../util/Validation.js"); module.exports = { SendConnectionLogs: async (client, guild, data) => { - if (!client.exists(guild.connectionLogsChannel)) return; + if (!isDefined(guild.connectionLogsChannel)) return; const channel = client.GetChannel(guild.connectionLogsChannel); if (!channel) return; @@ -27,13 +28,13 @@ module.exports = { } else connectionLog.addFields({ name: "**Session Time**", value: `**Unknown**`, inline: false }); } - // if (client.exists(channel)) await channel.send({ embeds: [connectionLog] }); + // if (isDefined(channel)) await channel.send({ embeds: [connectionLog] }); await WebhookSend(client, webhook, { embeds: [connectionLog] }); }, DetectCombatLog: async (client, guild, data) => { - if (!client.exists(data.lastDamageDate)) return; - if (!client.exists(guild.connectionLogsChannel)) return; + if (!isDefined(data.lastDamageDate)) return; + if (!isDefined(guild.connectionLogsChannel)) return; const channel = client.GetChannel(guild.connectionLogsChannel); if (!channel) return; // Ensure channel exists @@ -61,7 +62,7 @@ module.exports = { const webhook = await GetWebhook(client, NAME, guild.connectionLogsChannel); let content = { embeds: [combatLog] }; - if (client.exists(guild.adminRole)) content.content = `<@&${guild.adminRole}>`; + if (isDefined(guild.adminRole)) content.content = `<@&${guild.adminRole}>`; WebhookSend(client, webhook, content); // return channel.send({ embeds: [combatLog] }); } diff --git a/src/util/AlarmsHandler.js b/src/handlers/AlarmsHandler.js similarity index 95% rename from src/util/AlarmsHandler.js rename to src/handlers/AlarmsHandler.js index f4e0700..c15488a 100644 --- a/src/util/AlarmsHandler.js +++ b/src/handlers/AlarmsHandler.js @@ -2,14 +2,15 @@ const { BanPlayer, UnbanPlayer } = require("./NitradoAPI"); const { EmbedBuilder } = require("discord.js"); const { nearest } = require("../database/destinations"); const { GetGuild } = require("../database/guild"); -const { GetWebhook, WebhookSend } = require("./WebhookHandler"); +const { GetWebhook, WebhookSend } = require("../services/WebhookService"); +const isDefined = require("../util/Validation.js"); // Private functions (only called locally) const ExpireEvent = async (client, guild, e) => { let hasMR = (guild.memberRole != ""); const channel = client.GetChannel(e.channel); - if (client.exists(e.channel)) channel.send({ embeds: [new EmbedBuilder().setColor(client.config.Colors.Default).setDescription(`${hasMR ? `<@&${guild.memberRole}>\n` : ""}**The ${e.name} Event has ended!**`)] }); + if (isDefined(e.channel)) channel.send({ embeds: [new EmbedBuilder().setColor(client.config.Colors.Default).setDescription(`${hasMR ? `<@&${guild.memberRole}>\n` : ""}**The ${e.name} Event has ended!**`)] }); client.dbo.collection("guilds").updateOne({ "server.serverID": guild.serverID }, { $pull: { @@ -21,7 +22,7 @@ const ExpireEvent = async (client, guild, e) => { } const HandlePlayerTrackEvent = async (client, guild, e) => { - if (!client.exists(e.channel)) return ExpireEvent(client, guild, e); // Expire event since it has invalid channel. + if (!isDefined(e.channel)) return ExpireEvent(client, guild, e); // Expire event since it has invalid channel. const channel = client.GetChannel(e.channel); if (!channel) return; @@ -40,7 +41,7 @@ const HandlePlayerTrackEvent = async (client, guild, e) => { const webhook = await GetWebhook(client, NAME, e.channel); let content = { embeds: [trackEvent] }; - if (client.exists(guild.adminRole)) content.content = `<@&${e.role}>`; + if (isDefined(guild.adminRole)) content.content = `<@&${e.role}>`; WebhookSend(client, webhook, content); // if (e.role) channel.send({ content: `<@&${e.role}>`, embeds: [trackEvent] }); diff --git a/src/util/KillfeedHandler.js b/src/handlers/KillfeedHandler.js similarity index 83% rename from src/util/KillfeedHandler.js rename to src/handlers/KillfeedHandler.js index b9c53a9..4879b40 100644 --- a/src/util/KillfeedHandler.js +++ b/src/handlers/KillfeedHandler.js @@ -3,9 +3,10 @@ const { createUser, addUser } = require("../database/user"); const { KillInAlarm } = require("./AlarmsHandler"); const { nearest } = require("../database/destinations"); const { getDefaultPlayer, UpdatePlayer } = require("../database/player"); -const { calculateNewCombatRating } = require("./CombatRatingHandler"); +const { calculateNewCombatRating } = require("../util/CombatRating"); const { weapons, weaponClassOf } = require("../database/weapons"); const { GetWebhook, WebhookSend } = require("../util/WebhookHandler"); +const isDefined = require("../util/Validation.js"); const Templates = { Killed: 1, @@ -71,7 +72,7 @@ module.exports = { const newDt = await client.getDateEST(info.time); let victimStat = await client.dbo.collection("players").findOne({ "playerID": info.playerID }); - if (!client.exists(victimStat)) victimStat = getDefaultPlayer(info.player, info.playerID, NitradoServerID); + if (!isDefined(victimStat)) victimStat = getDefaultPlayer(info.player, info.playerID, NitradoServerID); victimStat.lastDeathDate = newDt; @@ -126,15 +127,15 @@ module.exports = { const newDt = await client.getDateEST(info.time); const unixTime = Math.floor(newDt.getTime() / 1000); - const showCoords = client.exists(guild.showKillfeedCoords) ? guild.showKillfeedCoords : false; // default to false if no record of configuration. - const showWeapon = client.exists(guild.showKillfeedWeapon) ? guild.showKillfeedWeapon : false; // default to false if no record of configuration. + const showCoords = isDefined(guild.showKillfeedCoords) ? guild.showKillfeedCoords : false; // default to false if no record of configuration. + const showWeapon = isDefined(guild.showKillfeedWeapon) ? guild.showKillfeedWeapon : false; // default to false if no record of configuration. const destination = nearest(info.victimPOS, guild.Nitrado.Mission); if ([Templates.LandMine, Templates.Explosion, Templates.Vehicle].includes(killedBy)) if (killedBy == Templates.LandMine || killedBy == Templates.Explosion || killedBy == Templates.Vehicle) { let victimStat = await client.dbo.collection("players").findOne({ "playerID": info.victimID }); - if (!client.exists(victimStat)) victimStat = getDefaultPlayer(info.victim, info.victimID, NitradoServerID); + if (!isDefined(victimStat)) victimStat = getDefaultPlayer(info.victim, info.victimID, NitradoServerID); victimStat.deaths++; victimStat.deathStreak++; victimStat.worstDeathStreak = victimStat.deathStreak > victimStat.worstDeathStreak ? victimStat.deathStreak : victimStat.worstDeathStreak; @@ -157,18 +158,18 @@ module.exports = { const webhook = await GetWebhook(client, NAME, guild.killfeedChannel); WebhookSend(client, webhook, { embeds: [killEvent] }); - // if (client.exists(channel)) await channel.send({ embeds: [killEvent] }); + // if (isDefined(channel)) await channel.send({ embeds: [killEvent] }); return; } KillInAlarm(client, guild.serverID, info); // check if kill happened in a no kill zone - if (!client.exists(info.victim) || !client.exists(info.victimID) || !client.exists(info.killer) || !client.exists(info.killerID)) return; + if (!isDefined(info.victim) || !isDefined(info.victimID) || !isDefined(info.killer) || !isDefined(info.killerID)) return; let victimStat = await client.dbo.collection("players").findOne({ "playerID": info.victimID }); let killerStat = await client.dbo.collection("players").findOne({ "playerID": info.killerID }); - if (!client.exists(victimStat)) victimStat = getDefaultPlayer(info.victim, info.victimID, NitradoServerID); - if (!client.exists(killerStat)) killerStat = getDefaultPlayer(info.killer, info.killerID, NitradoServerID); + if (!isDefined(victimStat)) victimStat = getDefaultPlayer(info.victim, info.victimID, NitradoServerID); + if (!isDefined(killerStat)) killerStat = getDefaultPlayer(info.killer, info.killerID, NitradoServerID); let weapon = info.weapon.includes("Engraved") ? info.weapon.split("Engraved ")[1] : info.weapon.includes("Sawed-off") ? info.weapon.split("Sawed-off ")[1] : @@ -181,7 +182,7 @@ module.exports = { killerStat.KDR = killerStat.kills / (killerStat.deaths == 0 ? 1 : killerStat.deaths); // prevent division by 0 killerStat.longestKill = info.distance > killerStat.longestKill ? info.distance : killerStat.longestKill; killerStat.deathStreak = 0; - if (!client.exists(killerStat.weaponStats[weapon].kills)) killerStat.weaponStats[weapon].kills = 0; + if (!isDefined(killerStat.weaponStats[weapon].kills)) killerStat.weaponStats[weapon].kills = 0; killerStat.weaponStats[weapon].kills++; // Update victim stats @@ -191,21 +192,21 @@ module.exports = { victimStat.KDR = victimStat.kills / (victimStat.deaths == 0 ? 1 : victimStat.deaths); // prevent division by 0 victimStat.killStreak = 0; victimStat.lastDeathDate = newDt; - if (!client.exists(victimStat.weaponStats[weapon].deaths)) victimStat.weaponStats[weapon].death = 0; + if (!isDefined(victimStat.weaponStats[weapon].deaths)) victimStat.weaponStats[weapon].death = 0; victimStat.weaponStats[weapon].deaths++; // Create defaults for non-existing ratings - if (!client.exists(killerStat.combatRating)) killerStat.combatRating = 800; - if (!client.exists(victimStat.combatRating)) victimStat.combatRating = 800; - if (!client.exists(killerStat.combatRatingHistory)) killerStat.combatRatingHistory = [800]; - if (!client.exists(victimStat.combatRatingHistory)) victimStat.combatRatingHistory = [800]; - if (!client.exists(killerStat.highestCombatRating)) killerStat.highestCombatRating = Math.max(...killerStat.combatRatingHistory); - if (!client.exists(victimStat.lowestCombatRating)) victimStat.lowestCombatRating = Math.min(...victimStat.combatRatingHistory); + if (!isDefined(killerStat.combatRating)) killerStat.combatRating = 800; + if (!isDefined(victimStat.combatRating)) victimStat.combatRating = 800; + if (!isDefined(killerStat.combatRatingHistory)) killerStat.combatRatingHistory = [800]; + if (!isDefined(victimStat.combatRatingHistory)) victimStat.combatRatingHistory = [800]; + if (!isDefined(killerStat.highestCombatRating)) killerStat.highestCombatRating = Math.max(...killerStat.combatRatingHistory); + if (!isDefined(victimStat.lowestCombatRating)) victimStat.lowestCombatRating = Math.min(...victimStat.combatRatingHistory); // Calculate new ratings let killerOldRating = killerStat.combatRating; let victimOldRating = victimStat.combatRating; - killerStat.combatRating = calculateNewCombatRating(killerStat.combatRating, victimStat.combatRating, client.exists(info.bodyPart) && info.bodyPart.includes("Head") ? 1.25 : 1); + killerStat.combatRating = calculateNewCombatRating(killerStat.combatRating, victimStat.combatRating, isDefined(info.bodyPart) && info.bodyPart.includes("Head") ? 1.25 : 1); victimStat.combatRating = calculateNewCombatRating(victimStat.combatRating, killerStat.combatRating, 0); // Update combat rating records @@ -230,11 +231,11 @@ module.exports = { if (!banking) { banking = await createUser(interaction.member.user.id, guild.serverID, guild.startingBalance, client) - if (!client.exists(banking)) return client.sendInternalError(interaction, err); + if (!isDefined(banking)) return client.sendInternalError(interaction, err); } banking = banking.user; - if (!client.exists(banking.guilds[guild.serverID])) { + if (!isDefined(banking.guilds[guild.serverID])) { const success = addUser(banking.guilds, guild.serverID, interaction.member.user.id, client, guild.startingBalance); if (!success) return client.sendInternalError(interaction, "Failed to add bank"); } @@ -280,10 +281,10 @@ module.exports = { const webhook = await GetWebhook(client, NAME, guild.killfeedChannel); WebhookSend(client, webhook, { embeds: [killEvent] }); - if (client.exists(receivedBounty) && client.exists(channel)) WebhookSend(client, webhook, { content: `<@${killerStat.discordID}>`, embeds: [receivedBounty] }); + if (isDefined(receivedBounty) && isDefined(channel)) WebhookSend(client, webhook, { content: `<@${killerStat.discordID}>`, embeds: [receivedBounty] }); - // if (client.exists(channel)) await channel.send({ embeds: [killEvent] }); - // if (client.exists(receivedBounty) && client.exists(channel)) await channel.send({ content: `<@${killerStat.discordID}>`, embeds: [receivedBounty] }); + // if (isDefined(channel)) await channel.send({ embeds: [killEvent] }); + // if (isDefined(receivedBounty) && isDefined(channel)) await channel.send({ content: `<@${killerStat.discordID}>`, embeds: [receivedBounty] }); return; } diff --git a/src/util/LogsHandler.js b/src/handlers/LogsHandler.js similarity index 85% rename from src/util/LogsHandler.js rename to src/handlers/LogsHandler.js index 25fc86c..150af1c 100644 --- a/src/util/LogsHandler.js +++ b/src/handlers/LogsHandler.js @@ -5,7 +5,8 @@ const { getDefaultPlayer } = require("../database/player"); const { FetchServerSettings } = require("./NitradoAPI"); const { UpdatePlayer, insertPVPstats, createWeaponStats } = require("../database/player") const { Missions } = require("../database/destinations"); -const { GetWebhook, WebhookSend, WebhookMessageEdit } = require("./WebhookHandler"); +const { GetWebhook, WebhookSend, WebhookMessageEdit } = require("../services/WebhookService"); +const isDefined = require("../util/Validation.js"); module.exports = { @@ -27,15 +28,15 @@ module.exports = { playerID: data[3], }; - if (!client.exists(info.player) || !client.exists(info.playerID)) return; + if (!isDefined(info.player) || !isDefined(info.playerID)) return; let playerStat = await client.dbo.collection("players").findOne({ "playerID": info.playerID }); - if (!client.exists(playerStat)) playerStat = getDefaultPlayer(info.player, info.playerID, NitradoServerID); + if (!isDefined(playerStat)) playerStat = getDefaultPlayer(info.player, info.playerID, NitradoServerID); const newDt = await client.getDateEST(info.time); playerStat.lastConnectionDate = newDt; playerStat.connected = true; - if (!client.exists(playerStat.connections)) playerStat.connections = 0; + if (!isDefined(playerStat.connections)) playerStat.connections = 0; playerStat.connections++; // Track adjusted sessions this instance has handled (e.g. no bot crashes or restarts). @@ -72,10 +73,10 @@ module.exports = { playerID: data[3], }; - if (!client.exists(info.player) || !client.exists(info.playerID)) return; + if (!isDefined(info.player) || !isDefined(info.playerID)) return; let playerStat = await client.dbo.collection("players").findOne({ "playerID": info.playerID }); - if (!client.exists(playerStat)) playerStat = getDefaultPlayer(info.player, info.playerID, NitradoServerID); + if (!isDefined(playerStat)) playerStat = getDefaultPlayer(info.player, info.playerID, NitradoServerID); let oldUnixTime; let sessionTimeSeconds; @@ -85,7 +86,7 @@ module.exports = { oldUnixTime = Math.round(playerStat.lastConnectionDate.getTime() / 1000); // Seconds sessionTimeSeconds = unixTime - oldUnixTime; } else sessionTimeSeconds = 0; - if (!client.exists(playerStat.longestSessionTime)) playerStat.longestSessionTime = 0; + if (!isDefined(playerStat.longestSessionTime)) playerStat.longestSessionTime = 0; playerStat.totalSessionTime = playerStat.totalSessionTime + sessionTimeSeconds; playerStat.lastSessionTime = sessionTimeSeconds; @@ -126,11 +127,11 @@ module.exports = { pos: data[4].split(", ").map(v => parseFloat(v)) }; - if (!client.exists(info.player) || !client.exists(info.playerID)) return; + if (!isDefined(info.player) || !isDefined(info.playerID)) return; let playerStat = await client.dbo.collection("players").findOne({ "playerID": info.playerID }); - if (!client.exists(playerStat)) playerStat = getDefaultPlayer(info.player, info.playerID, NitradoServerID); - if (!client.exists(playerStat.lastConnectionDate)) playerStat.lastConnectionDate = await client.getDateEST(info.time); + if (!isDefined(playerStat)) playerStat = getDefaultPlayer(info.player, info.playerID, NitradoServerID); + if (!isDefined(playerStat.lastConnectionDate)) playerStat.lastConnectionDate = await client.getDateEST(info.time); playerStat.lastPos = playerStat.pos; playerStat.pos = info.pos; @@ -165,18 +166,18 @@ module.exports = { weapon: data[12], }; - if (!client.exists(info.player) || !client.exists(info.playerID) || !client.exists(info.attacker) || !client.exists(info.attackerID)) return; + if (!isDefined(info.player) || !isDefined(info.playerID) || !isDefined(info.attacker) || !isDefined(info.attackerID)) return; let playerStat = await client.dbo.collection("players").findOne({ "playerID": info.playerID }); let attackerStat = await client.dbo.collection("players").findOne({ "playerID": info.attackerID }); - if (!client.exists(playerStat)) playerStat = getDefaultPlayer(info.player, info.playerID, NitradoServerID); - if (!client.exists(attackerStat)) attackerStat = getDefaultPlayer(info.attacker, info.attackerID, NitradoServerID); + if (!isDefined(playerStat)) playerStat = getDefaultPlayer(info.player, info.playerID, NitradoServerID); + if (!isDefined(attackerStat)) attackerStat = getDefaultPlayer(info.attacker, info.attackerID, NitradoServerID); playerStat.lastDamageDate = await client.getDateEST(info.time); playerStat.lastHitBy = info.attacker; - if (!client.exists(playerStat.shotsLanded)) playerStat = insertPVPstats(playerStat); - if (!client.exists(attackerStat.shotsLanded)) attackerStat = insertPVPstats(attackerStat); + if (!isDefined(playerStat.shotsLanded)) playerStat = insertPVPstats(playerStat); + if (!isDefined(attackerStat.shotsLanded)) attackerStat = insertPVPstats(attackerStat); // Update in depth PVP stats if non Melee weapon if (info.weapon.includes("Engraved")) info.weapon = info.weapon.split("Engraved ")[1]; @@ -184,13 +185,13 @@ module.exports = { if (info.weapon in playerStat.weaponStats) { playerStat.timesShot++; playerStat.timesShotPerBodyPart[info.bodyPart]++; - if (!client.exists(playerStat.weaponStats[info.weapon])) playerStat = createWeaponStats(playerStat, info.weapon); + if (!isDefined(playerStat.weaponStats[info.weapon])) playerStat = createWeaponStats(playerStat, info.weapon); playerStat.weaponStats[info.weapon].timesShot++; playerStat.weaponStats[info.weapon].timesShotPerBodyPart[info.bodyPart]++; attackerStat.shotsLanded++; attackerStat.shotsLandedPerBodyPart[info.bodyPart]++; - if (!client.exists(attackerStat.weaponStats[info.weapon])) attackerStat = createWeaponStats(attackerStat, info.weapon); + if (!isDefined(attackerStat.weaponStats[info.weapon])) attackerStat = createWeaponStats(attackerStat, info.weapon); attackerStat.weaponStats[info.weapon].shotsLanded++; attackerStat.weaponStats[info.weapon].shotsLandedPerBodyPart[info.bodyPart]++; } @@ -205,7 +206,7 @@ module.exports = { HandleActivePlayersList: async (nitrado_cred, client, guild) => { client.activePlayersTick = 0; // reset hour tick - if (!client.exists(guild.activePlayersChannel)) return; + if (!isDefined(guild.activePlayersChannel)) return; const channel = client.GetChannel(guild.activePlayersChannel); if (!channel) return; diff --git a/src/util/Logger.ts b/src/services/Logger.ts similarity index 100% rename from src/util/Logger.ts rename to src/services/Logger.ts diff --git a/src/util/NitradoAPI.ts b/src/services/NitradoAPI.ts similarity index 99% rename from src/util/NitradoAPI.ts rename to src/services/NitradoAPI.ts index 073906d..1e284ad 100644 --- a/src/util/NitradoAPI.ts +++ b/src/services/NitradoAPI.ts @@ -3,6 +3,7 @@ const concat = require("concat-stream"); // convert to import ? import { Readable } from "stream"; import FormData from "form-data"; import * as fs from "fs"; +import isDefined from "../util/Validation"; const MAX_RETRIES = 5; const RETRY_DELAY_MS = 5000; // 5 seconds @@ -73,7 +74,7 @@ const HandlePlayerBan = async (nitrado_cred: any, client: any, gamertag: any, ba } const GetRemoteDir = async (nitrado_cred: any, client: any, dir = "") => { - const dirParam = client.exists(dir) ? `?dir=${dir}` : ""; + const dirParam = isDefined(dir) ? `?dir=${dir}` : ""; for (let retries = 0; retries <= MAX_RETRIES; retries++) { try { const res = await fetch(`https://api.nitrado.net/services/${nitrado_cred.ServerID}/gameservers/file_server/list${dirParam}`, { diff --git a/src/util/RegisterSlashCommands.js b/src/services/RegisterSlashCommands.js similarity index 100% rename from src/util/RegisterSlashCommands.js rename to src/services/RegisterSlashCommands.js diff --git a/src/util/WebhookHandler.js b/src/services/WebhookService.js similarity index 100% rename from src/util/WebhookHandler.js rename to src/services/WebhookService.js diff --git a/src/services/index.ts b/src/services/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/util/CombatRatingHandler.js b/src/util/CombatRating.js similarity index 100% rename from src/util/CombatRatingHandler.js rename to src/util/CombatRating.js diff --git a/src/util/Validation.ts b/src/util/Validation.ts new file mode 100644 index 0000000..8ce251e --- /dev/null +++ b/src/util/Validation.ts @@ -0,0 +1,11 @@ +/** + * isDefined simply ensures a given input is not + * null, undefined, an empty string or NaN. + * + * @param n to validate + * @returns if this object exists (boolean) + */ +export default function isDefined(n: T | null | undefined | "" | number): n is T +{ + return typeof n === "number" ? !isNaN(n) : n !== null && n !== undefined && n !== ""; +} From 39c2e45fde7f128308e1e65401f72355220ea0e8 Mon Sep 17 00:00:00 2001 From: SowinskiBraeden Date: Wed, 1 Oct 2025 12:38:25 -0700 Subject: [PATCH 6/6] remove deprecated file, will impliment later --- src/services/index.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/services/index.ts diff --git a/src/services/index.ts b/src/services/index.ts deleted file mode 100644 index e69de29..0000000