From 6b54ef2eb218a4b21599b7347941830dc896a850 Mon Sep 17 00:00:00 2001 From: Mirasaki Date: Thu, 2 Feb 2023 12:50:06 +0100 Subject: [PATCH 1/8] add servers.json to gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 911a078..58d93a2 100755 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,9 @@ lerna-debug.log* src/commands/developer/deploy.js src/commands/developer/test.js +# Config/Credentials +config/servers.json + # Eslint output linter-output.txt From b32584350faf51e124724ef3ac636a67d6a96296 Mon Sep 17 00:00:00 2001 From: Mirasaki Date: Thu, 2 Feb 2023 12:50:20 +0100 Subject: [PATCH 2/8] update instructions to include multiple servers --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c669271..3b17d4a 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![GitHub forks](https://img.shields.io/github/forks/Mirasaki/dayz-leaderboard-bot?style=flat-square)](https://github.com/Mirasaki/dayz-leaderboard-bot/network) [![GitHub stars](https://img.shields.io/github/stars/Mirasaki/dayz-leaderboard-bot?style=flat-square)](https://github.com/Mirasaki/dayz-leaderboard-bot/stargazers) -A DayZ bot writting in Javascript to display your leaderboard using the CFTools Cloud API. +A DayZ bot written in Javascript to display your leaderboard using the CFTools Cloud API. ## Demo @@ -56,9 +56,10 @@ The people at [Custom DayZ Services](https://discord.gg/customdayzservices) were - `DISCORD_BOT_TOKEN`: After creating your bot on the link above, navigate to `Bot` in the left-side menu to reveal your bot-token - `CFTOOLS_API_APPLICATION_ID`: Application ID from your [CFTools Developer Apps](https://developer.cftools.cloud/applications) - Authorization has to be granted by navigating to the `Grant URL` that's displayed in your app overview - `CFTOOLS_API_SECRET`: Same as above, click `Reveal Secret` +7. Open the `config/servers.example.json` file and rename it to `servers.json`. Fill in your values. - `CFTOOLS_SERVER_API_ID`: Click `Manage Server` in your [CF Cloud Panel](https://app.cftools.cloud/dashboard) > `Settings` > `API Key` > `Server ID` -7. Add the bot to your server by using the following link: (Replace CLIENT-ID with your CLIENT_ID from before) -8. Run the command `node .` in the project root folder/directory or `npm run start` if you have [PM2](https://pm2.keymetrics.io/) installed to keep the process alive. +8. Add the bot to your server by using the following link: (Replace CLIENT-ID with your CLIENT_ID from before) +9. Run the command `node .` in the project root folder/directory or `npm run start` if you have [PM2](https://pm2.keymetrics.io/) installed to keep the process alive. ### FAQ From 3c51cd8989736049f3b9225f70cef2a49a395ec4 Mon Sep 17 00:00:00 2001 From: Mirasaki Date: Thu, 2 Feb 2023 12:50:26 +0100 Subject: [PATCH 3/8] remove CFTOOLS_SERVER_API_ID --- config/.env.example | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config/.env.example b/config/.env.example index b3bd551..a78ce07 100644 --- a/config/.env.example +++ b/config/.env.example @@ -5,6 +5,8 @@ CLIENT_ID= TEST_SERVER_GUILD_ID= # Automatic leaderboard posting +# ONLY supported for the first server entry +# when using multiple server setup AUTO_LB_ENABLED=true AUTO_LB_CHANNEL_ID= AUTO_LB_INTERVAL_IN_MINUTES=60 @@ -18,7 +20,6 @@ PORT= # CFTools Cloud API & GameLabs CFTOOLS_API_APPLICATION_ID= CFTOOLS_API_SECRET= -CFTOOLS_SERVER_API_ID= # Amount of players to display on the leaderboard. 10 min, 25 max CFTOOLS_API_PLAYER_DATA_COUNT=15 From dbe5ed2d843a958389ae72992d5a1c84472a0427 Mon Sep 17 00:00:00 2001 From: Mirasaki Date: Thu, 2 Feb 2023 12:50:39 +0100 Subject: [PATCH 4/8] Include example `servers.json` file --- config/servers.example.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 config/servers.example.json diff --git a/config/servers.example.json b/config/servers.example.json new file mode 100644 index 0000000..018b75f --- /dev/null +++ b/config/servers.example.json @@ -0,0 +1,6 @@ +[ + { + "CFTOOLS_SERVER_API_ID": "Your secret server API id", + "name": "Name to display" + } +] \ No newline at end of file From 71f9c74c12b95933afdb0be7d6760ce86039d2e4 Mon Sep 17 00:00:00 2001 From: Mirasaki Date: Thu, 2 Feb 2023 12:51:05 +0100 Subject: [PATCH 5/8] use multi server option in commands --- src/commands/dayz/leaderboard.js | 43 +++++++++++++++++++++++++++-- src/commands/dayz/stats.js | 46 +++++++++++++++++++++++++++----- 2 files changed, 80 insertions(+), 9 deletions(-) diff --git a/src/commands/dayz/leaderboard.js b/src/commands/dayz/leaderboard.js index 53941e7..d193878 100644 --- a/src/commands/dayz/leaderboard.js +++ b/src/commands/dayz/leaderboard.js @@ -1,9 +1,21 @@ const logger = require('@mirasaki/logger'); const { Statistic } = require('cftools-sdk'); const { stripIndents } = require('common-tags/lib'); -const cftClient = require('../../modules/cftClient'); +const cftClients = require('../../modules/cftClients'); const { parseSnakeCaseArray, colorResolver } = require('../../util'); +// Getting our servers config +const serverConfig = require('../../../config/servers.json') + .filter( + ({ CFTOOLS_SERVER_API_ID, name }) => + name !== '' + && CFTOOLS_SERVER_API_ID !== '' + ); + +// Mapping our API choices data +const serverConfigChoices = serverConfig + .map(({ CFTOOLS_SERVER_API_ID, name }) => ({ name, value: name })); + // Include our blacklist file const leaderboardBlacklist = require('../../../config/blacklist.json'); @@ -57,6 +69,13 @@ module.exports = { data: { description: 'Display your DayZ Leaderboard', options: [ + { + name: 'server', + description: 'Which leaderboard to display', + type: 3, // String + required: false, + choices: serverConfigChoices + }, { name: 'type', description: 'The type of leaderboard to display', @@ -85,6 +104,21 @@ module.exports = { const statToGet = options.getString('type') || 'OVERALL'; let mappedStat = statMap[statToGet]; + // Resolving server input + let serverName = options.getString('server'); + if (!serverName) serverName = serverConfig[0].name; + + // Getting the server api ID + const apiServerId = serverConfig.find(({ name }) => name === serverName)?.CFTOOLS_SERVER_API_ID; + + // Invalid config fallback + if (!apiServerId) { + interaction.reply({ + content: `${emojis.error} ${member}, invalid config in /config/servers.json - missing apiServerId for ${serverName}` + }); + return; + } + // Deferring our interaction // due to possible API latency await interaction.deferReply(); @@ -111,11 +145,16 @@ module.exports = { let res; try { // Fetching our leaderboard data from the CFTools API - res = await cftClient + res = await cftClients[serverName] .getLeaderboard({ order: 'ASC', statistic: mappedStat, limit: 100 + // serverApiId: apiServerId + // overwriting literally doesn't work + // Which is why we use the cftClients[serverName] approach + // Error: ResourceNotFound: https://data.cftools.cloud/v1/server/undefined/leaderboard?stat=kills&order=-1&limit=15 + // c'mon bro =( }); } catch (err) { // Properly logging the error if it is encountered diff --git a/src/commands/dayz/stats.js b/src/commands/dayz/stats.js index b6441ca..b67be6b 100644 --- a/src/commands/dayz/stats.js +++ b/src/commands/dayz/stats.js @@ -1,10 +1,19 @@ const logger = require('@mirasaki/logger'); const { stripIndents } = require('common-tags/lib'); -const { fetchPlayerDetails, getCftoolsId } = require('../../modules/cftClient'); +const { fetchPlayerDetails, getCftoolsId } = require('../../modules/cftClients'); const { colorResolver, titleCase } = require('../../util'); -// const steamIdRegex = /^[0-9]+$/g; - +// Getting our servers config +const serverConfig = require('../../../config/servers.json') + .filter( + ({ CFTOOLS_SERVER_API_ID, name }) => + name !== '' + && CFTOOLS_SERVER_API_ID !== '' + ); + +// Mapping our API choices data +const serverConfigChoices = serverConfig + .map(({ CFTOOLS_SERVER_API_ID, name }) => ({ name, value: name })); const { DEBUG_STAT_COMMAND_DATA } = process.env; module.exports = { @@ -16,6 +25,13 @@ module.exports = { name: 'identifier', description: 'The player\'s Steam64, CFTools Cloud, BattlEye, or Bohemia Interactive ID', required: true + }, + { + name: 'server', + description: 'Which leaderboard to display', + type: 3, // String + required: false, + choices: serverConfigChoices } ] }, @@ -29,15 +45,31 @@ module.exports = { run: async ({ client, interaction }) => { // Destructuring and assignments - const { options } = interaction; + const { options, member } = interaction; + const { emojis } = client.container; const identifier = options.getString('identifier'); // Deferring our reply await interaction.deferReply(); + // Resolving server input + let serverName = options.getString('server'); + if (!serverName) serverName = serverConfig[0].name; + + // Getting the server api ID + const apiServerId = serverConfig.find(({ name }) => name === serverName)?.CFTOOLS_SERVER_API_ID; + + // Invalid config fallback + if (!apiServerId) { + interaction.reply({ + content: `${emojis.error} ${member}, invalid config in /config/servers.json - missing apiServerId for ${serverName}` + }); + return; + } + // Reduce cognitive complexity // tryPlayerData replies to interaction if anything fails - const data = await tryPlayerData(client, interaction, identifier); + const data = await tryPlayerData(client, interaction, identifier, apiServerId); if (!data) return; // Data is delivered as on object with ID key parameters @@ -113,7 +145,7 @@ module.exports = { } }; -const tryPlayerData = async (client, interaction, identifier) => { +const tryPlayerData = async (client, interaction, identifier, CFTOOLS_SERVER_API_ID) => { const { emojis } = client.container; const { member } = interaction; @@ -124,7 +156,7 @@ const tryPlayerData = async (client, interaction, identifier) => { // fetching from API let data; try { - data = await fetchPlayerDetails(identifier); + data = await fetchPlayerDetails(identifier, CFTOOLS_SERVER_API_ID); } catch (err) { interaction.editReply({ content: `${emojis.error} ${member}, encountered an error while fetching data, please try again later.` From 8a70e1654527a86646f8aef1d0b7beb6d3133a96 Mon Sep 17 00:00:00 2001 From: Mirasaki Date: Thu, 2 Feb 2023 12:51:16 +0100 Subject: [PATCH 6/8] support autoLb only for first server entry --- src/modules/autoLeaderboard.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/modules/autoLeaderboard.js b/src/modules/autoLeaderboard.js index ef147e4..bc321c7 100644 --- a/src/modules/autoLeaderboard.js +++ b/src/modules/autoLeaderboard.js @@ -3,7 +3,15 @@ const chalk = require('chalk'); const logger = require('@mirasaki/logger'); const { buildLeaderboardEmbedMessages } = require('../commands/dayz/leaderboard'); const leaderboardBlacklist = require('../../config/blacklist.json'); -const cftClient = require('./cftClient'); +const cftClients = require('./cftClients'); + +// Getting our servers config +const serverConfig = require('../../config/servers.json') + .filter( + ({ CFTOOLS_SERVER_API_ID, name }) => + name !== '' + && CFTOOLS_SERVER_API_ID !== '' + ); // Definitions const MS_IN_TWO_WEEKS = 1000 * 60 * 60 * 24 * 14; @@ -50,7 +58,7 @@ const autoLbCycle = async (client, autoLbChannel) => { let res; try { // Fetching our leaderboard data from the CFTools API - res = await cftClient + res = await cftClients[serverConfig[0].name] .getLeaderboard({ order: 'ASC', statistic: 'kills', From d1f97d7951812ea1c7b106af700189e2b798881a Mon Sep 17 00:00:00 2001 From: Mirasaki Date: Thu, 2 Feb 2023 12:51:32 +0100 Subject: [PATCH 7/8] remove cftClient to include multi server support --- src/modules/{cftClient.js => cftClients.js} | 33 +++++++++++++++------ 1 file changed, 24 insertions(+), 9 deletions(-) rename src/modules/{cftClient.js => cftClients.js} (72%) diff --git a/src/modules/cftClient.js b/src/modules/cftClients.js similarity index 72% rename from src/modules/cftClient.js rename to src/modules/cftClients.js index 82a4152..201f15a 100644 --- a/src/modules/cftClient.js +++ b/src/modules/cftClients.js @@ -2,21 +2,36 @@ const logger = require('@mirasaki/logger'); const cftSDK = require('cftools-sdk'); const fetch = require('cross-fetch'); +// Initializing our clients object +const clients = {}; + // Destructure our environmental variables const { - CFTOOLS_SERVER_API_ID, CFTOOLS_API_SECRET, CFTOOLS_API_APPLICATION_ID } = process.env; -// export our CFTools client as unnamed default -module.exports = new cftSDK.CFToolsClientBuilder() - .withCache() - .withServerApiId(CFTOOLS_SERVER_API_ID) - .withCredentials(CFTOOLS_API_APPLICATION_ID, CFTOOLS_API_SECRET) - .build(); +// Getting our servers config +const serverConfig = require('../../config/servers.json') + .filter( + ({ CFTOOLS_SERVER_API_ID, name }) => + name !== '' + && CFTOOLS_SERVER_API_ID !== '' + ); + +// Creating a unique client for every entry +for (const { CFTOOLS_SERVER_API_ID, name } of serverConfig) { + clients[name] = new cftSDK.CFToolsClientBuilder() + .withCache() + .withServerApiId(CFTOOLS_SERVER_API_ID) + .withCredentials(CFTOOLS_API_APPLICATION_ID, CFTOOLS_API_SECRET) + .build(); +} + +// export our CFTools clients as unnamed default +module.exports = clients; -// Get API token, valid for 24 hours, dont export function +// Get API token, valid for 24 hours, don't export function const getAPIToken = async () => { // Getting our token let token = await fetch( @@ -47,7 +62,7 @@ module.exports.getAPIToken = async () => { return CFTOOLS_API_TOKEN; }; -const fetchPlayerDetails = async (cftoolsId) => { +const fetchPlayerDetails = async (cftoolsId, CFTOOLS_SERVER_API_ID = null) => { let data; try { data = await fetch( From 7ea9d2d1c7f0888d96b2917be2f266c57a6ddcf5 Mon Sep 17 00:00:00 2001 From: Mirasaki Date: Thu, 2 Feb 2023 12:55:18 +0100 Subject: [PATCH 8/8] `/stats` - reduce cognitive complexity --- src/commands/dayz/stats.js | 115 ++++++++++++++++++++----------------- 1 file changed, 61 insertions(+), 54 deletions(-) diff --git a/src/commands/dayz/stats.js b/src/commands/dayz/stats.js index b67be6b..92ddaef 100644 --- a/src/commands/dayz/stats.js +++ b/src/commands/dayz/stats.js @@ -88,63 +88,70 @@ module.exports = { logger.endLog('STAT COMMAND DATA'); } - // Assigning our stat variables - const { omega, game } = stats; - const { general } = game; - const daysPlayed = Math.floor(omega.playtime / 86400); - const hoursPlayed = Math.floor(omega.playtime / 3600) % 24; - const minutesPlayed = Math.floor(omega.playtime / 60) % 60; - const secondsPlayed = omega.playtime % 60; - const playSessions = omega.sessions; - const averagePlaytimePerSession = Math.round( - ((hoursPlayed * 60) - + minutesPlayed) - / playSessions - ); - const [ - day, month, date, year, time, timezone - ] = `${new Date(stats.updated_at)}`.split(' '); - let favoriteWeaponName = 'Knife'; - const highestKills = Object.entries(general?.weapons || {}).reduce((acc, [weaponName, weaponStats]) => { - const weaponKillsIsLower = acc > weaponStats.kills; - if (!weaponKillsIsLower) favoriteWeaponName = weaponName; - return weaponKillsIsLower ? acc : weaponStats.kills; - }, 0); - const cleanedWeaponName = titleCase(favoriteWeaponName.replace(/_/g, ' ')); - - // Reversing the name history array so the latest used name is the first item - omega.name_history.reverse(); - - // Returning our detailed player information - interaction.editReply({ - embeds: [ - { - color: colorResolver(), - title: `Stats for ${omega.name_history[0] || 'Survivor'}`, - description: stripIndents` - Survivor has played for ${daysPlayed} days, ${hoursPlayed} hours, ${minutesPlayed} minutes, and ${secondsPlayed} seconds - over ${playSessions} total sessions. - Bringing them to an average of ${!isNaN(averagePlaytimePerSession) ? averagePlaytimePerSession : 'n/a'} minutes per session. - - **Name History:** **\`${omega.name_history.slice(0, 10).join('`**, **`') || 'None'}\`** - - **Deaths:** ${general?.deaths || 0} - **Hits:** ${general?.hits || 0} - **KDRatio:** ${general?.kdratio || 0} - **Kills:** ${general?.kills || 0} - **Longest Kill:** ${general?.longest_kill || 0} m - **Longest Shot:** ${general?.longest_shot || 0} m - **Suicides:** ${general?.suicides || 0} - **Favorite Weapon:** ${cleanedWeaponName || 'Knife'} with ${highestKills || 0} kills - `, - footer: { - text: `Last action: ${time} | ${day} ${month} ${date} ${year} ${time} (${timezone})` - } - } - ] - }); + // Dedicated function for stat calculations + // and sending the result to reduce + // cognitive complexity + sendPlayerData(stats, interaction); } }; +const sendPlayerData = (stats, interaction) => { + // Assigning our stat variables + const { omega, game } = stats; + const { general } = game; + const daysPlayed = Math.floor(omega.playtime / 86400); + const hoursPlayed = Math.floor(omega.playtime / 3600) % 24; + const minutesPlayed = Math.floor(omega.playtime / 60) % 60; + const secondsPlayed = omega.playtime % 60; + const playSessions = omega.sessions; + const averagePlaytimePerSession = Math.round( + ((hoursPlayed * 60) + + minutesPlayed) + / playSessions + ); + const [ + day, month, date, year, time, timezone + ] = `${new Date(stats.updated_at)}`.split(' '); + let favoriteWeaponName = 'Knife'; + const highestKills = Object.entries(general?.weapons || {}).reduce((acc, [weaponName, weaponStats]) => { + const weaponKillsIsLower = acc > weaponStats.kills; + if (!weaponKillsIsLower) favoriteWeaponName = weaponName; + return weaponKillsIsLower ? acc : weaponStats.kills; + }, 0); + const cleanedWeaponName = titleCase(favoriteWeaponName.replace(/_/g, ' ')); + + // Reversing the name history array so the latest used name is the first item + omega.name_history.reverse(); + + // Returning our detailed player information + interaction.editReply({ + embeds: [ + { + color: colorResolver(), + title: `Stats for ${omega.name_history[0] || 'Survivor'}`, + description: stripIndents` + Survivor has played for ${daysPlayed} days, ${hoursPlayed} hours, ${minutesPlayed} minutes, and ${secondsPlayed} seconds - over ${playSessions} total sessions. + Bringing them to an average of ${!isNaN(averagePlaytimePerSession) ? averagePlaytimePerSession : 'n/a'} minutes per session. + + **Name History:** **\`${omega.name_history.slice(0, 10).join('`**, **`') || 'None'}\`** + + **Deaths:** ${general?.deaths || 0} + **Hits:** ${general?.hits || 0} + **KDRatio:** ${general?.kdratio || 0} + **Kills:** ${general?.kills || 0} + **Longest Kill:** ${general?.longest_kill || 0} m + **Longest Shot:** ${general?.longest_shot || 0} m + **Suicides:** ${general?.suicides || 0} + **Favorite Weapon:** ${cleanedWeaponName || 'Knife'} with ${highestKills || 0} kills + `, + footer: { + text: `Last action: ${time} | ${day} ${month} ${date} ${year} ${time} (${timezone})` + } + } + ] + }); +}; + const tryPlayerData = async (client, interaction, identifier, CFTOOLS_SERVER_API_ID) => { const { emojis } = client.container; const { member } = interaction;