From d53d96ff36f4a9023f0c133e64c6701617271d7f Mon Sep 17 00:00:00 2001 From: Razzmatazz Date: Sat, 13 Apr 2024 12:47:46 -0500 Subject: [PATCH 1/4] wip --- bot.mjs | 17 ++++++ commands/player.mjs | 121 ++++++++++++++++++++++++++++++++++++++++++ commands/quest.mjs | 4 +- modules/game-data.mjs | 29 ++++++++++ translations/en.json | 9 ++++ 5 files changed, 178 insertions(+), 2 deletions(-) create mode 100644 commands/player.mjs diff --git a/bot.mjs b/bot.mjs index c76740f..c8ee2da 100644 --- a/bot.mjs +++ b/bot.mjs @@ -79,6 +79,23 @@ discordClient.on('guildCreate', async guild => { discordClient.on('interactionCreate', async interaction => { interaction.start = new Date(); if (interaction.isAutocomplete()) { + if (interaction.commandName === 'player') { + const searchString = interaction.options.getString('account'); + if (!searchString || searchString.length < 3 || searchString > 15) { + return []; + } + const response = await fetch(`https://player.tarkov.dev/name/${searchString}`).then(r => r.json()); + return interaction.respond(response.map(result => { + return { + value: result.aid, + name: result.name, + } + })).catch(error => { + console.error(`Error responding to /${interaction.commandName} command autocomplete request on shard ${discordClient.shard.ids[0]}: ${error}`); + //console.error('interaction', interaction); + //console.error(error); + }); + } let options = await autocomplete(interaction); options = options.splice(0, 25); diff --git a/commands/player.mjs b/commands/player.mjs new file mode 100644 index 0000000..869521e --- /dev/null +++ b/commands/player.mjs @@ -0,0 +1,121 @@ +import { EmbedBuilder, SlashCommandBuilder } from 'discord.js'; + +import gameData from '../modules/game-data.mjs'; +import { getFixedT, getCommandLocalizations } from '../modules/translations.mjs'; +import progress from '../modules/progress-shard.mjs'; + +const getPlayerLevel = (exp, levels) => { + if (exp === 0) { + return 0; + } + let expTotal = 0; + for (let i = 0; i < levels.length; i++) { + const levelData = levels[i]; + expTotal += levelData.exp; + if (expTotal === exp) { + return levelData.level; + } + if (expTotal > exp) { + return levels[i - 1].level; + } + + } + return levels[levels.length-1].level; +}; + +const defaultFunction = { + data: new SlashCommandBuilder() + .setName('player') + .setDescription('Get player profile information') + .setNameLocalizations(getCommandLocalizations('player')) + .setDescriptionLocalizations(getCommandLocalizations('player_desc')) + .addStringOption(option => option + .setName('account') + .setDescription('Account to retrieve') + .setNameLocalizations(getCommandLocalizations('account')) + .setDescriptionLocalizations(getCommandLocalizations('account_seach_desc')) + .setAutocomplete(true) + .setRequired(true) + .setMinLength(3) + .setMaxLength(15) + ), + + async execute(interaction) { + await interaction.deferReply(); + const locale = await progress.getServerLanguage(interaction.guildId) || interaction.locale; + const t = getFixedT(locale); + const accountId = interaction.options.getString('account'); + if (isNaN(accountId)) { + return interaction.editReply({ + content: `❌ ${t('{{accountId}} is not a valid account id', {accountId})}` + }); + } + + const profile = await fetch(`https://player.tarkov.dev/account/${accountId}`).then(r => r.json()).catch(error => { + return { + err: error.message, + errmsg: error.message, + }; + }); + + if (profile.err) { + return interaction.editReply({ + content: `❌ ${t('Error retrieving account {{accountId}}: {{errorMessage}}', {accountId, errorMessage: profile.errmsg})}`, + }); + } + + const [playerLevels, items] = await Promise.all([ + gameData.playerLevels.getAll(), + gameData.items.getAll(locale), + ]); + + const playerLevel = getPlayerLevel(profile.info.experience, playerLevels); + + const dogtagIds = { + Usec: '59f32c3b86f77472a31742f0', + Bear: '59f32bb586f774757e1e8442' + }; + + const dogtagItem = items.find(i => i.id === dogtagIds[profile.info.side]); + + const embed = new EmbedBuilder(); + + // Construct the embed + embed.setTitle(`${profile.info.nickname} (${playerLevel} ${t(profile.info.side)})`); + embed.setThumbnail(dogtagItem.iconLink); + /*embed.setAuthor({ + name: trader.name, + iconURL: trader.imageLink, + url: `https://tarkov.dev/trader/${trader.normalizedName}`, + });*/ + embed.setURL(`https://tarkov.dev/player/${accountId}`); + const descriptionParts = [`${t('Started Wipe')}: ${new Date(profile.info.registrationDate * 1000).toLocaleString()}`]; + /*if (task.minPlayerLevel) { + descriptionParts.push(`${t('Minimum Level')}: ${task.minPlayerLevel}`); + }*/ + embed.setDescription(descriptionParts.join('\n')); + + /*embed.addFields( + { name: t('Objectives'), value: task.objectives.map(obj => `${obj.description}${obj.count > 1 ? ` (x${obj.count})` : ''}`).join('\n'), inline: false }, + ); + + const footerParts = [`${task.experience} EXP`]; + for (const repReward of task.finishRewards.traderStanding) { + const repTrader = traders.find(t => t.id === repReward.trader.id); + const sign = repReward.standing >= 0 ? '+' : ''; + footerParts.push(`${repTrader.name} ${sign}${repReward.standing}`); + } + + embed.setFooter({ text: footerParts.join(' | ') });*/ + + return interaction.editReply({ + embeds: [embed], + }); + }, + examples: [ + '/$t(player) Nikita', + '/$t(player) Prapor' + ] +}; + +export default defaultFunction; diff --git a/commands/quest.mjs b/commands/quest.mjs index 87d8a7a..67868fc 100644 --- a/commands/quest.mjs +++ b/commands/quest.mjs @@ -85,8 +85,8 @@ const defaultFunction = { }); }, examples: [ - '/$t(map) Woods', - '/$t(map) customs' + '/$t(quest) Debut', + '/$t(quest) Supplier' ] }; diff --git a/modules/game-data.mjs b/modules/game-data.mjs index fcbae65..b5820b9 100644 --- a/modules/game-data.mjs +++ b/modules/game-data.mjs @@ -16,6 +16,7 @@ const gameData = { items: false, itemNames: {}, tasks: {}, + playerLevels: [], flea: false, skills: [ { @@ -944,6 +945,29 @@ export async function getTasks(lang = 'en') { return updateTasks().then(ts => ts[lang]); }; +export async function updatePlayerLevels() { + const query = `query StashPlayerLevels { + playerLevels { + level + exp + } + }`; + gameData.playerLevels = await graphqlRequest({ graphql: query }).then(response => response.data.playerLevels); + + eventEmitter.emit('updatedTasks'); + return gameData.playerLevels; +}; + +export async function getPlayerLevels() { + if (process.env.IS_SHARD) { + return getParentReply({data: 'gameData', function: 'playerLevels.getAll'}); + } + if (gameData.playerLevels.length) { + return gameData.playerLevels; + } + return updatePlayerLevels(); +}; + export async function updateAll(rejectOnError = false) { try { await updateLanguages(); @@ -962,6 +986,7 @@ export async function updateAll(rejectOnError = false) { updateHideout(), updateItems(), updateTasks(), + updatePlayerLevels(), ]).then(results => { const taskNames = [ 'barters', @@ -972,6 +997,7 @@ export async function updateAll(rejectOnError = false) { 'hideout', 'items', 'tasks', + 'playerLevels', ]; let reject = false; results.forEach((result, index) => { @@ -1121,6 +1147,9 @@ export default { return tasks.find(task => task.id === id); }, }, + playerLevels: { + getAll: getPlayerLevels, + }, events: eventEmitter, updateAll: updateAll, validateLanguage, diff --git a/translations/en.json b/translations/en.json index ba46b02..041c915 100644 --- a/translations/en.json +++ b/translations/en.json @@ -7,6 +7,7 @@ "🔴 Poor": "🔴 Poor", "🛒 {{traderName}} restock in {{numMinutes}} minutes 🛒": "🛒 {{traderName}} restock in {{numMinutes}} minutes 🛒", "❌ ERROR ❌": "❌ ERROR ❌", + "{{accountId}} is not a valid account id": "{{accountId}} is not a valid account id", "{{thingName}} set to {{level}}.": "{{thingName}} set to {{level}}.", "A Dmg": "A Dmg", "About": "About", @@ -20,6 +21,7 @@ "An error has occurred": "An error has occurred", "Available Commands": "Available Commands", "Barter": "Barter", + "Bear": "BEAR", "Bosses": "Bosses", "Bugs? Missing features? Questions? Chat with us on Discord!": "Bugs? Missing features? Questions? Chat with us on Discord!", "Chance": "Chance", @@ -36,6 +38,7 @@ "Duration": "Duration", "EFT Discord Bot": "EFT Discord Bot", "Enjoy": "Enjoy", + "Error retrieving account {{accountId}}: {{errorMessage}}": "Error retrieving account {{accountId}}: {{errorMessage}}", "Escape from Tarkov Status": "Escape from Tarkov Status", "Escort": "Escort", "Examples": "Examples", @@ -103,6 +106,7 @@ "Spawn Chance": "Spawn Chance", "Spawn Locations": "Spawn Locations", "Special Loot": "Special Loot", + "Started Wipe": "Started Wipe", "Stash - An Escape from Tarkov Discord bot!": "Stash - An Escape from Tarkov Discord bot!", "Stash bot does not have access to #{{channelName}}.": "Stash bot does not have access to #{{channelName}}.", "Stash bot does not have permission to send messages in #{{channelName}}.": "Stash bot does not have permission to send messages in #{{channelName}}.", @@ -121,6 +125,7 @@ "Trader restocks": "Trader restocks", "Traders": "Traders", "Unknown": "Unknown", + "Usec": "USEC", "Value": "Value", "Velo": "Velo", "Want to check the status of our services (api, website, bot, etc)?": "Want to check the status of our services (api, website, bot, etc)?", @@ -140,6 +145,8 @@ "command": { "about": "about", "about_desc": "Tells you a bit about the bot", + "account": "account", + "account_search_desc": "Account to retrieve", "alert": "alert", "all_desc": "All", "ammo": "ammo", @@ -183,6 +190,8 @@ "none_desc": "None", "patchnotes": "patchnotes", "patchnotes_desc": "Get latest patch notes", + "player": "player", + "player_desc": "Get player profile information", "price": "price", "price_desc": "Get an item's flea and trader value", "progress": "progress", From 3afe7dd3ad9e4f56ce1e218c85a9766dbd0a2651 Mon Sep 17 00:00:00 2001 From: Razzmatazz Date: Tue, 30 Jul 2024 12:21:12 -0500 Subject: [PATCH 2/4] show stats and achievements --- .node-version | 2 +- bot.mjs | 20 +--- commands/ammo.mjs | 28 +++++- commands/player.mjs | 74 +++++++++++---- index.mjs | 5 +- modules/autocomplete.mjs | 8 ++ modules/game-data.mjs | 76 ++++++++++++++- modules/shard-messenger.mjs | 40 ++++++-- package-lock.json | 182 ++++++++++++++++++++---------------- package.json | 13 ++- translations/en.json | 13 +++ 11 files changed, 326 insertions(+), 135 deletions(-) diff --git a/.node-version b/.node-version index 72e4a48..8ce7030 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -18.14.2 +20.16.0 diff --git a/bot.mjs b/bot.mjs index 3f1e0bb..8b7d076 100644 --- a/bot.mjs +++ b/bot.mjs @@ -69,28 +69,14 @@ discordClient.on('guildCreate', async guild => { discordClient.on('interactionCreate', async interaction => { interaction.start = new Date(); if (interaction.isAutocomplete()) { - if (interaction.commandName === 'player') { - const searchString = interaction.options.getString('account'); - if (!searchString || searchString.length < 3 || searchString > 15) { - return []; - } - const response = await fetch(`https://player.tarkov.dev/name/${searchString}`).then(r => r.json()); - return interaction.respond(response.map(result => { - return { - value: result.aid, - name: result.name, - } - })).catch(error => { - console.error(`Error responding to /${interaction.commandName} command autocomplete request on shard ${discordClient.shard.ids[0]}: ${error}`); - //console.error('interaction', interaction); - //console.error(error); - }); - } let options = await autocomplete(interaction); options = options.splice(0, 25); await interaction.respond(options.map(name => { + if (typeof name === 'object' && name.name && name.value) { + return name; + } return { name: name, value: name, diff --git a/commands/ammo.mjs b/commands/ammo.mjs index d7bb8ac..558999f 100644 --- a/commands/ammo.mjs +++ b/commands/ammo.mjs @@ -117,8 +117,32 @@ const defaultFunction = { if (!caliberLabel) caliberLabel = caliber.replace('Caliber', ''); embed.setTitle(`${caliberLabel} ${t('Ammo Table')}`); + let maxNameLength = 11; if (ammos.length > 0) { - embed.setThumbnail(ammos[0].iconLink); + //embed.setThumbnail(ammos[0].iconLink); + embed.setThumbnail(ammos.reduce((currentThumb, ammo) => { + if (!currentThumb) { + currentThumb = ammo.iconLink; + } + if (ammo.name?.toLowerCase() === searchString.toLowerCase()) { + currentThumb = ammo.iconLink; + } + const longWidths = [ + ammo.properties.penetrationPower >= 100 ? 1 : 0, + ammo.properties.totalDamage >= 100 ? 1 : 0, + ammo.properties.armorDamage >= 100 ? 1 : 0, + ammo.properties.fragmentationChance >= 1 ? 1 : 0, + ammo.properties.initialSpeed >= 100 ? 1 : 0, + ].reduce((total, current) => { + return total + current; + }, 0); + if (longWidths > 2) { + // more than 2 numeric fields will be display with triple digits + // reduce the name field length to compensate + maxNameLength = maxNameLength - longWidths - 2; + } + return currentThumb; + }, null)); } for (const ammo of ammos) { @@ -126,7 +150,7 @@ const defaultFunction = { continue; } tableData.push([ - ammo.shortName.substring(0, 11), + ammo.shortName.substring(0, maxNameLength), ammo.properties.penetrationPower, ammo.properties.totalDamage, ammo.properties.armorDamage, diff --git a/commands/player.mjs b/commands/player.mjs index 869521e..3968c9c 100644 --- a/commands/player.mjs +++ b/commands/player.mjs @@ -1,4 +1,5 @@ import { EmbedBuilder, SlashCommandBuilder } from 'discord.js'; +import moment from 'moment'; import gameData from '../modules/game-data.mjs'; import { getFixedT, getCommandLocalizations } from '../modules/translations.mjs'; @@ -42,8 +43,8 @@ const defaultFunction = { async execute(interaction) { await interaction.deferReply(); - const locale = await progress.getServerLanguage(interaction.guildId) || interaction.locale; - const t = getFixedT(locale); + const { lang } = await progress.getInteractionSettings(interaction); + const t = getFixedT(lang); const accountId = interaction.options.getString('account'); if (isNaN(accountId)) { return interaction.editReply({ @@ -51,7 +52,7 @@ const defaultFunction = { }); } - const profile = await fetch(`https://player.tarkov.dev/account/${accountId}`).then(r => r.json()).catch(error => { + const profile = await fetch(`https://players.tarkov.dev/profile/${accountId}.json`).then(r => r.json()).catch(error => { return { err: error.message, errmsg: error.message, @@ -64,9 +65,10 @@ const defaultFunction = { }); } - const [playerLevels, items] = await Promise.all([ + const [playerLevels, items, achievements] = await Promise.all([ gameData.playerLevels.getAll(), - gameData.items.getAll(locale), + gameData.items.getAll(lang), + gameData.achievements.getAll(lang), ]); const playerLevel = getPlayerLevel(profile.info.experience, playerLevels); @@ -78,7 +80,10 @@ const defaultFunction = { const dogtagItem = items.find(i => i.id === dogtagIds[profile.info.side]); + const embeds = []; + const embed = new EmbedBuilder(); + embeds.push(embed); // Construct the embed embed.setTitle(`${profile.info.nickname} (${playerLevel} ${t(profile.info.side)})`); @@ -89,27 +94,62 @@ const defaultFunction = { url: `https://tarkov.dev/trader/${trader.normalizedName}`, });*/ embed.setURL(`https://tarkov.dev/player/${accountId}`); - const descriptionParts = [`${t('Started Wipe')}: ${new Date(profile.info.registrationDate * 1000).toLocaleString()}`]; + const descriptionParts = [`${t('Hours Played')}: ${Math.round(profile.pmcStats.eft.totalInGameTime / 60 / 60)}`]; /*if (task.minPlayerLevel) { descriptionParts.push(`${t('Minimum Level')}: ${task.minPlayerLevel}`); }*/ embed.setDescription(descriptionParts.join('\n')); + moment.locale(lang); + const footerText = t('Updated {{updateTimeAgo}}', {updateTimeAgo: moment(new Date(profile.updated)).fromNow()}); - /*embed.addFields( - { name: t('Objectives'), value: task.objectives.map(obj => `${obj.description}${obj.count > 1 ? ` (x${obj.count})` : ''}`).join('\n'), inline: false }, - ); - - const footerParts = [`${task.experience} EXP`]; - for (const repReward of task.finishRewards.traderStanding) { - const repTrader = traders.find(t => t.id === repReward.trader.id); - const sign = repReward.standing >= 0 ? '+' : ''; - footerParts.push(`${repTrader.name} ${sign}${repReward.standing}`); + const statTypes = { + pmc: 'PMC', + scav: 'Scav', + }; + for (const statType in statTypes) { + const sideLabel = statTypes[statType]; + const raidCount = profile[`${statType}Stats`].eft.overAllCounters.Items?.find(i => i.Key.includes('Sessions'))?.Value ?? 0 + const raidsSurvived = profile[`${statType}Stats`].eft.overAllCounters.Items?.find(i => i.Key.includes('Survived'))?.Value ?? 0; + const raidsDied = profile[`${statType}Stats`].eft.overAllCounters.Items?.find(i => i.Key.includes('Killed'))?.Value ?? 0; + const raidSurvivalRatio = raidCount > 0 ? raidsSurvived / raidCount : 0; + const raidDiedRatio = raidCount > 0 ? raidsDied / raidCount : 0; + const kills = profile[`${statType}Stats`].eft.overAllCounters.Items?.find(i => i.Key.includes('Kills'))?.Value ?? 0; + const kdr = raidsDied > 0 ? (kills / raidsDied).toFixed(2) : '∞'; + const survivalStreak = profile[`${statType}Stats`].eft.overAllCounters.Items?.find(i => i.Key.includes('LongestWinStreak'))?.Value ?? 0; + const fieldValue = `${t('Survive Rate')}: ${raidSurvivalRatio.toFixed(2)} (${raidsSurvived}/${raidCount}) + ${t('Death Rate')}: ${raidDiedRatio.toFixed(2)} (${raidsDied}/${raidCount}) + ${t('K:D', {nsSeparator: '|'})}: ${kdr} (${kills}/${raidsDied}) + ${t('Longest Survival Streak')}: ${survivalStreak}`; + embed.addFields( + { name: t('{{side}} Stats', {side: t(sideLabel)}), value: fieldValue, inline: true }, + ); } - embed.setFooter({ text: footerParts.join(' | ') });*/ + const completedAchievements = []; + for (const achievementId in profile.achievements) { + const achievement = achievements.find(a => a.id === achievementId); + if (!achievement) { + return; + } + completedAchievements.push({...achievement, completed: profile.achievements[achievementId]}); + } + if (completedAchievements.length > 0) { + const achievementsEmbed = new EmbedBuilder(); + embeds.push(achievementsEmbed); + achievementsEmbed.setTitle(t('Achievements')); + for (const achievement of completedAchievements) { + const completed = new Date(achievement.completed * 1000); + achievementsEmbed.addFields( + { name: achievement.name, value: completed.toLocaleString(lang), inline: true }, + ); + } + achievementsEmbed.setFooter({text: footerText}); + } else { + embed.setFooter({text: footerText}); + } return interaction.editReply({ - embeds: [embed], + embeds, }); }, examples: [ diff --git a/index.mjs b/index.mjs index 852f365..2c8fdab 100644 --- a/index.mjs +++ b/index.mjs @@ -1,7 +1,7 @@ import { fork } from 'child_process'; import got from 'got'; -import cron from 'cron'; +import { CronJob } from 'cron'; import { ShardingManager } from 'discord.js'; import progress from './modules/progress.mjs'; @@ -45,7 +45,7 @@ manager.spawn().then(shards => { if (process.env.NODE_ENV === 'production') { // A healthcheck cron to send a GET request to our status server // The cron schedule is expressed in seconds for the first value - healthcheckJob = new cron.CronJob('*/45 * * * * *', () => { + healthcheckJob = new CronJob('*/45 * * * * *', () => { got( `https://status.tarkov.dev/api/push/${process.env.HEALTH_ENDPOINT}?msg=OK`, { @@ -105,6 +105,7 @@ gameData.updateAll().then(() => { } }); }); +gameData.updateProfileIndex(); process.on('uncaughtException', (error) => { try { diff --git a/modules/autocomplete.mjs b/modules/autocomplete.mjs index b941d8d..e0abd71 100644 --- a/modules/autocomplete.mjs +++ b/modules/autocomplete.mjs @@ -52,6 +52,14 @@ async function autocomplete(interaction) { console.error(getError); } let cacheKey = interaction.commandName; + if (cacheKey === 'player') { + const nameResults = await gameData.profiles.search(interaction.options.getString('account')); + const names = []; + for (const id in nameResults) { + names.push({name: nameResults[id], value: id}); + } + return names; + } if (cacheKey === 'progress' && interaction.options.getSubcommand() === 'hideout') { cacheKey = interaction.options.getSubcommand(); searchString = interaction.options.getString('station'); diff --git a/modules/game-data.mjs b/modules/game-data.mjs index 645a379..9061b2f 100644 --- a/modules/game-data.mjs +++ b/modules/game-data.mjs @@ -35,6 +35,7 @@ const gameData = { playerLevels: [], flea: {}, goonReports: {}, + achievements: {}, skills: [ { id: 'hideoutManagement', @@ -63,8 +64,11 @@ const gameData = { 'tr', 'zh', ], + profiles: {}, }; +let profileIndexUpdateInterval = false; + const mapKeys = { '5b0fc42d86f7744a585f9105': 'labs', '59fc81d786f774390775787e': 'factory' @@ -994,7 +998,7 @@ export async function updatePlayerLevels() { }`; gameData.playerLevels = await graphqlRequest({ graphql: query }).then(response => response.data.playerLevels); - eventEmitter.emit('updatedTasks'); + eventEmitter.emit('updatedPlayerLevels'); return gameData.playerLevels; }; @@ -1008,6 +1012,42 @@ export async function getPlayerLevels() { return updatePlayerLevels(); }; +export async function updateAchievements() { + const achievementQueries = []; + for (const langCode of gameData.languages) { + achievementQueries.push(`${langCode}: achievements(lang: ${langCode}) { + ...AchievementFields + }`); + } + const query = `query StashAchievements { + ${achievementQueries.join('\n')} + } + fragment AchievementFields on Achievement { + id + name + adjustedPlayersCompletedPercent + }`; + const response = await graphqlRequest({ graphql: query }).then(response => response.data); + for (const lang in response) { + gameData.achievements[lang] = response[lang]; + } + + eventEmitter.emit('updatedAchievements'); + return gameData.achievements; +}; + +export async function getAchievements(options = defaultOptions) { + if (process.env.IS_SHARD) { + return getParentReply({data: 'gameData', function: 'achievements.getAll', args: options}); + } + let { lang } = mergeOptions(options); + lang = validateLanguage(lang); + if (gameData.achievements[lang]) { + return gameData.achievements[lang]; + } + return updateAchievements().then(as => as[lang]); +}; + export async function updateAll(rejectOnError = false) { try { await updateLanguages(); @@ -1028,6 +1068,7 @@ export async function updateAll(rejectOnError = false) { updateTasks(), updateGoonReports(), updatePlayerLevels(), + updateAchievements(), ]).then(results => { const taskNames = [ 'barters', @@ -1040,6 +1081,7 @@ export async function updateAll(rejectOnError = false) { 'tasks', 'goonReports', 'playerLevels', + 'achievements', ]; let reject = false; results.forEach((result, index) => { @@ -1215,9 +1257,41 @@ const gameDataExport = { playerLevels: { getAll: getPlayerLevels, }, + achievements: { + getAll: getAchievements, + }, + profiles: { + search: async (name) => { + if (process.env.IS_SHARD) { + return getParentReply({data: 'gameData', function: 'profiles.search', args: [name]}); + } + if (!name || name.length < 3) { + return []; + } + const nameLower = name.toLowerCase(); + const results = {}; + for (const id in gameData.profiles) { + const playerName = gameData.profiles[id]; + if (!playerName.toLowerCase().includes(nameLower)) { + continue; + } + results[id] = playerName; + } + return results; + }, + }, events: eventEmitter, updateAll: updateAll, validateLanguage, + updateProfileIndex: async () => { + const response = await fetch('https://players.tarkov.dev/profile/index.json'); + gameData.profiles = await response.json(); + console.log(`Retrieved player profile index of ${Object.keys(gameData.profiles).length} profiles`); + if (!profileIndexUpdateInterval) { + profileIndexUpdateInterval = setInterval(gameDataExport.updateProfileIndex, 1000 * 60 * 60 * 24); + profileIndexUpdateInterval.unref(); + } + } }; export default gameDataExport; diff --git a/modules/shard-messenger.mjs b/modules/shard-messenger.mjs index b424fb7..f04b508 100644 --- a/modules/shard-messenger.mjs +++ b/modules/shard-messenger.mjs @@ -1,4 +1,4 @@ -import { v4 as uuidv4 } from "uuid"; +import crypto from 'node:crypto'; import gameData from './game-data.mjs'; import progress from './progress.mjs'; @@ -11,14 +11,25 @@ export const getShardReply = async(shardId, message) => { if (process.env.IS_SHARD) { return Promise.reject(new Error('getShardReply can only be called by the parent process')); } - message.uuid = uuidv4(); + message.uuid = crypto.randomUUID(); message.type = 'getReply'; return new Promise((resolve, reject) => { - shardingManager.shards.get(shardId).once(message.uuid, response => { + const responseFunction = (response) => { if (response.error) return reject(response.error); resolve(response.data); - }); - shardingManager.shards.get(shardId).send(message); + }; + shardingManager.shards.get(shardId).once(message.uuid, responseFunction); + try { + shardingManager.shards.get(shardId).send(message).catch((error) => { + shardingManager.shards.get(shardId).off(message.uuid, responseFunction); + console.log(`Error sending message to shard ${shardId}`, message, error); + reject(error); + }); + } catch (error) { + shardingManager.shards.get(shardId).off(message.uuid, responseFunction); + console.log(`Error sending message to shard ${shardId}`, message, error); + reject(error); + } }); }; @@ -182,14 +193,25 @@ export const getParentReply = async (message) => { if (!process.env.IS_SHARD) { return Promise.reject(new Error('getParentReply can only be called by a shard')); } - message.uuid = uuidv4(); + message.uuid = crypto.randomUUID(); message.type = 'getReply'; return new Promise((resolve, reject) => { - process.once(message.uuid, response => { + const responseFunction = response => { if (response.error) return reject(response.error); resolve(response.data); - }); - discordClient.shard.send(message); + }; + process.once(message.uuid, responseFunction); + try { + discordClient.shard.send(message).catch((error) => { + process.off(message.uuid, responseFunction); + console.log('Error sending message to shard mananger', message, error); + reject(error); + }); + } catch (error) { + process.off(message.uuid, responseFunction); + console.log('Error sending message to shard mananger', message, error); + reject(error); + } }); }; diff --git a/package-lock.json b/package-lock.json index 5d709e1..78e9e77 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,23 +12,22 @@ "@discordjs/rest": "^2.3.0", "ascii-table": "^0.0.9", "cheerio": "^1.0.0-rc.12", - "cron": "^2.4.0", + "cron": "^3.1.7", "cross-env": "^7.0.3", - "discord-api-types": "^0.37.91", + "discord-api-types": "^0.37.93", "discord.js": "^14.15.3", "dotenv": "^16.4.5", - "got": "^13.0.0", - "i18next": "^23.11.5", + "got": "^14.4.2", + "i18next": "^23.12.2", "moment": "^2.30.1", "nodemon": "^3.1.4", - "turndown": "^7.2.0", - "uuid": "^10.0.0" + "turndown": "^7.2.0" }, "devDependencies": { "@railway/cli": "^3.11.0" }, "engines": { - "node": "18.*" + "node": "20.*" } }, "node_modules/@babel/runtime": { @@ -221,12 +220,17 @@ "npm": ">=7.0.0" } }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==" + }, "node_modules/@sindresorhus/is": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.3.0.tgz", - "integrity": "sha512-CX6t4SYQ37lzxicAqsBtxA3OseeoVrh9cSJ5PFYam0GksYlupRfy1A+Q4aYD3zvcfECLc0zO2u+ZnR2UYKvCrw==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.0.0.tgz", + "integrity": "sha512-WDTlVTyvFivSOuyvMeedzg2hdoBLZ3f1uNVuEida2Rl9BrfjrIRjWA/VZIrMRLvSwJYCAlCRA3usDt1THytxWQ==", "engines": { - "node": ">=14.16" + "node": ">=18" }, "funding": { "url": "https://github.com/sindresorhus/is?sponsor=1" @@ -244,9 +248,14 @@ } }, "node_modules/@types/http-cache-semantics": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz", - "integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==" + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", + "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==" + }, + "node_modules/@types/luxon": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.4.2.tgz", + "integrity": "sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==" }, "node_modules/@types/node": { "version": "20.14.9", @@ -342,20 +351,20 @@ } }, "node_modules/cacheable-request": { - "version": "10.2.9", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.9.tgz", - "integrity": "sha512-CaAMr53AS1Tb9evO1BIWFnZjSr8A4pbXofpsNVWPMDZZj3ZQKHwsQG9BrTqQ4x5ZYJXz1T2b8LLtTZODxSpzbg==", + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-12.0.1.tgz", + "integrity": "sha512-Yo9wGIQUaAfIbk+qY0X4cDQgCosecfBe3V9NSyeY4qPC2SAkbCS4Xj79VP8WOzitpJUZKc/wsRCYF5ariDIwkg==", "dependencies": { - "@types/http-cache-semantics": "^4.0.1", - "get-stream": "^6.0.1", + "@types/http-cache-semantics": "^4.0.4", + "get-stream": "^9.0.1", "http-cache-semantics": "^4.1.1", - "keyv": "^4.5.2", + "keyv": "^4.5.4", "mimic-response": "^4.0.0", - "normalize-url": "^8.0.0", + "normalize-url": "^8.0.1", "responselike": "^3.0.0" }, "engines": { - "node": ">=14.16" + "node": ">=18" } }, "node_modules/cheerio": { @@ -435,11 +444,12 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, "node_modules/cron": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/cron/-/cron-2.4.0.tgz", - "integrity": "sha512-Cx77ic1TyIAtUggr0oAhtS8MLzPBUqGNIvdDM7jE3oFIxfe8LXWI9q3iQN/H2CebAiMir53LQKWOhEKnzkJTAQ==", + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/cron/-/cron-3.1.7.tgz", + "integrity": "sha512-tlBg7ARsAMQLzgwqVxy8AZl/qlTc5nibqYwtNGoCrd+cV+ugI+tvZC1oT/8dFH8W455YrywGykx/KMmAqOr7Jw==", "dependencies": { - "luxon": "^3.2.1" + "@types/luxon": "~3.4.0", + "luxon": "~3.4.0" } }, "node_modules/cross-env": { @@ -557,9 +567,9 @@ } }, "node_modules/discord-api-types": { - "version": "0.37.91", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.91.tgz", - "integrity": "sha512-yPGOP1SlkRtnT9Vor5noZHNm7t+EpRdaDvEoeqWTSCzeenMWyB1RbOGKYxdZ1OZijji51Yd1yodhOTi9CfrrQg==" + "version": "0.37.93", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.93.tgz", + "integrity": "sha512-M5jn0x3bcXk8EI2c6F6V6LeOWq10B/cJf+YJSyqNmg7z4bdXK+Z7g9zGJwHS0h9Bfgs0nun2LQISFOzwck7G9A==" }, "node_modules/discord.js": { "version": "14.15.3", @@ -712,11 +722,11 @@ } }, "node_modules/form-data-encoder": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz", - "integrity": "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-4.0.2.tgz", + "integrity": "sha512-KQVhvhK8ZkWzxKxOr56CPulAhH3dobtuQ4+hNQ+HekH/Wp5gSOafqRAeTphQUJAIk0GBvHZgJ2ZGRWd5kphMuw==", "engines": { - "node": ">= 14.17" + "node": ">= 18" } }, "node_modules/formdata-polyfill": { @@ -769,11 +779,15 @@ } }, "node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -791,24 +805,24 @@ } }, "node_modules/got": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/got/-/got-13.0.0.tgz", - "integrity": "sha512-XfBk1CxOOScDcMr9O1yKkNaQyy865NbYs+F7dr4H0LZMVgCj2Le59k6PqbNHoL5ToeaEQUYh6c6yMfVcc6SJxA==", + "version": "14.4.2", + "resolved": "https://registry.npmjs.org/got/-/got-14.4.2.tgz", + "integrity": "sha512-+Te/qEZ6hr7i+f0FNgXx/6WQteSM/QqueGvxeYQQFm0GDfoxLVJ/oiwUKYMTeioColWUTdewZ06hmrBjw6F7tw==", "dependencies": { - "@sindresorhus/is": "^5.2.0", + "@sindresorhus/is": "^7.0.0", "@szmarczak/http-timer": "^5.0.1", "cacheable-lookup": "^7.0.0", - "cacheable-request": "^10.2.8", + "cacheable-request": "^12.0.1", "decompress-response": "^6.0.0", - "form-data-encoder": "^2.1.2", - "get-stream": "^6.0.1", - "http2-wrapper": "^2.1.10", + "form-data-encoder": "^4.0.2", + "http2-wrapper": "^2.2.1", "lowercase-keys": "^3.0.0", - "p-cancelable": "^3.0.0", - "responselike": "^3.0.0" + "p-cancelable": "^4.0.1", + "responselike": "^3.0.0", + "type-fest": "^4.19.0" }, "engines": { - "node": ">=16" + "node": ">=20" }, "funding": { "url": "https://github.com/sindresorhus/got?sponsor=1" @@ -846,9 +860,9 @@ "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==" }, "node_modules/http2-wrapper": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.0.tgz", - "integrity": "sha512-kZB0wxMo0sh1PehyjJUWRFEd99KC5TLjZ2cULC4f9iqJBAmKQQXEICjxl5iPJRwP40dpeHFqqhm7tYCvODpqpQ==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz", + "integrity": "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==", "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.2.0" @@ -858,9 +872,9 @@ } }, "node_modules/i18next": { - "version": "23.11.5", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.11.5.tgz", - "integrity": "sha512-41pvpVbW9rhZPk5xjCX2TPJi2861LEig/YRhUkY+1FQ2IQPS0bKUDYnEqY8XPPbB48h1uIwLnP9iiEfuSl20CA==", + "version": "23.12.2", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.12.2.tgz", + "integrity": "sha512-XIeh5V+bi8SJSWGL3jqbTEBW5oD6rbP5L+E7dVQh1MNTxxYef0x15rhJVcRb7oiuq4jLtgy2SD8eFlf6P2cmqg==", "funding": [ { "type": "individual", @@ -922,6 +936,17 @@ "node": ">=0.12.0" } }, + "node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -933,9 +958,9 @@ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" }, "node_modules/keyv": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.2.tgz", - "integrity": "sha512-5MHbFaKn8cNSmVW7BYnijeAVlE4cYA/SVkifVgrh7yotnfhKmjuXpDKjrABLnT0SfHWV21P8ow07OGfRrNDg8g==", + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dependencies": { "json-buffer": "3.0.1" } @@ -973,9 +998,9 @@ } }, "node_modules/luxon": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.3.0.tgz", - "integrity": "sha512-An0UCfG/rSiqtAIiBPO0Y9/zAnHUZxAMiCpTd5h2smgsj7GGmcenvrvww2cqNA8/4A5ZrD1gJpHN2mIHZQF+Mg==", + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", + "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==", "engines": { "node": ">=12" } @@ -1153,9 +1178,9 @@ } }, "node_modules/normalize-url": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.0.tgz", - "integrity": "sha512-uVFpKhj5MheNBJRTiMZ9pE/7hD1QTeEvugSJW/OmLzAp78PB5O6adfMNTvmfKhXBkvCzC+rqifWcVYpGFwTjnw==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.1.tgz", + "integrity": "sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w==", "engines": { "node": ">=14.16" }, @@ -1175,11 +1200,11 @@ } }, "node_modules/p-cancelable": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz", - "integrity": "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-4.0.1.tgz", + "integrity": "sha512-wBowNApzd45EIKdO1LaU+LrMBwAcjfPaYtVzV3lmfM3gf8Z4CHZsiIqlM8TZZ8okYvh5A1cP6gTfCRQtwUpaUg==", "engines": { - "node": ">=12.20" + "node": ">=14.16" } }, "node_modules/parse5": { @@ -1387,6 +1412,17 @@ "@mixmark-io/domino": "^2.2.0" } }, + "node_modules/type-fest": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.23.0.tgz", + "integrity": "sha512-ZiBujro2ohr5+Z/hZWHESLz3g08BBdrdLMieYFULJO+tWc437sn8kQsWLJoZErY8alNhxre9K4p3GURAG11n+w==", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", @@ -1405,18 +1441,6 @@ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" }, - "node_modules/uuid": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", - "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/web-streams-polyfill": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", diff --git a/package.json b/package.json index 33c2355..84a9847 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "description": "The Tarkov.dev's Escape from Tarkov Discord bot", "main": "index.mjs", "engines": { - "node": "18.*" + "node": "20.*" }, "repository": { "type": "git", @@ -24,17 +24,16 @@ "@discordjs/rest": "^2.3.0", "ascii-table": "^0.0.9", "cheerio": "^1.0.0-rc.12", - "cron": "^2.4.0", + "cron": "^3.1.7", "cross-env": "^7.0.3", - "discord-api-types": "^0.37.91", + "discord-api-types": "^0.37.93", "discord.js": "^14.15.3", "dotenv": "^16.4.5", - "got": "^13.0.0", - "i18next": "^23.11.5", + "got": "^14.4.2", + "i18next": "^23.12.2", "moment": "^2.30.1", "nodemon": "^3.1.4", - "turndown": "^7.2.0", - "uuid": "^10.0.0" + "turndown": "^7.2.0" }, "devDependencies": { "@railway/cli": "^3.11.0" diff --git a/translations/en.json b/translations/en.json index f69ae26..abee683 100644 --- a/translations/en.json +++ b/translations/en.json @@ -8,11 +8,16 @@ "🛒 {{traderName}} restock in {{numMinutes}} minutes 🛒 ({{gameMode}})": "🛒 {{traderName}} restock in {{numMinutes}} minutes 🛒 ({{gameMode}})", "❌ ERROR ❌": "❌ ERROR ❌", "{{accountId}} is not a valid account id": "{{accountId}} is not a valid account id", + "{{side}} Death Rate": "{{side}} Death Rate", + "{{side}} K:D": "{{side}} K:D", + "{{side}} Longest Survival Streak": "{{side}} Longest Survival Streak", + "{{side}} Survive Rate": "{{side}} Survive Rate", "{{thingName}} set to {{level}}.": "{{thingName}} set to {{level}}.", "A Dmg": "A Dmg", "About": "About", "Access item(s)": "Access item(s)", "Access item(s) for level >= {{playerLevel}}": "Access item(s) for level >= {{playerLevel}}", + "Achievements": "Achievements", "additional results not shown.": "additional results not shown.", "All hideout stations set to {{level}}.": "All hideout stations set to {{level}}.", "All traders set to {{level}}.": "All traders set to {{level}}.", @@ -38,6 +43,7 @@ "Craft": "Craft", "Cures": "Cures", "Default progress": "Default progress", + "Death Rate": "Death Rate", "Delay": "Delay", "Dmg": "Dmg", "Due to technical issues, please visit the official EFT website to view the latest patch notes.": "Due to technical issues, please visit the official EFT website to view the latest patch notes.", @@ -66,10 +72,12 @@ "Help for /": "Help for /", "Hideout": "Hideout", "Hideout Management skill set to {{managementLevel}}.": "Hideout Management skill set to {{managementLevel}}.", + "Hours Played": "Hours Played", "I have been online for": "I have been online for", "Intelligence Center": "Intelligence Center", "Invalid token": "Invalid token", "Item Tier": "Item Tier", + "K:D": "K:D", "kg": "kg", "Latest EFT Changes": "Latest EFT Changes", "Latest Goon Reports": "Latest Goon Reports", @@ -77,6 +85,7 @@ "Level": "Level", "level": "level", "LL": "LL", + "Longest Survival Streak": "Longest Survival Streak", "Loot Tiers": "Loot Tiers", "Loot tiers are divided primarily by the per-slot value of the item": "Loot tiers are divided primarily by the per-slot value of the item", "Map": "Map", @@ -99,6 +108,7 @@ "Pen": "Pen", "Players": "Players", "Please visit {{url}} for more information": "Please visit {{url}} for more information", + "PMC": "PMC", "PMC level set to {{level}}.": "PMC level set to {{level}}.", "Price": "Price", "Price and Item Details": "Price and Item Details", @@ -108,6 +118,7 @@ "Restock alert disabled for {{traderName}}.": "Restock alert disabled for {{traderName}}.", "Restock alert enabled for {{traderName}}.": "Restock alert enabled for {{traderName}}.", "Required Tools": "Required Tools", + "Scav": "Scav", "seconds": "seconds", "Sell to": "Sell to", "Server language set to {{languageCode}}.": "Server language set to {{languageCode}}.", @@ -124,6 +135,7 @@ "Stash bot does not have permission to send messages in #{{channelName}}.": "Stash bot does not have permission to send messages in #{{channelName}}.", "Stash Invite Link": "Stash Invite Link", "Stash Uptime": "Stash Uptime", + "Survive Rate": "Survive Rate", "TarkovTracker account unlinked.": "TarkovTracker account unlinked.", "Task": "Task", "Thank you for your report!": "Thank you for your report!", @@ -137,6 +149,7 @@ "Total": "Total", "Trader restocks": "Trader restocks", "Traders": "Traders", + "Updated {{updateTimeAgo}}": "Updated {{updateTimeAgo}}", "Unknown": "Unknown", "Usec": "USEC", "Value": "Value", From dbeb007191b1f80981360a028ae874fd0087fc2c Mon Sep 17 00:00:00 2001 From: Razzmatazz Date: Tue, 30 Jul 2024 12:44:22 -0500 Subject: [PATCH 3/4] bug fix --- commands/player.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commands/player.mjs b/commands/player.mjs index 3968c9c..8b1d52e 100644 --- a/commands/player.mjs +++ b/commands/player.mjs @@ -129,7 +129,7 @@ const defaultFunction = { for (const achievementId in profile.achievements) { const achievement = achievements.find(a => a.id === achievementId); if (!achievement) { - return; + continue; } completedAchievements.push({...achievement, completed: profile.achievements[achievementId]}); } From 909b50e94cdc263f274d8d6f2816cca51107b47b Mon Sep 17 00:00:00 2001 From: Razzmatazz Date: Tue, 30 Jul 2024 12:58:22 -0500 Subject: [PATCH 4/4] add last active --- commands/player.mjs | 9 +++++++++ translations/en.json | 6 ++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/commands/player.mjs b/commands/player.mjs index 8b1d52e..2549fe2 100644 --- a/commands/player.mjs +++ b/commands/player.mjs @@ -95,6 +95,15 @@ const defaultFunction = { });*/ embed.setURL(`https://tarkov.dev/player/${accountId}`); const descriptionParts = [`${t('Hours Played')}: ${Math.round(profile.pmcStats.eft.totalInGameTime / 60 / 60)}`]; + const lastActive = profile.skills.Common.reduce((mostRecent, skill) => { + if (skill.LastAccess > mostRecent) { + return skill.LastAccess; + } + return mostRecent; + }, 0); + if (lastActive > 0) { + descriptionParts.push(`${t('Last Active')}: ${new Date(lastActive * 1000).toLocaleString(lang)}`); + } /*if (task.minPlayerLevel) { descriptionParts.push(`${t('Minimum Level')}: ${task.minPlayerLevel}`); }*/ diff --git a/translations/en.json b/translations/en.json index abee683..e6fcffa 100644 --- a/translations/en.json +++ b/translations/en.json @@ -8,10 +8,7 @@ "🛒 {{traderName}} restock in {{numMinutes}} minutes 🛒 ({{gameMode}})": "🛒 {{traderName}} restock in {{numMinutes}} minutes 🛒 ({{gameMode}})", "❌ ERROR ❌": "❌ ERROR ❌", "{{accountId}} is not a valid account id": "{{accountId}} is not a valid account id", - "{{side}} Death Rate": "{{side}} Death Rate", - "{{side}} K:D": "{{side}} K:D", - "{{side}} Longest Survival Streak": "{{side}} Longest Survival Streak", - "{{side}} Survive Rate": "{{side}} Survive Rate", + "{{side}} Stats": "{{side}} Stats", "{{thingName}} set to {{level}}.": "{{thingName}} set to {{level}}.", "A Dmg": "A Dmg", "About": "About", @@ -81,6 +78,7 @@ "kg": "kg", "Latest EFT Changes": "Latest EFT Changes", "Latest Goon Reports": "Latest Goon Reports", + "Last Active": "Last Active", "Last Updated": "Last Updated", "Level": "Level", "level": "level",