Skip to content
This repository has been archived by the owner on Jun 3, 2023. It is now read-only.

Commit

Permalink
Merge pull request #43 from Mirasaki/dev
Browse files Browse the repository at this point in the history
feat: support multiple servers
  • Loading branch information
Mirasaki authored Feb 2, 2023
2 parents 5c7fbb3 + 7ea9d2d commit 8b2f86c
Show file tree
Hide file tree
Showing 8 changed files with 190 additions and 78 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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) <https://discord.com/api/oauth2/authorize?client_id=CLIENT-ID&permissions=0&scope=bot%20applications.commands>
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) <https://discord.com/api/oauth2/authorize?client_id=CLIENT-ID&permissions=0&scope=bot%20applications.commands>
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

Expand Down
3 changes: 2 additions & 1 deletion config/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
6 changes: 6 additions & 0 deletions config/servers.example.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[
{
"CFTOOLS_SERVER_API_ID": "Your secret server API id",
"name": "Name to display"
}
]
43 changes: 41 additions & 2 deletions src/commands/dayz/leaderboard.js
Original file line number Diff line number Diff line change
@@ -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');

Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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();
Expand All @@ -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
Expand Down
161 changes: 100 additions & 61 deletions src/commands/dayz/stats.js
Original file line number Diff line number Diff line change
@@ -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 = {
Expand All @@ -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
}
]
},
Expand All @@ -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
Expand All @@ -56,64 +88,71 @@ 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 tryPlayerData = async (client, interaction, identifier) => {
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;

Expand All @@ -124,7 +163,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.`
Expand Down
12 changes: 10 additions & 2 deletions src/modules/autoLeaderboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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',
Expand Down
Loading

0 comments on commit 8b2f86c

Please sign in to comment.