generate a better oc member list, and fetch it better too
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -143,3 +143,4 @@ state.json
|
|||||||
docker-compose.yml
|
docker-compose.yml
|
||||||
public/
|
public/
|
||||||
data/
|
data/
|
||||||
|
activity_visualization.md
|
||||||
@@ -29,25 +29,59 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let members = [];
|
|
||||||
try {
|
|
||||||
// Fetch own faction members
|
// Fetch own faction members
|
||||||
|
try {
|
||||||
members = await torn.faction.members();
|
members = await torn.faction.members();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("inactive: Failed to fetch members", e);
|
console.error("inactive: Failed to fetch members", e);
|
||||||
return interaction.editReply('Failed to fetch faction members from API.');
|
return interaction.editReply('Failed to fetch faction members from API.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fetch currently active/planned/recruiting crimes to check for current participation
|
||||||
|
const activeUserIds = new Set();
|
||||||
|
try {
|
||||||
|
const categories = ['recruiting', 'planned', 'active'];
|
||||||
|
const promises = categories.map(cat => torn.faction.crimes({ category: cat, limit: 100 })); // limit 100 to catch most
|
||||||
|
const results = await Promise.all(promises);
|
||||||
|
|
||||||
|
results.forEach(crimes => {
|
||||||
|
if (crimes && Array.isArray(crimes)) {
|
||||||
|
crimes.forEach(crime => {
|
||||||
|
// Only consider truly active/pending statuses
|
||||||
|
const completedStatuses = ['Successful', 'Failure', 'Canceled', 'Expired', 'Timeout'];
|
||||||
|
if (completedStatuses.includes(crime.status)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (crime.slots && Array.isArray(crime.slots)) {
|
||||||
|
crime.slots.forEach(slot => {
|
||||||
|
if (slot.user && slot.user.id) {
|
||||||
|
activeUserIds.add(slot.user.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log(`inactive: Found ${activeUserIds.size} users currently in crimes.`);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("inactive: Failed to fetch current crimes", e);
|
||||||
|
}
|
||||||
|
|
||||||
const inactiveUsers = [];
|
const inactiveUsers = [];
|
||||||
|
|
||||||
// Check each member
|
// Check each member
|
||||||
for (const member of members) {
|
for (const member of members) {
|
||||||
const userId = member.id;
|
const userId = member.id;
|
||||||
|
|
||||||
|
// Skip if user is currently in a crime
|
||||||
|
if (activeUserIds.has(userId)) continue;
|
||||||
|
|
||||||
const userName = member.name;
|
const userName = member.name;
|
||||||
const userStat = stats[userId];
|
const userStat = stats[userId];
|
||||||
|
|
||||||
if (!userStat) {
|
if (!userStat || !userStat.lastSeen) {
|
||||||
// Never seen in tracking
|
// Never seen in tracking or no lastSeen data
|
||||||
inactiveUsers.push({
|
inactiveUsers.push({
|
||||||
id: userId,
|
id: userId,
|
||||||
name: userName,
|
name: userName,
|
||||||
@@ -60,6 +94,7 @@ module.exports = {
|
|||||||
id: userId,
|
id: userId,
|
||||||
name: userName,
|
name: userName,
|
||||||
lastSeen: new Date(userStat.lastSeen),
|
lastSeen: new Date(userStat.lastSeen),
|
||||||
|
lastCrimeId: userStat.lastCrimeId,
|
||||||
daysInactive: Math.floor((Date.now() - userStat.lastSeen) / (24 * 60 * 60 * 1000))
|
daysInactive: Math.floor((Date.now() - userStat.lastSeen) / (24 * 60 * 60 * 1000))
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -92,7 +127,14 @@ module.exports = {
|
|||||||
value = "Never seen in OCs (since tracking started)";
|
value = "Never seen in OCs (since tracking started)";
|
||||||
} else {
|
} else {
|
||||||
const ts = Math.floor(user.lastSeen.getTime() / 1000);
|
const ts = Math.floor(user.lastSeen.getTime() / 1000);
|
||||||
value = `Last crime: <t:${ts}:d>\n(<t:${ts}:R>)`;
|
let dateStr = `<t:${ts}:d>`;
|
||||||
|
if (user.lastCrimeId) {
|
||||||
|
const url = `https://www.torn.com/factions.php?step=your&type=1#/tab=crimes&crimeId=${user.lastCrimeId}`;
|
||||||
|
dateStr = `[Last crime](${url}): <t:${ts}:d>`;
|
||||||
|
} else {
|
||||||
|
dateStr = `Last crime: <t:${ts}:d>`;
|
||||||
|
}
|
||||||
|
value = `${dateStr}\n(<t:${ts}:R>)`;
|
||||||
}
|
}
|
||||||
|
|
||||||
embed.addFields({
|
embed.addFields({
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
const { SlashCommandBuilder } = require('discord.js');
|
const { SlashCommandBuilder } = require('discord.js');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const { getMemberMap, processCrimes } = require('../../utils/ocLogic');
|
const { fetchAndProcessHistory } = require('../../utils/ocLogic');
|
||||||
const torn = require('../../torn.js');
|
const torn = require('../../torn.js');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
@@ -18,68 +18,21 @@ module.exports = {
|
|||||||
await interaction.deferReply();
|
await interaction.deferReply();
|
||||||
|
|
||||||
const days = interaction.options.getInteger('days') || 30;
|
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');
|
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...`);
|
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 {
|
try {
|
||||||
// Fetch with a higher limit since we are scanning back further
|
const updates = await fetchAndProcessHistory(torn, statsPath, days);
|
||||||
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) {
|
if (updates > 0) {
|
||||||
try {
|
await interaction.editReply(`Scan complete. Updated stats for ${updates} users.`);
|
||||||
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 {
|
} else {
|
||||||
await interaction.editReply(`Scan complete. Processed ${crimesList.length} crimes. No new updates needed.`);
|
await interaction.editReply(`Scan complete. No new updates needed.`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("scanOC: Failed to scan history", e);
|
||||||
|
await interaction.editReply(`Scan failed: ${e.message}`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
21
index.js
21
index.js
@@ -2,6 +2,7 @@
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('node:path');
|
const path = require('node:path');
|
||||||
const torn = require('./torn.js');
|
const torn = require('./torn.js');
|
||||||
|
const { fetchAndProcessHistory } = require('./utils/ocLogic');
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
|
|
||||||
let config, state;
|
let config, state;
|
||||||
@@ -112,6 +113,26 @@ for (const folder of commandFolders) {
|
|||||||
|
|
||||||
// On client ready, generate upgrades image if missing or on first run
|
// On client ready, generate upgrades image if missing or on first run
|
||||||
client.on(Events.ClientReady, async () => {
|
client.on(Events.ClientReady, async () => {
|
||||||
|
// 1. Check and populate OC Stats if missing
|
||||||
|
try {
|
||||||
|
const statsPath = path.resolve(__dirname, 'data/ocStats.json');
|
||||||
|
const dataDir = path.dirname(statsPath);
|
||||||
|
if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true });
|
||||||
|
|
||||||
|
if (!fs.existsSync(statsPath)) {
|
||||||
|
console.log('Startup: ocStats.json missing. Initiating auto-population (scanning last 90 days)...');
|
||||||
|
// Scan 90 days by default for safety
|
||||||
|
fetchAndProcessHistory(torn, statsPath, 90).then(count => {
|
||||||
|
console.log(`Startup: Auto-population complete. Updated/Created stats for ${count} users.`);
|
||||||
|
}).catch(e => {
|
||||||
|
console.error('Startup: Auto-population failed', e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Startup: Error checking ocStats', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Upgrades Image check
|
||||||
try {
|
try {
|
||||||
const imgDir = path.resolve(__dirname, 'public');
|
const imgDir = path.resolve(__dirname, 'public');
|
||||||
if (!fs.existsSync(imgDir)) fs.mkdirSync(imgDir, { recursive: true });
|
if (!fs.existsSync(imgDir)) fs.mkdirSync(imgDir, { recursive: true });
|
||||||
|
|||||||
147
scripts/listOC.js
Normal file
147
scripts/listOC.js
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
// makes a list of members last OC time, outputs to activity_visualization.md
|
||||||
|
|
||||||
|
|
||||||
|
const torn = require('../torn.js');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
// Load stats
|
||||||
|
const statsPath = './data/ocStats.json';
|
||||||
|
let stats = {};
|
||||||
|
if (fs.existsSync(statsPath)) {
|
||||||
|
stats = JSON.parse(fs.readFileSync(statsPath, 'utf8'));
|
||||||
|
} else {
|
||||||
|
console.log("No ocStats.json found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch members
|
||||||
|
console.log("Fetching members...");
|
||||||
|
const members = await torn.faction.members();
|
||||||
|
|
||||||
|
// Fetch current crimes
|
||||||
|
console.log("Fetching current crimes...");
|
||||||
|
const activeCrimes = new Map(); // userId -> { crimeId, category, time }
|
||||||
|
const categories = ['recruiting', 'planned', 'active'];
|
||||||
|
|
||||||
|
const promises = categories.map(cat => torn.faction.crimes({ category: cat, limit: 100 }));
|
||||||
|
const results = await Promise.all(promises);
|
||||||
|
|
||||||
|
results.forEach((crimes, index) => {
|
||||||
|
const cat = categories[index];
|
||||||
|
if (crimes && Array.isArray(crimes)) {
|
||||||
|
crimes.forEach(c => {
|
||||||
|
const completed = ['Successful', 'Failure', 'Canceled', 'Expired', 'Timeout'];
|
||||||
|
// We catch everything but flag status for visualization
|
||||||
|
// But if we want to mimic inactive.js strict active check:
|
||||||
|
// Only treat as 'current' if NOT completed.
|
||||||
|
|
||||||
|
// Actually, for visualization, let's keep the record but mark it differently?
|
||||||
|
// The user wants to see "Stage".
|
||||||
|
// If it is completed, it should be treated as "Historic" essentially, logic-wise for "Active" label.
|
||||||
|
|
||||||
|
if (c.slots && Array.isArray(c.slots)) {
|
||||||
|
c.slots.forEach(s => {
|
||||||
|
if (s.user && s.user.id) {
|
||||||
|
const newStatus = c.status;
|
||||||
|
const newIsCompleted = completed.includes(newStatus);
|
||||||
|
|
||||||
|
const existing = activeCrimes.get(s.user.id);
|
||||||
|
if (existing && !existing.isCompleted && newIsCompleted) {
|
||||||
|
// Existing is active, new is completed. Do NOT overwrite.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
activeCrimes.set(s.user.id, {
|
||||||
|
crimeId: c.id,
|
||||||
|
category: cat,
|
||||||
|
status: newStatus,
|
||||||
|
started: c.time_started || c.initiated_at || c.created_at,
|
||||||
|
isCompleted: newIsCompleted
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let output = "# Activity Visualization\n\n";
|
||||||
|
output += "| Name | Stage | Last Time | Details |\n";
|
||||||
|
output += "|---|---|---|---|\n";
|
||||||
|
|
||||||
|
// Calculate latestTime for everyone first to allow sorting
|
||||||
|
const memberData = members.map(m => {
|
||||||
|
const stat = stats[m.id];
|
||||||
|
const current = activeCrimes.get(m.id);
|
||||||
|
|
||||||
|
const currentStart = current ? current.started * 1000 : 0;
|
||||||
|
const lastSeen = stat ? stat.lastSeen : 0;
|
||||||
|
const latestTime = Math.max(currentStart, lastSeen);
|
||||||
|
|
||||||
|
return { m, stat, current, latestTime };
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sort: Longest time ago (smallest timestamp) first
|
||||||
|
memberData.sort((a, b) => {
|
||||||
|
if (a.latestTime === 0 && b.latestTime === 0) return 0;
|
||||||
|
if (a.latestTime === 0) return -1; // Keep members with no activity at the top (or bottom, depending on desired order)
|
||||||
|
if (b.latestTime === 0) return 1;
|
||||||
|
return a.latestTime - b.latestTime;
|
||||||
|
});
|
||||||
|
|
||||||
|
memberData.forEach(({ m, stat, current, latestTime }) => {
|
||||||
|
|
||||||
|
let stage = "Unknown";
|
||||||
|
let timeStr = "Never";
|
||||||
|
let details;
|
||||||
|
|
||||||
|
const isActuallyActive = current && !current.isCompleted;
|
||||||
|
|
||||||
|
// Helper to linkify ID
|
||||||
|
const linkify = (id) => `[${id}](https://www.torn.com/factions.php?step=your&type=1#/tab=crimes&crimeId=${id})`;
|
||||||
|
|
||||||
|
// Determine Stage and Details string
|
||||||
|
if (isActuallyActive) {
|
||||||
|
stage = `**${current.status}**`;
|
||||||
|
details = `In: ${linkify(current.crimeId)}`;
|
||||||
|
} else if (current && current.isCompleted) {
|
||||||
|
// It was found in API but is completed
|
||||||
|
stage = `${current.status}`;
|
||||||
|
details = `Done: ${linkify(current.crimeId)}`;
|
||||||
|
} else if (stat) {
|
||||||
|
// Historic
|
||||||
|
stage = "Historic";
|
||||||
|
const diff = Date.now() - stat.lastSeen;
|
||||||
|
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
|
||||||
|
if (days < 3) stage = "Recent";
|
||||||
|
else if (days > 7) stage = "Inactive";
|
||||||
|
|
||||||
|
details = `Last: ${stat.lastCrimeId ? linkify(stat.lastCrimeId) : '?'}`;
|
||||||
|
} else {
|
||||||
|
stage = "No Data";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (latestTime > 0) {
|
||||||
|
const date = new Date(latestTime);
|
||||||
|
timeStr = date.toLocaleString();
|
||||||
|
|
||||||
|
// Add relative time
|
||||||
|
const diff = Date.now() - latestTime;
|
||||||
|
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
|
||||||
|
if (days === 0) timeStr += " (Today)";
|
||||||
|
else if (days === 1) timeStr += " (Yesterday)";
|
||||||
|
else timeStr += ` (${days} days ago)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
output += `| ${m.name} | ${stage} | ${timeStr} | ${details} |\n`;
|
||||||
|
});
|
||||||
|
|
||||||
|
fs.writeFileSync('activity_visualization.md', output, 'utf8');
|
||||||
|
console.log("Written output to activity_visualization.md");
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error:", e);
|
||||||
|
}
|
||||||
|
})();
|
||||||
@@ -105,8 +105,79 @@ async function processCrimes(crimesList, stats, memberMap, torn) {
|
|||||||
return updatedUsers.size;
|
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 = {
|
module.exports = {
|
||||||
getMemberMap,
|
getMemberMap,
|
||||||
calculateCrimeTimestamp,
|
calculateCrimeTimestamp,
|
||||||
processCrimes
|
processCrimes,
|
||||||
|
fetchAndProcessHistory
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user