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"] }