diff --git a/commands/admin-gamertag-link.js b/commands/admin-gamertag-link.js index 6e6c783..09d43c5 100644 --- a/commands/admin-gamertag-link.js +++ b/commands/admin-gamertag-link.js @@ -49,7 +49,7 @@ module.exports = { "server.playerstats": [] } }, function (err, res) { - if (err) return this.sendInternalError(interaction, err); + if (err) return client.sendInternalError(interaction, err); }); } diff --git a/commands/gamertag-link.js b/commands/gamertag-link.js index 47b7ccf..eccd33c 100644 --- a/commands/gamertag-link.js +++ b/commands/gamertag-link.js @@ -34,7 +34,7 @@ module.exports = { "server.playerstats": [] } }, function (err, res) { - if (err) return this.sendInternalError(interaction, err); + if (err) return client.sendInternalError(interaction, err); }); } diff --git a/commands/player-list.js b/commands/player-list.js index b234413..2cb7f6c 100644 --- a/commands/player-list.js +++ b/commands/player-list.js @@ -29,7 +29,7 @@ module.exports = { } const activePlayersEmbed = new EmbedBuilder() - .setColor(this.config.Colors.Default) + .setColor(client.config.Colors.Default) .setTitle(`Online List - ${activePlayers.length} Player${(activePlayers.length>1||activePlayers.length==0)?'s':''} Online`) .setDescription(des); diff --git a/package-lock.json b/package-lock.json index 11c82e7..63af741 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,17 @@ { "name": "dayz-armbands", - "version": "5.7.0", + "version": "5.12.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "dayz-armbands", - "version": "5.7.0", + "version": "5.12.3", "license": "ISC", "dependencies": { "@discordjs/rest": "^1.1.0", "colors": "^1.4.0", + "concat-stream": "^2.0.0", "discord-bitfield-calculator": "^1.0.0", "discord.js": "^14.3.0", "dotenv": "^16.0.3", @@ -1307,6 +1308,11 @@ "ieee754": "^1.1.13" } }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, "node_modules/chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -1388,6 +1394,20 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -2171,6 +2191,11 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" + }, "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", @@ -3338,6 +3363,11 @@ "ieee754": "^1.1.13" } }, + "buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, "chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -3405,6 +3435,17 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, "debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -3966,6 +4007,11 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" + }, "undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", diff --git a/package.json b/package.json index 387efef..bb52b8e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dayz-armbands", - "version": "5.12.3", + "version": "6.0.0", "description": "A General Purpose Discord Bot for DayZ Servers.", "main": "index.js", "nodemonConfig": { @@ -19,6 +19,7 @@ "dependencies": { "@discordjs/rest": "^1.1.0", "colors": "^1.4.0", + "concat-stream": "^2.0.0", "discord-bitfield-calculator": "^1.0.0", "discord.js": "^14.3.0", "dotenv": "^16.0.3", diff --git a/structures/DayzArmbands.js b/structures/DayzArmbands.js index 676fba2..e4d1882 100644 --- a/structures/DayzArmbands.js +++ b/structures/DayzArmbands.js @@ -5,10 +5,13 @@ const { REST } = require('@discordjs/rest'); const Logger = require("../util/Logger"); const mongoose = require('mongoose'); +// custom util imports +const { DownloadNitradoFile } = require('../util/NitradoAPI'); +const { HandlePlayerLogs, HandleActivePlayersList } = require('../util/LogsHandler'); +const { HandleKillfeed } = require('../util/KillfeedHandler'); + const path = require("path"); const fs = require('fs'); -const { Readable } = require('stream'); -const { finished } = require('stream/promises'); const readline = require('readline'); const minute = 60000; // 1 minute in milliseconds @@ -103,409 +106,6 @@ class DayzArmbands extends Client { return n; } - async downloadFile(file, outputDir) { - const res = await fetch(`https://api.nitrado.net/services/${this.config.Nitrado.ServerID}/gameservers/file_server/download?file=${file}`, { - headers: { - "Authorization": this.config.Nitrado.Auth - } - }).then(response => - response.json().then(data => data) - ).then(res => res); - - const stream = fs.createWriteStream(outputDir); - const { body } = await fetch(res.data.token.url); - await finished(Readable.fromWeb(body).pipe(stream)); - } - - async handleBanList(gamertag) { - await this.downloadFile(`/games/${this.config.Nitrado.UserID}/noftp/dayzxb/ban.txt`, './logs/ban.txt').then(() => { - fs.appendFile('./logs/ban.txt', gamertag, (err) => { - if (err) throw err; - }); - }); - } - - async handleKillfeed(guildId, stats, line) { - - let guild = await this.GetGuild(guildId); - const channel = this.channels.cache.get(guild.killfeedChannel); - - let template = /(.*) \| Player \"(.*)\" \(DEAD\) \(id=(.*) pos=<(.*)>\)\[HP\: 0\] hit by Player \"(.*)\" \(id=(.*) pos=<(.*)>\) into (.*) for (.*) damage \((.*)\) with (.*) from (.*) meters /g; - let data = [...line.matchAll(template)][0]; - if (!data) return stats; - - let info = { - time: data[1], - victim: data[2], - victimID: data[3], - victimPOS: data[4], - killer: data[5], - killerID: data[6], - killerPOS: data[7], - bodyPart: data[8], - damage: data[9], - bullet: data[10], - weapon: data[11], - distance: data[12] - }; - - if (!this.exists(info.victim) || !this.exists(info.victimID) || !this.exists(info.killer) || !this.exists(info.killerID)) return stats; - - let killerStat = stats.find(stat => stat.playerID == info.killerID) - let victimStat = stats.find(stat => stat.playerID == info.victimID) - let killerStatIndex = stats.indexOf(killerStat); - let victimStatIndex = stats.indexOf(victimStat); - if (killerStat == undefined) killerStat = this.getDefaultPlayerStats(info.killer, info.killerID); - if (victimStat == undefined) victimStat = this.getDefaultPlayerStats(info.victim, info.victimID); - - killerStat.kills++; - killerStat.killStreak++; - killerStat.bestKillStreak = killerStat.killStreak > killerStat.bestKillStreak ? killerStat.killStreak : killerStat.bestKillStreak; - killerStat.longestKill = parseFloat(info.distance) > killerStat.longestKill ? parseFloat(info.distance).toFixed(1) : killerStat.longestKill; - victimStat.deaths++; - victimStat.deathStreak++; - victimStat.worstDeathStreak = victimStat.deathStreak > victimStat.worstDeathStreak ? victimStat.deathStreak : victimStat.bestKillStreak; - killerStat.KDR = killerStat.kills / (killerStat.deaths == 0 ? 1 : killerStat.deaths); // prevent division by 0 - victimStat.KDR = victimStat.kills / (victimStat.deaths == 0 ? 1 : victimStat.deaths); // prevent division by 0 - victimStat.killStreak = 0; - killerStat.deathStreak = 0; - - let receivedBounty = null; - if (victimStat.bounties.length > 0 && killerStat.discordID != "") { - let totalBounty = 0; - for (let i = 0; i < victimStat.bounties.length; i++) { - totalBounty += victimStat.bounties[i].value; - } - - let banking = await this.dbo.collection("users").findOne({"user.userID": killerStat.discordID}).then(banking => banking); - - if (!banking) { - banking = { - userID: killerStat.discordID, - guilds: { - [guildId]: { - bankAccount: { - balance: guild.startingBalance, - cash: 0.00, - } - } - } - } - - // Register inventory for user - let newBank = new User(); - newBank.createUser(killerStat.discordID, guildId, guild.startingBalance, 0); - newBank.save().catch(err => { - if (err) return this.sendInternalError(interaction, err); - }); - - } else banking = banking.user; - - if (!this.exists(banking.guilds[guildId])) { - const success = addUser(banking.guilds, guildId, killer.discordID, this, guild.startingBalance); - if (!success) return this.sendError(guild.connectionLogsChannel, 'Failed to add bank'); - } - - const newBalance = banking.guilds[guildId].bankAccount.balance + totalBounty; - - await this.dbo.collection("users").updateOne({ "user.userID": killerStat.discordID }, { - $set: { - [`user.guilds.${guildId}.bankAccount.balance`]: newBalance, - } - }, function(err, res) { - if (err) return this.sendError(guild.connectionLogsChannel, err); - }); - - receivedBounty = new EmbedBuilder() - .setColor(this.config.Colors.Default) - .setDescription(`<@${killerStat.discordID}> received **$${totalBounty.toFixed(2)}** in bounty rewards.`); - } - - if (killerStatIndex == -1) stats.push(killerStat); - else stats[killerStatIndex] = killerStat; - if (victimStatIndex == -1) stats.push(victimStat); - else stats[victimStatIndex] = victimStat; - - let newDt = await this.getDateEST(info.time); - let unixTime = Math.floor(newDt.getTime()/1000); - - const killEvent = new EmbedBuilder() - .setColor(this.config.Colors.Default) - .setDescription(`**Kill Event** - \n**${info.killer}** killed **${info.victim}**\n> **__Kill Data__**\n> **Weapon:** \` ${info.weapon} \`\n> **Distance:** \` ${info.distance} \`\n> **Body Part:** \` ${info.bodyPart.split('(')[0]} \`\n> **Damage:** \` ${info.damage} \`\n **Killer\n${killerStat.KDR.toFixed(2)} K/D - ${killerStat.kills} Kills - Killstreak: ${killerStat.killStreak}\nVictim\n${victimStat.KDR.toFixed(2)} K/D - ${victimStat.deaths} Deaths - Deathstreak: ${victimStat.deathStreak}**`); - - if (this.exists(channel)) await channel.send({ embeds: [killEvent] }); - if (this.exists(receivedBounty) && this.exists(channel)) await channel.send({ content: `<@${killerStat.discordID}>`, embeds: [receivedBounty] }); - - return stats; - } - - async handleAlarms(guildId, data) { - let guild = await this.GetGuild(guildId); - - if (!this.exists(guild.alarms)) { - guild.alarms = []; - await this.dbo.collection("guilds").updateOne({ 'server.serverID': guildId }, { - $set: { - 'server.alarms': [], - } - }, function (err, res) { - if (err) this.error(err); - }); - } - - for (let i = 0; i < guild.alarms.length; i++) { - let alarm = guild.alarms[i]; - if (alarm.disabled) continue; // ignore if alarm is disabled due to emp - if (alarm.ignoredPlayers.includes(data.playerID)) continue; - - let diff = [Math.round(alarm.origin[0] - data.pos[0]), Math.round(alarm.origin[1] - data.pos[1])]; - let distance = Math.sqrt(Math.pow(diff[0], 2) + Math.pow(diff[1], 2)).toFixed(2) - - if (distance < alarm.radius) { - const channel = this.channels.cache.get(alarm.channel); - - let newDt = await this.getDateEST(data.time); - let unixTime = Math.floor(newDt.getTime()/1000); - - // if (alarm.rules.includes['ban_on_entry']) { - - - - // let alarmEmbed = new EmbedBuilder() - // .setColor(this.config.Colors.Default) - // .setDescription(`**Zone Ping - **\n**${data.player}** was located within **${distance} meters** of the Zone **${alarm.name}** __and has been banned.__`) - // .addFields({ name: '**Location**', value: `**[${data.pos[0]}, ${data.pos[1]}](https://www.izurvive.com/chernarusplussatmap/#location=${data.pos[0]};${data.pos[1]})**`, inline: false }) - - // channel.send({ content: `<@&${alarm.role}>`, embeds: [alarmEmbed] }); - // } else { - let alarmEmbed = new EmbedBuilder() - .setColor(this.config.Colors.Default) - .setDescription(`**Zone Ping - **\n**${data.player}** was located within **${distance} meters** of the Zone **${alarm.name}**`) - .addFields({ name: '**Location**', value: `**[${data.pos[0]}, ${data.pos[1]}](https://www.izurvive.com/chernarusplussatmap/#location=${data.pos[0]};${data.pos[1]})**`, inline: false }) - - return await channel.send({ content: `<@&${alarm.role}>`, embeds: [alarmEmbed] }); - // } - } - } - } - - async sendConnectionLogs(guildId, data) { - - let guild = await this.GetGuild(guildId); - if (!this.exists(guild.connectionLogsChannel)) return; - const channel = this.channels.cache.get(guild.connectionLogsChannel); - - let newDt = await this.getDateEST(data.time); - let unixTime = Math.floor(newDt.getTime()/1000); - - let connectionLog = new EmbedBuilder() - .setColor(data.connected ? this.config.Colors.Green : this.config.Colors.Red) - .setDescription(`**${data.connected ? 'Connect' : 'Disconnect'} Event - \n${data.player} ${data.connected ? 'Connected' : 'Disconnected'}**`); - - if (!data.connected) { - if (!(data.lastConnectionDate == null)) { - let oldUnixTime = Math.floor(data.lastConnectionDate.getTime()/1000); - let sessionTime = this.secondsToDhms(unixTime - oldUnixTime); - connectionLog.addFields({ name: '**Session Time**', value: `**${sessionTime}**`, inline: false }); - } else connectionLog.addFields({ name: '**Session Time**', value: `**Unknown**`, inline: false }); - } - - if (this.exists(channel)) await channel.send({ embeds: [connectionLog] }); - } - - async detectCombatLog(guildId, data) { - if (data.lastDamageDate == null) return; - - let newDt = await this.getDateEST(data.time); - - let diffMs = (newDt - data.lastDamageDate) - let diffMins = Math.round(((diffMs % 86400000) % 3600000) / 60000); // minutes - - if (diffMins > 5) return; - - let guild = await this.GetGuild(guildId); - if (!this.exists(guild.connectionLogsChannel)) return; - const channel = this.channels.cache.get(guild.connectionLogsChannel); - if (!channel) return; - - let unixTime = Math.floor(newDt.getTime()/1000); - - let combatLog = new EmbedBuilder() - .setColor(this.config.Colors.Red) - .setDescription(`**NOTICE:**\n**${data.player}** has combat logged at when fighting **${data.lastHitBy}**`); - - if (client.exists(guild.adminRole)) channel.send({ content: `<@&${guild.adminRole}>` }); - - return channel.send({ embeds: [combatLog] }); - } - - async handlePlayerLogs(guildId, stats, line) { - - const connectTemplate = /(.*) \| Player \"(.*)\" is connected \(id=(.*)\)/g; - const disconnectTemplate = /(.*) \| Player \"(.*)\"\(id=(.*)\) has been disconnected/g; - const positionTemplate = /(.*) \| Player \"(.*)\" \(id=(.*) pos=<(.*)>\)/g; - const damageTemplate = /(.*) \| Player \"(.*)\" \(id=(.*) pos=<(.*)>\)\[HP\: (.*)\] hit by Player \"(.*)\" \(id=(.*) pos=<(.*)>\) into (.*) for (.*) damage \((.*)\) with (.*) from (.*) meters /g; - const deadTemplate = /(.*) \| Player \"(.*)\" \(DEAD\) \(id=(.*) pos=<(.*)>\)\[HP\: (.*)\] hit by Player \"(.*)\" \(id=(.*) pos=<(.*)>\) into (.*) for (.*) damage \((.*)\) with (.*) from (.*) meters /g; - - if (line.includes(' connected')) { - let data = [...line.matchAll(connectTemplate)][0]; - if (!data) return stats; - - let info = { - time: data[1], - player: data[2], - playerID: data[3], - connected: true, - }; - - if (!this.exists(info.player) || !this.exists(info.playerID)) return stats; - - let playerStat = stats.find(stat => stat.playerID == info.playerID) - let playerStatIndex = stats.indexOf(playerStat); - if (playerStat == undefined) playerStat = this.getDefaultPlayerStats(info.player, info.playerID); - - let newDt = await this.getDateEST(info.time); - - playerStat.lastConnectionDate = newDt; - playerStat.connected = true; - - if (playerStatIndex == -1) stats.push(playerStat); - else stats[playerStatIndex] = playerStat; - - this.sendConnectionLogs(guildId, { - time: info.time, - player: info.player, - connected: info.connected, - lastConnectionDate: null, - }); - } - - if (line.includes(' disconnected')) { - let data = [...line.matchAll(disconnectTemplate)][0]; - if (!data) return stats; - - let info = { - time: data[1], - player: data[2], - playerID: data[3], - connected: false - } - - if (!this.exists(info.player) || !this.exists(info.playerID)) return stats; - - let playerStat = stats.find(stat => stat.playerID == info.playerID) - let playerStatIndex = stats.indexOf(playerStat); - if (playerStat == undefined) playerStat = this.getDefaultPlayerStats(info.player, info.playerID); - - playerStat.connected = false; - - if (playerStatIndex == -1) stats.push(playerStat); - else stats[playerStatIndex] = playerStat; - - this.sendConnectionLogs(guildId, { - time: info.time, - player: info.player, - connected: info.connected, - lastConnectionDate: playerStat.lastConnectionDate, - }); - - this.detectCombatLog(guildId, { - time: info.time, - player: info.player, - lastDamageDate: playerStat.lastDamageDate, - lastHitBy: playerStat.lastHitBy, - }); - } - - if (line.includes('pos=<')) { - let data = [...line.matchAll(positionTemplate)][0]; - if (!data) return stats; - - let info = { - time: data[1], - player: data[2], - playerID: data[3], - pos: data[4].split(', ').map(v => parseFloat(v)) - }; - - if (!this.exists(info.player) || !this.exists(info.playerID)) return stats; - - let playerStat = stats.find(stat => stat.playerID == info.playerID) - let playerStatIndex = stats.indexOf(playerStat); - if (playerStat == undefined) playerStat = this.getDefaultPlayerStats(info.player, info.playerID); - - playerStat.lastPos = playerStat.pos; - playerStat.pos = info.pos; - playerStat.lastTime = playerStat.time; - playerStat.time = `${info.time} EST`; - - this.handleAlarms(guildId, { - time: info.time, - player: info.player, - playerID: info.playerID, - pos: info.pos, - }); - - if (playerStatIndex == -1) stats.push(playerStat); - else stats[playerStatIndex] = playerStat; - } - - if (line.includes('hit by Player')) { - let data = line.includes('(DEAD)') ? [...line.matchAll(deadTemplate)][0] : [...line.matchAll(damageTemplate)][0]; - if (!data) return stats; - - let info = { - time: data[1], - player: data[2], - playerID: data[3], - attacker: data[5], - attackerID: data[6] - } - - if (!this.exists(info.player) || !this.exists(info.playerID)) return stats; - - let playerStat = stats.find(stat => stat.playerID == info.playerID) - let playerStatIndex = stats.indexOf(playerStat); - if (playerStat == undefined) playerStat = this.getDefaultPlayerStats(info.player, info.playerID); - - let newDt = await this.getDateEST(info.time); - - playerStat.lastDamageDate = newDt; - playerStat.lastHitBy = info.attacker; - - if (playerStatIndex == -1) stats.push(playerStat); - else stats[playerStatIndex] = playerStat; - } - - return stats; - } - - async handleActivePlayersList(guildId) { - this.activePlayersTick = 0; // reset hour tick - - let guild = await this.GetGuild(guildId); - if (!this.exists(guild.playerstats)) guild.playerstats = []; - if (!this.exists(guild.activePlayersChannel)) return; - - const channel = this.channels.cache.get(guild.activePlayersChannel); - - let activePlayers = guild.playerstats.filter(p => p.connected); - - if (activePlayers.length == 0) return; - - let des = ``; - for (let i = 0; i < activePlayers.length; i++) { - des += `**- ${activePlayers[i].gamertag}**\n`; - } - - const activePlayersEmbed = new EmbedBuilder() - .setColor(this.config.Colors.Default) - .setTitle(`Online List - ${activePlayers.length} Player${activePlayers.length>1?'s':''} Online`) - .setDescription(des); - - return channel.send({ embeds: [activePlayersEmbed] }); - } - async readLogs(guildId) { const fileStream = fs.createReadStream('./logs/server-logs.ADM'); @@ -543,8 +143,8 @@ class DayzArmbands extends Client { } if (!inLog || !connected) s[j].connected = false; } - if (lines[i].includes('connected') || lines[i].includes('pos=<') || lines[1].includes['hit by Player']) s = await this.handlePlayerLogs(guildId, s, lines[i]); - if (!(i + 1 >= lines.length) && lines[i + 1].includes('killed by Player')) s = await this.handleKillfeed(guildId, s, lines[i]); + if (lines[i].includes('connected') || lines[i].includes('pos=<') || lines[1].includes['hit by Player']) s = await HandlePlayerLogs(this, guildId, s, lines[i]); + if (!(i + 1 >= lines.length) && lines[i + 1].includes('killed by Player')) s = await HandleKillfeed(this, guildId, s, lines[i]); } await this.dbo.collection("guilds").updateOne({ "server.serverID": guildId }, { @@ -566,9 +166,9 @@ class DayzArmbands extends Client { if (this.config.Dev != 'PROD.') console.log('---- tick ----'); this.activePlayersTick++; - await this.downloadFile(`/games/${this.config.Nitrado.UserID}/noftp/dayzxb/config/DayZServer_X1_x64.ADM`, './logs/server-logs.ADM').then(async () => { + await DownloadNitradoFile(`/games/${this.config.Nitrado.UserID}/noftp/dayzxb/config/DayZServer_X1_x64.ADM`, './logs/server-logs.ADM').then(async () => { await this.readLogs(this.config.GuildID).then(async () => { - if (this.activePlayersTick == 12) await this.handleActivePlayersList(this.config.GuildID); + if (this.activePlayersTick == 12) await HandleActivePlayersList(this.config.GuildID); }) }); this.logsUpdateTimer(); // restart this function diff --git a/util/AdminLogsHandler.js b/util/AdminLogsHandler.js new file mode 100644 index 0000000..7d36418 --- /dev/null +++ b/util/AdminLogsHandler.js @@ -0,0 +1,54 @@ +const { EmbedBuilder } = require('discord.js'); + +module.exports = { + + SendConnectionLogs: async (client, guildId, data) => { + + let guild = await client.GetGuild(guildId); + if (!client.exists(guild.connectionLogsChannel)) return; + const channel = client.channels.cache.get(guild.connectionLogsChannel); + + let newDt = await client.getDateEST(data.time); + let unixTime = Math.floor(newDt.getTime()/1000); + + let connectionLog = new EmbedBuilder() + .setColor(data.connected ? client.config.Colors.Green : client.config.Colors.Red) + .setDescription(`**${data.connected ? 'Connect' : 'Disconnect'} Event - \n${data.player} ${data.connected ? 'Connected' : 'Disconnected'}**`); + + if (!data.connected) { + if (!(data.lastConnectionDate == null)) { + let oldUnixTime = Math.floor(data.lastConnectionDate.getTime()/1000); + let sessionTime = client.secondsToDhms(unixTime - oldUnixTime); + connectionLog.addFields({ name: '**Session Time**', value: `**${sessionTime}**`, inline: false }); + } else connectionLog.addFields({ name: '**Session Time**', value: `**Unknown**`, inline: false }); + } + + if (client.exists(channel)) await channel.send({ embeds: [connectionLog] }); + }, + + DetectCombatLog: async (client, guildId, data) => { + if (data.lastDamageDate == null) return; + + let newDt = await client.getDateEST(data.time); + + let diffMs = (newDt - data.lastDamageDate) + let diffMins = Math.round(((diffMs % 86400000) % 3600000) / 60000); // minutes + + if (diffMins > 5) return; + + let guild = await client.GetGuild(guildId); + if (!client.exists(guild.connectionLogsChannel)) return; + const channel = client.channels.cache.get(guild.connectionLogsChannel); + if (!channel) return; + + let unixTime = Math.floor(newDt.getTime()/1000); + + let combatLog = new EmbedBuilder() + .setColor(client.config.Colors.Red) + .setDescription(`**NOTICE:**\n**${data.player}** has combat logged at when fighting **${data.lastHitBy}**`); + + if (client.exists(guild.adminRole)) channel.send({ content: `<@&${guild.adminRole}>` }); + + return channel.send({ embeds: [combatLog] }); + } +} \ No newline at end of file diff --git a/util/AlarmsHandler.js b/util/AlarmsHandler.js new file mode 100644 index 0000000..c9715d9 --- /dev/null +++ b/util/AlarmsHandler.js @@ -0,0 +1,54 @@ +const { HandlePlayerBan } = require('./NitradoAPI'); +const { EmbedBuilder } = require('discord.js'); + +module.exports = { + + HandleAlarms: async (client, guildId, data) => { + + let guild = await client.GetGuild(guildId); + + if (!client.exists(guild.alarms)) { + guild.alarms = []; + await client.dbo.collection("guilds").updateOne({ 'server.serverID': guildId }, { + $set: { + 'server.alarms': [], + } + }, function (err, res) { + if (err) client.error(err); + }); + } + + for (let i = 0; i < guild.alarms.length; i++) { + let alarm = guild.alarms[i]; + if (alarm.disabled) continue; // ignore if alarm is disabled due to emp + if (alarm.ignoredPlayers.includes(data.playerID)) continue; + + let diff = [Math.round(alarm.origin[0] - data.pos[0]), Math.round(alarm.origin[1] - data.pos[1])]; + let distance = Math.sqrt(Math.pow(diff[0], 2) + Math.pow(diff[1], 2)).toFixed(2) + + if (distance < alarm.radius) { + const channel = client.channels.cache.get(alarm.channel); + + let newDt = await client.getDateEST(data.time); + let unixTime = Math.floor(newDt.getTime()/1000); + + // if (alarm.rules.includes['ban_on_entry']) { + + // let alarmEmbed = new EmbedBuilder() + // .setColor(client.config.Colors.Default) + // .setDescription(`**Zone Ping - **\n**${data.player}** was located within **${distance} meters** of the Zone **${alarm.name}** __and has been banned.__`) + // .addFields({ name: '**Location**', value: `**[${data.pos[0]}, ${data.pos[1]}](https://www.izurvive.com/chernarusplussatmap/#location=${data.pos[0]};${data.pos[1]})**`, inline: false }) + + // channel.send({ content: `<@&${alarm.role}>`, embeds: [alarmEmbed] }); + // } else { + let alarmEmbed = new EmbedBuilder() + .setColor(client.config.Colors.Default) + .setDescription(`**Zone Ping - **\n**${data.player}** was located within **${distance} meters** of the Zone **${alarm.name}**`) + .addFields({ name: '**Location**', value: `**[${data.pos[0]}, ${data.pos[1]}](https://www.izurvive.com/chernarusplussatmap/#location=${data.pos[0]};${data.pos[1]})**`, inline: false }) + + return await channel.send({ content: `<@&${alarm.role}>`, embeds: [alarmEmbed] }); + // } + } + } + } +} \ No newline at end of file diff --git a/util/KillfeedHandler.js b/util/KillfeedHandler.js new file mode 100644 index 0000000..8784bfb --- /dev/null +++ b/util/KillfeedHandler.js @@ -0,0 +1,119 @@ +const { EmbedBuilder } = require('discord.js'); +const { User, addUser } = require('../structures/user'); + +module.exports = { + + HandleKillfeed: async (client, guildId, stats, line) => { + + let guild = await client.GetGuild(guildId); + const channel = client.channels.cache.get(guild.killfeedChannel); + + let template = /(.*) \| Player \"(.*)\" \(DEAD\) \(id=(.*) pos=<(.*)>\)\[HP\: 0\] hit by Player \"(.*)\" \(id=(.*) pos=<(.*)>\) into (.*) for (.*) damage \((.*)\) with (.*) from (.*) meters /g; + let data = [...line.matchAll(template)][0]; + if (!data) return stats; + + let info = { + time: data[1], + victim: data[2], + victimID: data[3], + victimPOS: data[4], + killer: data[5], + killerID: data[6], + killerPOS: data[7], + bodyPart: data[8], + damage: data[9], + bullet: data[10], + weapon: data[11], + distance: data[12] + }; + + if (!client.exists(info.victim) || !client.exists(info.victimID) || !client.exists(info.killer) || !client.exists(info.killerID)) return stats; + + let killerStat = stats.find(stat => stat.playerID == info.killerID) + let victimStat = stats.find(stat => stat.playerID == info.victimID) + let killerStatIndex = stats.indexOf(killerStat); + let victimStatIndex = stats.indexOf(victimStat); + if (killerStat == undefined) killerStat = client.getDefaultPlayerStats(info.killer, info.killerID); + if (victimStat == undefined) victimStat = client.getDefaultPlayerStats(info.victim, info.victimID); + + killerStat.kills++; + killerStat.killStreak++; + killerStat.bestKillStreak = killerStat.killStreak > killerStat.bestKillStreak ? killerStat.killStreak : killerStat.bestKillStreak; + killerStat.longestKill = parseFloat(info.distance) > killerStat.longestKill ? parseFloat(info.distance).toFixed(1) : killerStat.longestKill; + victimStat.deaths++; + victimStat.deathStreak++; + victimStat.worstDeathStreak = victimStat.deathStreak > victimStat.worstDeathStreak ? victimStat.deathStreak : victimStat.bestKillStreak; + killerStat.KDR = killerStat.kills / (killerStat.deaths == 0 ? 1 : killerStat.deaths); // prevent division by 0 + victimStat.KDR = victimStat.kills / (victimStat.deaths == 0 ? 1 : victimStat.deaths); // prevent division by 0 + victimStat.killStreak = 0; + killerStat.deathStreak = 0; + + let receivedBounty = null; + if (victimStat.bounties.length > 0 && killerStat.discordID != "") { + let totalBounty = 0; + for (let i = 0; i < victimStat.bounties.length; i++) { + totalBounty += victimStat.bounties[i].value; + } + + let banking = await client.dbo.collection("users").findOne({"user.userID": killerStat.discordID}).then(banking => banking); + + if (!banking) { + banking = { + userID: killerStat.discordID, + guilds: { + [guildId]: { + bankAccount: { + balance: guild.startingBalance, + cash: 0.00, + } + } + } + } + + // Register inventory for user + let newBank = new User(); + newBank.createUser(killerStat.discordID, guildId, guild.startingBalance, 0); + newBank.save().catch(err => { + if (err) return client.sendInternalError(interaction, err); + }); + + } else banking = banking.user; + + if (!client.exists(banking.guilds[guildId])) { + const success = addUser(banking.guilds, guildId, killer.discordID, this, guild.startingBalance); + if (!success) return client.sendError(guild.connectionLogsChannel, 'Failed to add bank'); + } + + const newBalance = banking.guilds[guildId].bankAccount.balance + totalBounty; + + await client.dbo.collection("users").updateOne({ "user.userID": killerStat.discordID }, { + $set: { + [`user.guilds.${guildId}.bankAccount.balance`]: newBalance, + } + }, function(err, res) { + if (err) return client.sendError(guild.connectionLogsChannel, err); + }); + + receivedBounty = new EmbedBuilder() + .setColor(client.config.Colors.Default) + .setDescription(`<@${killerStat.discordID}> received **$${totalBounty.toFixed(2)}** in bounty rewards.`); + } + + if (killerStatIndex == -1) stats.push(killerStat); + else stats[killerStatIndex] = killerStat; + if (victimStatIndex == -1) stats.push(victimStat); + else stats[victimStatIndex] = victimStat; + + let newDt = await client.getDateEST(info.time); + let unixTime = Math.floor(newDt.getTime()/1000); + + const killEvent = new EmbedBuilder() + .setColor(client.config.Colors.Default) + .setDescription(`**Kill Event** - \n**${info.killer}** killed **${info.victim}**\n> **__Kill Data__**\n> **Weapon:** \` ${info.weapon} \`\n> **Distance:** \` ${info.distance} \`\n> **Body Part:** \` ${info.bodyPart.split('(')[0]} \`\n> **Damage:** \` ${info.damage} \`\n **Killer\n${killerStat.KDR.toFixed(2)} K/D - ${killerStat.kills} Kills - Killstreak: ${killerStat.killStreak}\nVictim\n${victimStat.KDR.toFixed(2)} K/D - ${victimStat.deaths} Deaths - Deathstreak: ${victimStat.deathStreak}**`); + + if (client.exists(channel)) await channel.send({ embeds: [killEvent] }); + if (client.exists(receivedBounty) && client.exists(channel)) await channel.send({ content: `<@${killerStat.discordID}>`, embeds: [receivedBounty] }); + + return stats; + } +} \ No newline at end of file diff --git a/util/LogsHandler.js b/util/LogsHandler.js new file mode 100644 index 0000000..88be2c3 --- /dev/null +++ b/util/LogsHandler.js @@ -0,0 +1,173 @@ +const { EmbedBuilder } = require('discord.js'); +const { HandleAlarms } = require('./AlarmsHandler'); +const { SendConnectionLogs, DetectCombatLog } = require('./AdminLogsHandler'); + +module.exports = { + + HandlePlayerLogs: async (client, guildId, stats, line) => { + + const connectTemplate = /(.*) \| Player \"(.*)\" is connected \(id=(.*)\)/g; + const disconnectTemplate = /(.*) \| Player \"(.*)\"\(id=(.*)\) has been disconnected/g; + const positionTemplate = /(.*) \| Player \"(.*)\" \(id=(.*) pos=<(.*)>\)/g; + const damageTemplate = /(.*) \| Player \"(.*)\" \(id=(.*) pos=<(.*)>\)\[HP\: (.*)\] hit by Player \"(.*)\" \(id=(.*) pos=<(.*)>\) into (.*) for (.*) damage \((.*)\) with (.*) from (.*) meters /g; + const deadTemplate = /(.*) \| Player \"(.*)\" \(DEAD\) \(id=(.*) pos=<(.*)>\)\[HP\: (.*)\] hit by Player \"(.*)\" \(id=(.*) pos=<(.*)>\) into (.*) for (.*) damage \((.*)\) with (.*) from (.*) meters /g; + + if (line.includes(' connected')) { + let data = [...line.matchAll(connectTemplate)][0]; + if (!data) return stats; + + let info = { + time: data[1], + player: data[2], + playerID: data[3], + connected: true, + }; + + if (!client.exists(info.player) || !client.exists(info.playerID)) return stats; + + let playerStat = stats.find(stat => stat.playerID == info.playerID) + let playerStatIndex = stats.indexOf(playerStat); + if (playerStat == undefined) playerStat = client.getDefaultPlayerStats(info.player, info.playerID); + + let newDt = await client.getDateEST(info.time); + + playerStat.lastConnectionDate = newDt; + playerStat.connected = true; + + if (playerStatIndex == -1) stats.push(playerStat); + else stats[playerStatIndex] = playerStat; + + SendConnectionLogs(client, guildId, { + time: info.time, + player: info.player, + connected: info.connected, + lastConnectionDate: null, + }); + } + + if (line.includes(' disconnected')) { + let data = [...line.matchAll(disconnectTemplate)][0]; + if (!data) return stats; + + let info = { + time: data[1], + player: data[2], + playerID: data[3], + connected: false + } + + if (!client.exists(info.player) || !client.exists(info.playerID)) return stats; + + let playerStat = stats.find(stat => stat.playerID == info.playerID) + let playerStatIndex = stats.indexOf(playerStat); + if (playerStat == undefined) playerStat = client.getDefaultPlayerStats(info.player, info.playerID); + + playerStat.connected = false; + + if (playerStatIndex == -1) stats.push(playerStat); + else stats[playerStatIndex] = playerStat; + + SendConnectionLogs(client, guildId, { + time: info.time, + player: info.player, + connected: info.connected, + lastConnectionDate: playerStat.lastConnectionDate, + }); + + DetectCombatLog(client, guildId, { + time: info.time, + player: info.player, + lastDamageDate: playerStat.lastDamageDate, + lastHitBy: playerStat.lastHitBy, + }); + } + + if (line.includes('pos=<')) { + let data = [...line.matchAll(positionTemplate)][0]; + if (!data) return stats; + + let info = { + time: data[1], + player: data[2], + playerID: data[3], + pos: data[4].split(', ').map(v => parseFloat(v)) + }; + + if (!client.exists(info.player) || !client.exists(info.playerID)) return stats; + + let playerStat = stats.find(stat => stat.playerID == info.playerID) + let playerStatIndex = stats.indexOf(playerStat); + if (playerStat == undefined) playerStat = client.getDefaultPlayerStats(info.player, info.playerID); + + playerStat.lastPos = playerStat.pos; + playerStat.pos = info.pos; + playerStat.lastTime = playerStat.time; + playerStat.time = `${info.time} EST`; + + HandleAlarms(client, guildId, { + time: info.time, + player: info.player, + playerID: info.playerID, + pos: info.pos, + }); + + if (playerStatIndex == -1) stats.push(playerStat); + else stats[playerStatIndex] = playerStat; + } + + if (line.includes('hit by Player')) { + let data = line.includes('(DEAD)') ? [...line.matchAll(deadTemplate)][0] : [...line.matchAll(damageTemplate)][0]; + if (!data) return stats; + + let info = { + time: data[1], + player: data[2], + playerID: data[3], + attacker: data[5], + attackerID: data[6] + } + + if (!client.exists(info.player) || !client.exists(info.playerID)) return stats; + + let playerStat = stats.find(stat => stat.playerID == info.playerID) + let playerStatIndex = stats.indexOf(playerStat); + if (playerStat == undefined) playerStat = client.getDefaultPlayerStats(info.player, info.playerID); + + let newDt = await client.getDateEST(info.time); + + playerStat.lastDamageDate = newDt; + playerStat.lastHitBy = info.attacker; + + if (playerStatIndex == -1) stats.push(playerStat); + else stats[playerStatIndex] = playerStat; + } + + return stats; + }, + + HandleActivePlayersList: async (client, guildId) => { + client.activePlayersTick = 0; // reset hour tick + + let guild = await client.GetGuild(guildId); + if (!client.exists(guild.playerstats)) guild.playerstats = []; + if (!client.exists(guild.activePlayersChannel)) return; + + const channel = client.channels.cache.get(guild.activePlayersChannel); + + let activePlayers = guild.playerstats.filter(p => p.connected); + + if (activePlayers.length == 0) return; + + let des = ``; + for (let i = 0; i < activePlayers.length; i++) { + des += `**- ${activePlayers[i].gamertag}**\n`; + } + + const activePlayersEmbed = new EmbedBuilder() + .setColor(client.config.Colors.Default) + .setTitle(`Online List - ${activePlayers.length} Player${activePlayers.length>1?'s':''} Online`) + .setDescription(des); + + return channel.send({ embeds: [activePlayersEmbed] }); + } +} \ No newline at end of file diff --git a/util/NitradoAPI.js b/util/NitradoAPI.js new file mode 100644 index 0000000..190046c --- /dev/null +++ b/util/NitradoAPI.js @@ -0,0 +1,60 @@ +const { finished } = require('stream/promises'); +const concat = require('concat-stream'); +const { Readable } = require('stream'); +const FormData = require('form-data'); +const fs = require('fs'); + +module.exports = { + + DownloadNitradoFile: async (client, filename, outputDir) => { + const res = await fetch(`https://api.nitrado.net/services/${client.config.Nitrado.ServerID}/gameservers/file_server/download?file=${filename}`, { + headers: { + "Authorization": client.config.Nitrado.Auth + } + }).then(response => + response.json().then(data => data) + ).then(res => res); + + const stream = fs.createWriteStream(outputDir); + const { body } = await fetch(res.data.token.url); + await finished(Readable.fromWeb(body).pipe(stream)); + }, + + HandlePlayerBan: async (client, gamertag, ban) => { + // get current bans + const res = await fetch(`https://api.nitrado.net/services/${client.config.Nitrado.ServerID}/gameservers`, { + headers: { + "Authorization": client.config.Nitrado.Auth + } + }).then(response => + response.json().then(data => data) + ).then(res => res); + + let bans = res.data.gameserver.settings.general.bans; + if (ban) bans += `${gamertag}\r\n`; + if (!ban) bans = bans.replace(`${gamertag}\r\n`, ''); + if (ban != true && ban != false) { + client.error("Incorrect Ban Option: HandlePlayerBan") + throw "Incorrect Ban Option: HandlePlayerBan" + } + + const formData = new FormData(); + formData.append("category", "general"); + formData.append("key", "bans"); + formData.append("value", bans); + formData.pipe(concat(data => { + async function sendList() { + await fetch(`https://api.nitrado.net/services/${client.config.Nitrado.ServerID}/gameservers/settings`, { + method: "POST", + credentials: 'include', + headers: { + ...formData.getHeaders(), + "Authorization": client.config.Nitrado.Auth + }, + body: data, + }); + } + sendList(); + })); + } +} \ No newline at end of file diff --git a/util/RegisterSlashCommands.js b/util/RegisterSlashCommands.js index c432bc8..447e841 100644 --- a/util/RegisterSlashCommands.js +++ b/util/RegisterSlashCommands.js @@ -9,7 +9,7 @@ const { REST } = require('@discordjs/rest'); */ module.exports = { // Register guild commands - RegisterGuildCommands: async function(client, guild) { + RegisterGuildCommands: async (client, guild) => { const commands = []; const commandFiles = fs.readdirSync(path.join(__dirname, "..", "commands")).filter(file => file.endsWith('.js')); @@ -39,7 +39,7 @@ module.exports = { }, // Register global commands - RegisterGlobalCommands: async function(client) { + RegisterGlobalCommands: async (client) => { const commands = []; const commandFiles = fs.readdirSync(path.join(__dirname, "..", "commands")).filter(file => file.endsWith('.js'));