inactive command to show people not participating in OC
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -140,6 +140,6 @@ dist
|
|||||||
|
|
||||||
config.json
|
config.json
|
||||||
state.json
|
state.json
|
||||||
cache.json
|
|
||||||
docker-compose.yml
|
docker-compose.yml
|
||||||
public/
|
public/
|
||||||
|
data/
|
||||||
@@ -7,14 +7,17 @@ module.exports = {
|
|||||||
.setDescription('How is KZNKing doing'),
|
.setDescription('How is KZNKing doing'),
|
||||||
async execute(interaction) {
|
async execute(interaction) {
|
||||||
const kznID = 3392710
|
const kznID = 3392710
|
||||||
const KZNKing = await torn.user.profile(kznID);
|
const data = await torn.user.get(kznID, ['profile', 'job', 'faction']);
|
||||||
let message = `${KZNKing.name} has ${KZNKing.friends} friends and ${KZNKing.enemies} enemies. `;
|
|
||||||
(KZNKing.married.duration) ? message += `He has been married to [${KZNKing.married.spouse_name}](https://www.torn.com/profiles.php?XID=${KZNKing.married.spouse_id}) for ${KZNKing.married.duration} days. ` : message += `He is not married. `;
|
|
||||||
(KZNKing.property === "Private Island") ? message += `He has a Private Island. ` : message += `He does not have a Private Island. `;
|
|
||||||
(KZNKing.job.position === "Director") ? message += `He is director of ${KZNKing.job.company_name}. ` : message += `He is not director of his company. `;
|
|
||||||
(KZNKing.faction.position === "Leader") ? message += `He is leader of ${KZNKing.faction.faction_name}. ` : message += `He is not leader of his faction. `;
|
|
||||||
|
|
||||||
const company = (await torn.company(KZNKing.job.company_id));
|
const KZNKing = { ...data.profile, job: data.job, faction: data.faction };
|
||||||
|
|
||||||
|
let message = `${KZNKing.name} has ${KZNKing.friends} friends and ${KZNKing.enemies} enemies. `;
|
||||||
|
(KZNKing.spouse && KZNKing.spouse.days_married) ? message += `He has been married to [${KZNKing.spouse.name}](https://www.torn.com/profiles.php?XID=${KZNKing.spouse.id}) for ${KZNKing.spouse.days_married} days. ` : message += `He is not married. `;
|
||||||
|
(KZNKing.property === "Private Island") ? message += `He has a Private Island. ` : message += `He does not have a Private Island. `;
|
||||||
|
(KZNKing.job.position === "Director") ? message += `He is director of ${KZNKing.job.name}. ` : message += `He is not director of his company. `;
|
||||||
|
(KZNKing.faction.position === "Leader") ? message += `He is leader of ${KZNKing.faction.name}. ` : message += `He is not leader of his faction. `;
|
||||||
|
|
||||||
|
const company = (await torn.company(KZNKing.job.id));
|
||||||
const embeds = [];
|
const embeds = [];
|
||||||
if (KZNKing.job.position === "Director") {
|
if (KZNKing.job.position === "Director") {
|
||||||
const jobEmbed = new EmbedBuilder()
|
const jobEmbed = new EmbedBuilder()
|
||||||
@@ -60,35 +63,35 @@ module.exports = {
|
|||||||
embeds.push(jobEmbed);
|
embeds.push(jobEmbed);
|
||||||
}
|
}
|
||||||
|
|
||||||
const faction = await torn.faction.basic(KZNKing.faction.faction_id)
|
const factionBasic = await torn.faction.basic(KZNKing.faction.id)
|
||||||
if (KZNKing.faction.position === "Leader") {
|
if (KZNKing.faction.position === "Leader") {
|
||||||
const facEmbed = new EmbedBuilder()
|
const facEmbed = new EmbedBuilder()
|
||||||
.setTitle(faction.name)
|
.setTitle(factionBasic.name)
|
||||||
.setURL(`https://www.torn.com/factions.php?step=profile&ID=${faction.id}`)
|
.setURL(`https://www.torn.com/factions.php?step=profile&ID=${factionBasic.id}`)
|
||||||
.addFields(
|
.addFields(
|
||||||
{
|
{
|
||||||
name: "Members",
|
name: "Members",
|
||||||
value: `${faction.members}/${faction.capacity}`,
|
value: `${factionBasic.members}/${factionBasic.capacity}`,
|
||||||
inline: true
|
inline: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Rank",
|
name: "Rank",
|
||||||
value: `${faction.rank.name} ${faction.rank.division}`,
|
value: `${factionBasic.rank.name} ${factionBasic.rank.division}`,
|
||||||
inline: true
|
inline: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Respect",
|
name: "Respect",
|
||||||
value: `${faction.respect.toLocaleString()}`,
|
value: `${factionBasic.respect.toLocaleString()}`,
|
||||||
inline: true
|
inline: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Age",
|
name: "Age",
|
||||||
value: `${faction.days_old}`,
|
value: `${factionBasic.days_old}`,
|
||||||
inline: true
|
inline: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Wars Won",
|
name: "Wars Won",
|
||||||
value: `${faction.rank.wins}`,
|
value: `${factionBasic.rank.wins}`,
|
||||||
inline: true
|
inline: true
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -108,7 +111,7 @@ module.exports = {
|
|||||||
|
|
||||||
let factionFemales = 0;
|
let factionFemales = 0;
|
||||||
let factionTotal = 0;
|
let factionTotal = 0;
|
||||||
const factionMembers = await torn.faction.members(KZNKing.faction.faction_id);
|
const factionMembers = await torn.faction.members(KZNKing.faction.id);
|
||||||
const factionFemalePromises = factionMembers.map((user) => {
|
const factionFemalePromises = factionMembers.map((user) => {
|
||||||
return torn.user.basic(user.id).then(data => {
|
return torn.user.basic(user.id).then(data => {
|
||||||
factionTotal++;
|
factionTotal++;
|
||||||
|
|||||||
112
commands/utility/inactive.js
Normal file
112
commands/utility/inactive.js
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const torn = require('../../torn.js');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
data: new SlashCommandBuilder()
|
||||||
|
.setName('inactive')
|
||||||
|
.setDescription('Shows users who haven\'t participated in an OC recently.')
|
||||||
|
.addIntegerOption(option =>
|
||||||
|
option.setName('days')
|
||||||
|
.setDescription('Number of days of inactivity (default 3)')
|
||||||
|
.setMinValue(1)
|
||||||
|
),
|
||||||
|
async execute(interaction) {
|
||||||
|
await interaction.deferReply();
|
||||||
|
|
||||||
|
const days = interaction.options.getInteger('days') || 3;
|
||||||
|
const cutoffTime = Date.now() - (days * 24 * 60 * 60 * 1000);
|
||||||
|
const statsPath = path.join(__dirname, '../../data/ocStats.json');
|
||||||
|
|
||||||
|
// Load tracked stats
|
||||||
|
let stats = {};
|
||||||
|
if (fs.existsSync(statsPath)) {
|
||||||
|
try {
|
||||||
|
stats = JSON.parse(fs.readFileSync(statsPath, 'utf8'));
|
||||||
|
} catch (e) {
|
||||||
|
console.error("inactive: Failed to load ocStats.json", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let members = [];
|
||||||
|
try {
|
||||||
|
// Fetch own faction members
|
||||||
|
members = await torn.faction.members();
|
||||||
|
} catch (e) {
|
||||||
|
console.error("inactive: Failed to fetch members", e);
|
||||||
|
return interaction.editReply('Failed to fetch faction members from API.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const inactiveUsers = [];
|
||||||
|
|
||||||
|
// Check each member
|
||||||
|
for (const member of members) {
|
||||||
|
const userId = member.id;
|
||||||
|
const userName = member.name;
|
||||||
|
const userStat = stats[userId];
|
||||||
|
|
||||||
|
if (!userStat) {
|
||||||
|
// Never seen in tracking
|
||||||
|
inactiveUsers.push({
|
||||||
|
id: userId,
|
||||||
|
name: userName,
|
||||||
|
lastSeen: null,
|
||||||
|
daysInactive: -1
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (userStat.lastSeen < cutoffTime) {
|
||||||
|
inactiveUsers.push({
|
||||||
|
id: userId,
|
||||||
|
name: userName,
|
||||||
|
lastSeen: new Date(userStat.lastSeen),
|
||||||
|
daysInactive: Math.floor((Date.now() - userStat.lastSeen) / (24 * 60 * 60 * 1000))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort: Never seen first, then by longest inactivity
|
||||||
|
inactiveUsers.sort((a, b) => {
|
||||||
|
if (a.lastSeen === null && b.lastSeen === null) return 0;
|
||||||
|
if (a.lastSeen === null) return -1; // a comes first
|
||||||
|
if (b.lastSeen === null) return 1; // b comes first
|
||||||
|
return a.lastSeen - b.lastSeen; // Older timestamp (smaller) comes first
|
||||||
|
});
|
||||||
|
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setTitle(`Inactive OC Members (> ${days} days)`)
|
||||||
|
.setColor(0xFF0000)
|
||||||
|
.setTimestamp();
|
||||||
|
|
||||||
|
if (inactiveUsers.length === 0) {
|
||||||
|
embed.setDescription(`Everyone has participated in an OC in the last ${days} days!`);
|
||||||
|
embed.setColor(0x00FF00);
|
||||||
|
} else {
|
||||||
|
const limit = 25;
|
||||||
|
const shownUsers = inactiveUsers.slice(0, limit);
|
||||||
|
|
||||||
|
shownUsers.forEach(user => {
|
||||||
|
let value = "";
|
||||||
|
if (user.lastSeen === null) {
|
||||||
|
value = "Never seen in OCs (since tracking started)";
|
||||||
|
} else {
|
||||||
|
const ts = Math.floor(user.lastSeen.getTime() / 1000);
|
||||||
|
value = `Last crime: <t:${ts}:d>\n(<t:${ts}:R>)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
embed.addFields({
|
||||||
|
name: `${user.name} [${user.id}]`,
|
||||||
|
value: value,
|
||||||
|
inline: true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (inactiveUsers.length > limit) {
|
||||||
|
embed.setFooter({ text: `...and ${inactiveUsers.length - limit} more.` });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await interaction.editReply({ embeds: [embed] });
|
||||||
|
},
|
||||||
|
};
|
||||||
85
commands/utility/scanOC.js
Normal file
85
commands/utility/scanOC.js
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
const { SlashCommandBuilder } = require('discord.js');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const { getMemberMap, processCrimes } = require('../../utils/ocLogic');
|
||||||
|
const torn = require('../../torn.js');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
data: new SlashCommandBuilder()
|
||||||
|
.setName('scanoc')
|
||||||
|
.setDescription('Scans historical OCs to populate participation stats.')
|
||||||
|
.addIntegerOption(option =>
|
||||||
|
option.setName('days')
|
||||||
|
.setDescription('How many days back to scan (default 30)')
|
||||||
|
.setMinValue(1)
|
||||||
|
.setMaxValue(365)
|
||||||
|
),
|
||||||
|
async execute(interaction) {
|
||||||
|
await interaction.deferReply();
|
||||||
|
|
||||||
|
const days = interaction.options.getInteger('days') || 30;
|
||||||
|
const now = Date.now();
|
||||||
|
const fromTimestamp = Math.floor((now - (days * 24 * 60 * 60 * 1000)) / 1000);
|
||||||
|
const statsPath = path.join(__dirname, '../../data/ocStats.json');
|
||||||
|
|
||||||
|
// Load existing stats
|
||||||
|
let stats = {};
|
||||||
|
if (fs.existsSync(statsPath)) {
|
||||||
|
try {
|
||||||
|
stats = JSON.parse(fs.readFileSync(statsPath, 'utf8'));
|
||||||
|
} catch (e) {
|
||||||
|
console.error("scanOC: Failed to load ocStats.json", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch faction members
|
||||||
|
const memberMap = await getMemberMap(torn);
|
||||||
|
|
||||||
|
await interaction.editReply(`Scanning OCs from the last ${days} days...`);
|
||||||
|
|
||||||
|
let crimesList = [];
|
||||||
|
const categories = ['recruiting', 'planned', 'active', 'successful', 'failed'];
|
||||||
|
|
||||||
|
for (const cat of categories) {
|
||||||
|
try {
|
||||||
|
// Fetch with a higher limit since we are scanning back further
|
||||||
|
const crimes = await torn.faction.crimes({
|
||||||
|
from: fromTimestamp,
|
||||||
|
sort: 'ASC',
|
||||||
|
category: cat,
|
||||||
|
limit: 300 // Reasonable batch size?
|
||||||
|
});
|
||||||
|
|
||||||
|
if (crimes && Array.isArray(crimes)) {
|
||||||
|
crimesList = crimesList.concat(crimes);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`scanOC: Failed to fetch crimes for category '${cat}'`, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (crimesList.length === 0) {
|
||||||
|
return interaction.editReply(`Scan complete. No OCs found in the last ${days} days.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process with utility
|
||||||
|
const updates = await processCrimes(crimesList, stats, memberMap, torn);
|
||||||
|
|
||||||
|
// Save
|
||||||
|
if (updates > 0) {
|
||||||
|
try {
|
||||||
|
const dir = path.dirname(statsPath);
|
||||||
|
if (!fs.existsSync(dir)) {
|
||||||
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
|
}
|
||||||
|
fs.writeFileSync(statsPath, JSON.stringify(stats, null, 4));
|
||||||
|
await interaction.editReply(`Scan complete. Processed ${crimesList.length} crimes. Updated stats for ${updates} users.`);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("scanOC: Failed to save stats", e);
|
||||||
|
await interaction.editReply(`Scan complete, but failed to save stats: ${e.message}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await interaction.editReply(`Scan complete. Processed ${crimesList.length} crimes. No new updates needed.`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
61
tasks/trackOC.js
Normal file
61
tasks/trackOC.js
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const { getMemberMap, processCrimes } = require('../utils/ocLogic');
|
||||||
|
|
||||||
|
module.exports = async (client, torn, config) => {
|
||||||
|
const statsPath = path.join(__dirname, '../data/ocStats.json');
|
||||||
|
|
||||||
|
// Load existing stats
|
||||||
|
let stats = {};
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(statsPath)) {
|
||||||
|
stats = JSON.parse(fs.readFileSync(statsPath, 'utf8'));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("trackOC: Failed to load ocStats.json", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch faction members
|
||||||
|
const memberMap = await getMemberMap(torn);
|
||||||
|
|
||||||
|
// Fetch recent crimes (last 48 hours to be safe and catch up)
|
||||||
|
const now = Date.now();
|
||||||
|
const twoDaysAgo = Math.floor((now - (48 * 60 * 60 * 1000)) / 1000);
|
||||||
|
|
||||||
|
let crimesList = [];
|
||||||
|
const categories = ['recruiting', 'planned', 'active', 'successful', 'failed'];
|
||||||
|
|
||||||
|
for (const cat of categories) {
|
||||||
|
try {
|
||||||
|
const crimes = await torn.faction.crimes({ from: twoDaysAgo, sort: 'ASC', category: cat });
|
||||||
|
if (crimes && Array.isArray(crimes)) {
|
||||||
|
crimesList = crimesList.concat(crimes);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`trackOC: Failed to fetch crimes for category '${cat}'`, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!crimesList || crimesList.length === 0) {
|
||||||
|
console.debug("trackOC: No crimes found in the last 48 hours.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process crimes using utility
|
||||||
|
const updates = await processCrimes(crimesList, stats, memberMap, torn);
|
||||||
|
|
||||||
|
if (updates > 0) {
|
||||||
|
try {
|
||||||
|
const dir = path.dirname(statsPath);
|
||||||
|
if (!fs.existsSync(dir)) {
|
||||||
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
|
}
|
||||||
|
fs.writeFileSync(statsPath, JSON.stringify(stats, null, 4));
|
||||||
|
console.log(`trackOC: Updated participation for ${updates} users.`);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("trackOC: Failed to save stats", e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.debug("trackOC: No new updates.");
|
||||||
|
}
|
||||||
|
};
|
||||||
40
torn.js
40
torn.js
@@ -10,7 +10,7 @@ try {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
cache = require('./cache.json');
|
cache = require('./data/cache.json');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
cache = {
|
cache = {
|
||||||
users: {},
|
users: {},
|
||||||
@@ -19,7 +19,11 @@ try {
|
|||||||
items: {}
|
items: {}
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
fs.writeFileSync('./cache.json', JSON.stringify(cache));
|
const dir = './data';
|
||||||
|
if (!fs.existsSync(dir)) {
|
||||||
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
|
}
|
||||||
|
fs.writeFileSync('./data/cache.json', JSON.stringify(cache));
|
||||||
} catch (writeErr) {
|
} catch (writeErr) {
|
||||||
console.error("Failed to write initial cache.json", writeErr);
|
console.error("Failed to write initial cache.json", writeErr);
|
||||||
}
|
}
|
||||||
@@ -38,7 +42,11 @@ const TTL = {
|
|||||||
// Helper to save cache
|
// Helper to save cache
|
||||||
function saveCache() {
|
function saveCache() {
|
||||||
try {
|
try {
|
||||||
fs.writeFileSync('./cache.json', JSON.stringify(cache));
|
const dir = './data';
|
||||||
|
if (!fs.existsSync(dir)) {
|
||||||
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
|
}
|
||||||
|
fs.writeFileSync('./data/cache.json', JSON.stringify(cache));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to save cache:", e);
|
console.error("Failed to save cache:", e);
|
||||||
}
|
}
|
||||||
@@ -151,6 +159,32 @@ const api = {
|
|||||||
if (statName) { url += `&stat=${statName}`; }
|
if (statName) { url += `&stat=${statName}`; }
|
||||||
return fetchApi(url);
|
return fetchApi(url);
|
||||||
},
|
},
|
||||||
|
async job(user, force = false) {
|
||||||
|
const endpoint = user ? `https://api.torn.com/v2/user/${user}/job` : `https://api.torn.com/v2/user/job`;
|
||||||
|
return getCached('users_job', user || 'self', async () => {
|
||||||
|
const data = await fetchApi(endpoint);
|
||||||
|
return data.job;
|
||||||
|
}, TTL.USER, force);
|
||||||
|
},
|
||||||
|
async faction(user, force = false) {
|
||||||
|
const endpoint = user ? `https://api.torn.com/v2/user/${user}/faction` : `https://api.torn.com/v2/user/faction`;
|
||||||
|
return getCached('users_faction', user || 'self', async () => {
|
||||||
|
const data = await fetchApi(endpoint);
|
||||||
|
return data.faction;
|
||||||
|
}, TTL.USER, force);
|
||||||
|
},
|
||||||
|
async get(user, selections = [], force = false) {
|
||||||
|
const selStr = selections.join(',');
|
||||||
|
const endpoint = user ? `https://api.torn.com/v2/user/${user}?selections=${selStr}` : `https://api.torn.com/v2/user?selections=${selStr}`;
|
||||||
|
// Cache usage for composite calls is tricky. For now, let's skip complex caching or cache by selection string key.
|
||||||
|
// A simple key like "users_profile_job_faction_ID" works.
|
||||||
|
const cacheKey = selections.sort().join('_');
|
||||||
|
|
||||||
|
return getCached(`users_${cacheKey}`, user || 'self', async () => {
|
||||||
|
const data = await fetchApi(endpoint);
|
||||||
|
return data;
|
||||||
|
}, TTL.USER, force);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
faction: {
|
faction: {
|
||||||
|
|||||||
112
utils/ocLogic.js
Normal file
112
utils/ocLogic.js
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches faction members and returns a map of ID -> Name.
|
||||||
|
* @param {Object} torn The torn wrapper instance.
|
||||||
|
* @returns {Promise<Object>} Map of member ID to Name.
|
||||||
|
*/
|
||||||
|
async function getMemberMap(torn) {
|
||||||
|
let memberMap = {};
|
||||||
|
try {
|
||||||
|
const members = await torn.faction.members();
|
||||||
|
members.forEach(m => memberMap[m.id] = m.name);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("ocLogic: Failed to fetch faction members for name resolution", e);
|
||||||
|
}
|
||||||
|
return memberMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the best timestamp for a crime participation.
|
||||||
|
* @param {Object} slot The user slot object.
|
||||||
|
* @param {Object} crime The crime object.
|
||||||
|
* @returns {number|null} Timestamp in milliseconds, or null if invalid.
|
||||||
|
*/
|
||||||
|
function calculateCrimeTimestamp(slot, crime) {
|
||||||
|
// Priority: Max of (User join time, Crime related times)
|
||||||
|
const timestamps = [
|
||||||
|
slot.user.joined_at,
|
||||||
|
crime.initiated_at,
|
||||||
|
crime.time_started,
|
||||||
|
crime.time_completed,
|
||||||
|
crime.executed_at,
|
||||||
|
crime.time_ready,
|
||||||
|
crime.created_at
|
||||||
|
].filter(t => t && t > 0);
|
||||||
|
|
||||||
|
if (timestamps.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.max(...timestamps) * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes a list of crimes and updates the stats object.
|
||||||
|
* @param {Array} crimesList List of crime objects.
|
||||||
|
* @param {Object} stats The current stats object.
|
||||||
|
* @param {Object} memberMap Map of user ID to name.
|
||||||
|
* @returns {number} Number of updates made.
|
||||||
|
*/
|
||||||
|
async function processCrimes(crimesList, stats, memberMap, torn) {
|
||||||
|
let updatedUsers = new Set();
|
||||||
|
// We iterate sequentially to allow async fetching if needed without blasting API
|
||||||
|
for (const crime of crimesList) {
|
||||||
|
if (crime.slots && Array.isArray(crime.slots)) {
|
||||||
|
for (const slot of crime.slots) {
|
||||||
|
if (!slot.user) continue; // Skip if user data is missing
|
||||||
|
const userId = slot.user.id;
|
||||||
|
|
||||||
|
// Try to resolve name
|
||||||
|
let userName = slot.user.name;
|
||||||
|
if (!userName) userName = memberMap[userId];
|
||||||
|
|
||||||
|
if ((!userName || userName === 'Unknown') && userId && torn) {
|
||||||
|
try {
|
||||||
|
const profile = await torn.user.profile(userId);
|
||||||
|
if (profile && profile.name) {
|
||||||
|
userName = profile.name;
|
||||||
|
} else {
|
||||||
|
console.debug(`ocLogic: Failed to resolve name for ${userId} (No name in response)`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.debug(`ocLogic: Error resolving name for ${userId}`, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userId) {
|
||||||
|
const existing = stats[userId] || {};
|
||||||
|
const crimeTime = calculateCrimeTimestamp(slot, crime);
|
||||||
|
|
||||||
|
if (!crimeTime || crimeTime === 0) continue;
|
||||||
|
|
||||||
|
// Update if this crime is newer than what we have stored
|
||||||
|
// OR if we have a better name now (and same or newer time)
|
||||||
|
const isNewer = !existing.lastSeen || crimeTime > existing.lastSeen;
|
||||||
|
const isBetterName = userName && userName !== 'Unknown' && existing.name === "Unknown";
|
||||||
|
|
||||||
|
if (isNewer || isBetterName) {
|
||||||
|
// Only update timestamp if it's actually newer
|
||||||
|
const newTime = isNewer ? crimeTime : existing.lastSeen;
|
||||||
|
const newCrimeId = isNewer ? crime.id : existing.lastCrimeId;
|
||||||
|
|
||||||
|
stats[userId] = {
|
||||||
|
name: userName || existing.name || "Unknown",
|
||||||
|
lastSeen: newTime,
|
||||||
|
lastCrimeId: newCrimeId
|
||||||
|
};
|
||||||
|
updatedUsers.add(userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return updatedUsers.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getMemberMap,
|
||||||
|
calculateCrimeTimestamp,
|
||||||
|
processCrimes
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user