Files
saddbot/utils/ocLogic.js

184 lines
6.1 KiB
JavaScript

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;
}
/**
* Fetches historical OCs and updates the stats file.
* @param {Object} torn The torn wrapper instance.
* @param {string} statsPath Path to the stats JSON file.
* @param {number} days Number of days back to scan.
* @returns {Promise<number>} Number of users updated.
*/
async function fetchAndProcessHistory(torn, statsPath, days) {
const now = Date.now();
const fromTimestamp = Math.floor((now - (days * 24 * 60 * 60 * 1000)) / 1000);
// Load existing stats
let stats = {};
if (fs.existsSync(statsPath)) {
try {
stats = JSON.parse(fs.readFileSync(statsPath, 'utf8'));
} catch (e) {
console.error("ocLogic: Failed to load existing stats during fetch", e);
}
}
// Fetch faction members
const memberMap = await getMemberMap(torn);
let crimesList = [];
const categories = ['recruiting', 'planned', 'active', 'successful', 'failed'];
console.debug(`ocLogic: Scanning history for last ${days} days...`);
for (const cat of categories) {
try {
const crimes = await torn.faction.crimes({
from: fromTimestamp,
sort: 'ASC',
category: cat,
limit: 300 // Match scanoc batch size
});
if (crimes && Array.isArray(crimes)) {
crimesList = crimesList.concat(crimes);
}
} catch (e) {
console.error(`ocLogic: Failed to fetch crimes for category '${cat}'`, e);
}
}
if (crimesList.length === 0) {
return 0;
}
// Process crimes
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));
console.log(`ocLogic: Updated history for ${updates} users.`);
} catch (e) {
console.error("ocLogic: Failed to save stats", e);
}
}
return updates;
}
module.exports = {
getMemberMap,
calculateCrimeTimestamp,
processCrimes,
fetchAndProcessHistory
};