const fs = require('fs'); let config; let cache; // Load config and cache try { config = require('./config.json'); } catch (e) { console.error("Failed to load config.json", e); } try { cache = require('./cache.json'); } catch (e) { cache = { users: {}, factions: {}, companies: {}, items: {} }; try { fs.writeFileSync('./cache.json', JSON.stringify(cache)); } catch (writeErr) { console.error("Failed to write initial cache.json", writeErr); } } // Constants const HOURS = 60 * 60 * 1000; const TTL = { USER: 12 * HOURS, FACTION: 12 * HOURS, COMPANY: 12 * HOURS, ITEM: 7 * 24 * HOURS, ITEM_LOOKUP: 30 * 24 * HOURS }; // Helper to save cache function saveCache() { try { fs.writeFileSync('./cache.json', JSON.stringify(cache)); } catch (e) { console.error("Failed to save cache:", e); } } // Generic Caching Helper async function getCached(collectionName, id, fetchFn, ttl, force = false) { const now = new Date().getTime(); // Ensure nested object exists if (!cache[collectionName]) cache[collectionName] = {}; const item = cache[collectionName][id]; let lastUpdated = 0; if (item && item.updated) { try { lastUpdated = new Date(item.updated).getTime(); } catch (e) { lastUpdated = 0; } } if (!force && item && (now - lastUpdated < ttl)) { console.debug(`Cache: Hit for ${collectionName} ${item.name || id}`); return item; } else { if (force) console.debug(`Cache: Force refresh for ${collectionName} ${id || 'unknown'}`); else console.debug(`Cache: Miss for ${collectionName} ${id || 'unknown'}`); try { const result = await fetchFn(); console.debug(`Cache: Resolved ${collectionName} ${result.name || result.title || id}`); // Update cache with full result cache[collectionName][id] = { ...result, updated: new Date().toISOString() }; saveCache(); return cache[collectionName][id]; } catch (e) { console.error(`Error fetching for ${collectionName} ${id}:`, e); throw e; } } } // Helper for generic API fetching async function fetchApi(path) { const glue = path.includes('?') ? '&' : '?'; const response = await fetch(`${path}${glue}key=${config.torn}`); const data = await response.json(); if (data.error) { console.error(`Torn API Error on ${path}:`, JSON.stringify(data.error)); throw new Error(data.error.error || "Torn API Error"); } return data; } const api = { self: {}, // Will be populated by readyCheck readyCheck: async (key) => { try { // Fetch own 'basic' data using V2 (which returns profile object) // By passing null/undefined as user, api.user.basic defaults to 'self' cache key const data = await api.user.basic(null, true); api.self = data; console.log(`Torn: Connected as ${data.name} [${data.player_id}]`); } catch (e) { console.error("Torn: Critical error during startup check", e); } }, test: async () => { const url = `https://api.torn.com/user/?selections=basic&key=${config.torn}`; const response = await fetch(url); const data = await response.json(); return `Connected to Torn as ${data.name} [${data.player_id}]`; }, api: async (url) => { const response = await fetch(`${url}&key=${config.torn}`); return response.json(); }, user: { async basic(user, force = false) { const endpoint = user ? `https://api.torn.com/v2/user/${user}/basic` : `https://api.torn.com/v2/user/basic`; return getCached('users', user || 'self', async () => { const data = await fetchApi(endpoint); if (data.profile) data.profile.player_id = data.profile.id; // Shim for V1 compatibility return data.profile; // V2 wraps in 'profile' }, TTL.USER, force); }, async profile(user, force = false) { const endpoint = user ? `https://api.torn.com/v2/user/${user}/profile` : `https://api.torn.com/v2/user/profile`; return getCached('users', user || 'self', async () => { const data = await fetchApi(endpoint); if (data.profile) data.profile.player_id = data.profile.id; // Shim for V1 compatibility return data.profile; // V2 wraps in 'profile' }, TTL.USER, force); }, async stats(user, category, statName) { let url = `https://api.torn.com/v2/user`; if (user) { url += `/${user}/personalstats`; } if (category) { url += `?cat=${category}`; } else { url += `?cat=all`; } if (statName) { url += `&stat=${statName}`; } return fetchApi(url); }, }, faction: { async basic(faction, force = false) { // If faction is null, we can't key by ID easily until we fetch. // For now, let's assume if faction is provided we use it as key. // If not provided, we might be fetching our own faction. // We can key it by "own" or similar if needed, but let's see. // If faction is missing, we fetch own faction, resulting data has ID. // Special handling: if faction is undefined, we can't check cache by ID easily without knowing ID. // However, we can use a special key like 'own' or skip cache check pre-fetch? // Better: If no ID provided, we just fetch to be safe, OR we assume config.factionID if we had it. // Let's implement transparent fetching without ID -> fetch -> cache by ID. if (!faction) { const endpoint = `https://api.torn.com/v2/faction/basic`; const response = await fetchApi(endpoint); const data = response.basic; // We can update cache here manually cache.factions[data.id] = { ...data, updated: new Date().toISOString() }; saveCache(); return data; } return getCached('factions', faction, async () => { const endpoint = `https://api.torn.com/v2/faction/${faction}/basic`; const response = await fetchApi(endpoint); return response.basic; }, TTL.FACTION, force); }, async members(faction) { const endpoint = faction ? `https://api.torn.com/v2/faction/${faction}/members?striptags=true` : `https://api.torn.com/v2/faction/members?striptags=true`; const data = await fetchApi(endpoint); return data.members; }, async crimes(options = {}) { let params = new URLSearchParams(); if (typeof options === 'string') { params.append('cat', options); } else { if (options.category) params.append('cat', options.category); if (options.from) params.append('from', options.from); if (options.to) params.append('to', options.to); if (options.limit) params.append('limit', options.limit); if (options.sort) params.append('sort', options.sort); if (options.offset !== undefined) params.append('offset', options.offset); if (options.initiator) params.append('initiator', options.initiator); } const endpoint = `https://api.torn.com/v2/faction/crimes`; const queryString = params.toString() ? `?${params.toString()}` : ''; const data = await fetchApi(`${endpoint}${queryString}`); return data.crimes; }, async upgrades() { const data = await fetchApi(`https://api.torn.com/v2/faction/upgrades`); return data; }, async news(category, from) { const data = await fetchApi(`https://api.torn.com/v2/faction/news?striptags=false&limit=100&sort=DESC&from=${from}&cat=${category}`); return data.news; }, async rankedWars(options = {}) { let params = new URLSearchParams(); if (options.limit) params.append('limit', options.limit); if (options.offset !== undefined) params.append('offset', options.offset); if (options.sort) params.append('sort', options.sort); if (options.to) params.append('to', options.to); if (options.from) params.append('from', options.from); const queryString = params.toString() ? `?${params.toString()}` : ''; const data = await fetchApi(`https://api.torn.com/v2/faction/rankedwars${queryString}`); return data.rankedwars; }, async rankedWarReport(id) { const data = await fetchApi(`https://api.torn.com/v2/faction/${id}/rankedwarreport`); return data.rankedwarreport; } }, company: async (company, force = false) => { if (!company) { const endpoint = `https://api.torn.com/company/?selections=profile`; const data = await fetchApi(endpoint); // ID is data.company.ID // Torn API v1/v2 difference? URL says /company/? so likely v1 standard structure // Let's assume data.company exists. if (data.company) { cache.companies[data.company.ID] = { ...data.company, updated: new Date().toISOString() }; saveCache(); return data.company; } return data; } return getCached('companies', company, async () => { const endpoint = `https://api.torn.com/company/${company}?selections=profile`; const data = await fetchApi(endpoint); return data.company; }, TTL.COMPANY, force); }, // item was a function with a .lookup property item: Object.assign( async (item, force = false) => { return getCached('items', item, async () => { const data = await fetchApi(`https://api.torn.com/v2/torn/${item}/items?sort=ASC`); return data.items[0]; }, TTL.ITEM, force); }, { lookup: async (itemName) => { console.debug(`Torn: Looking up item ${itemName}`); const now = new Date().getTime(); // Check cache first for (const itemId in cache.items) { if (cache.items[itemId].name === itemName) { let last = 0; try { last = new Date(cache.items[itemId].updated).getTime(); } catch (e) { } if (now - last < TTL.ITEM_LOOKUP) { console.debug(`Cache: Hit for item ${cache.items[itemId].name}`); return cache.items[itemId]; } } } console.debug(`Cache: Miss for item ${itemName}`); const data = await fetchApi(`https://api.torn.com/v2/torn/items?cat=All&sort=ASC`); let target; if (data.items) { data.items.forEach(item => { if (item.name === itemName) { console.debug(`Torn: Found item ${item.name} as ${item.id}`); target = item; } }); } if (target) { cache.items[target.id] = target; cache.items[target.id].updated = new Date().toISOString(); saveCache(); } return target; } } ), }; module.exports = api;