Files
saddbot/torn.js
2026-01-11 09:17:48 -05:00

336 lines
13 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 TIME_12H = 12 * 60 * 60 * 1000;
const TIME_7D = 7 * 24 * 60 * 60 * 1000;
const TIME_30D = 30 * 24 * 60 * 60 * 1000;
// 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) {
const now = new Date().getTime();
const item = cache[collectionName][id];
let lastUpdated = 0;
if (item && item.updated) {
try {
lastUpdated = new Date(item.updated).getTime();
} catch (e) {
lastUpdated = 0;
}
}
if (item && (now - lastUpdated < ttl)) {
console.debug(`Cache: Hit for ${collectionName} ${item.name || id}`);
return item;
} else {
console.debug(`Cache: Miss for ${collectionName} ${id || 'unknown'}`);
try {
// The fetchFn is expected to update the cache and return the data, or we can structure it differently.
// Based on the refactor code below, the fetchFn calls saveCache() and returns the data.
// But wait, the original logic for checking cache was inside the 'cache' object functions,
// calling the specific fetcher which updated the cache.
// In the refactored 'api.cache.user' below, I call 'api.user.basic(user)'.
// 'api.user.basic' updates the cache and returns data.
// So this helper just needs to return that result.
// BUT, I need to make sure I return the logical object.
const result = await fetchFn();
console.debug(`Cache: Resolved ${collectionName} ${id}`);
// If the fetchFn updated the cache, we can return the cached item to be consistent
// or just the result. The original returned the cached item in the cache wrapper.
// Let's return the result from fetchFn which is usually the data.
// However, the original cache wrappers returned `cache.users[user]`.
// Let's see if there is a difference.
// `api.user.basic` returns `data`. `cache.users[user]` is a subset of `data`?
// Original:
// `cache.users[user] = { name, player_id, level, ... }`
// `return(data)` (full api response)
// But `module.exports.cache.user` returned `cache.users[user]`.
// So the CACHE wrapper returned the CACHED OBJECT (subset), while the FETCH function returned the FULL API response.
// This is a subtle difference.
// If I want to maintain compatibility, `getCached` should return the cached item from `cache` after fetching.
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}`);
return response.json();
}
const api = {
readyCheck: async (key) => {
const url = `https://api.torn.com/user/?selections=basic&key=${key}`;
const response = await fetch(url);
const data = await response.json();
console.log(`Torn: Connected as ${data.name} [${data.player_id}]`);
},
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();
},
cache: {
async user(user) {
return getCached('users', user, async () => await api.user.basic(user), TIME_12H);
},
async faction(faction) {
return getCached('factions', faction, async () => await api.faction.basic(faction), TIME_12H);
},
async company(company) {
return getCached('companies', company, async () => await api.company(company), TIME_12H);
},
async item(item) {
return getCached('items', item, async () => await api.item(item), TIME_7D);
}
},
user: {
async basic(user) {
const data = await fetchApi(`https://api.torn.com/user/${user}?selections=basic`);
const now = new Date();
cache.users[user] = {
name: data.name,
player_id: data.player_id,
level: data.level,
gender: data.gender,
updated: now.toISOString()
};
saveCache();
return data;
},
async profile(user) {
const data = await fetchApi(`https://api.torn.com/user/${user}?selections=profile`);
const now = new Date();
cache.users[user] = {
name: data.name,
player_id: data.player_id,
level: data.level,
gender: data.gender,
updated: now.toISOString()
};
saveCache();
return data;
},
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);
},
// Added lookup to maintain feature parity if it was ever needed, though not in original user object
},
faction: {
async basic(faction) {
const endpoint = faction ? `https://api.torn.com/v2/faction/${faction}/basic` : `https://api.torn.com/v2/faction/basic`;
const response = await fetchApi(endpoint);
// v2 return structure: { basic: { ... } }
const data = response.basic;
const now = new Date();
// Store by ID. If faction is null (own faction), we rely on data.id
cache.factions[data.id] = {
name: data.name,
leader_id: data.leader_id,
capacity: data.capacity,
rank: data.rank,
best_chain: data.best_chain,
updated: now.toISOString()
};
saveCache();
return data;
},
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();
let category = '';
if (typeof options === 'string') {
category = options;
} else {
if (options.category) category = 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 = category ? `https://api.torn.com/v2/faction/crimes/${category}` : `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 was a top-level function in export, but also used as property
// Original: module.exports.company = async ...
// So api.company should be a function
company: async (company) => {
const endpoint = company ? `https://api.torn.com/company/${company}?selections=profile` : `https://api.torn.com/company/?selections=profile`;
const data = await fetchApi(endpoint);
const now = new Date();
// company ID is data.company.ID
cache.companies[data.company.ID] = {
name: data.company.name,
id: data.company.ID,
company_type: data.company.company_type,
director_id: data.company.director,
rating: data.company.rating,
updated: now.toISOString()
};
saveCache();
return data.company;
},
// item was a function with a .lookup property
item: Object.assign(
async (item) => {
const data = await fetchApi(`https://api.torn.com/v2/torn/${item}/items?sort=ASC`);
const now = new Date();
cache.items[item] = data.items[0]; // Assuming item is ID
if (cache.items[item]) {
cache.items[item].updated = now.toISOString();
}
saveCache();
return data.items[0];
},
{
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 < TIME_30D) {
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;
}
}
),
self: {
async id() {
if (!config.tornid) {
const url = `https://api.torn.com/user/?selections=basic&key=${config.torn}`;
const response = await fetch(url);
const data = await response.json();
config.tornid = data.player_id;
console.log(`Torn: Retrieved default ID as "${data.player_id}"`);
return data.player_id;
} else {
return config.tornid;
}
}
}
};
module.exports = api;