311 lines
12 KiB
JavaScript
311 lines
12 KiB
JavaScript
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; |