/** * Wraps XHR and fetch to listen for the request Torn sends when the * user clicks "Train". When such a request is detected, the response * is parsed via `parseTrainResponse`, the new attribute value is * compared against the previous one, and `onTrain({attr, delta, ts})` * is invoked. * * `parseTrainResponse(text, url)` is intentionally permissive and * returns `{ newValue, attr } | null`. The default implementation * tries JSON first, then a regex fallback. */ export function startRequestInterceptor({ prevValue, currentAttr, onTrain, onParseFail }) { let lastValue = prevValue; let warnedFor = null; const handle = (text, url) => { const parsed = parseTrainResponse(text, url, currentAttr); if (!parsed) { if (warnedFor !== url) { warnedFor = url; onParseFail && onParseFail(url); } return; } if (parsed.attr && currentAttr && parsed.attr !== currentAttr) return; const delta = parsed.newValue - lastValue; lastValue = parsed.newValue; if (delta > 0) onTrain({ attr: parsed.attr, delta, ts: Date.now() }); }; wrapXhr(handle); wrapFetch(handle); return { updatePrevValue(v) { lastValue = v; }, }; } function parseTrainResponse(text, url, fallbackAttr) { // Try JSON try { const j = JSON.parse(text); // Torn historically returns an HTML fragment; if it's JSON, look // for a known shape. This is a placeholder — adjust after manual // verification. if (j && typeof j === 'object' && 'newValue' in j && 'attr' in j) { return { newValue: Number(j.newValue), attr: String(j.attr) }; } } catch { /* not JSON */ } // Regex fallback: scan text for a number formatted like an attribute. // If we find one and the caller passed a fallbackAttr, use it; otherwise // the caller can choose to ignore the result. const m = text.match(/(\d{1,3}(?:,\d{3})+|\d{4,})/); if (m) { const newValue = parseInt(m[1].replace(/,/g, ''), 10); if (Number.isFinite(newValue) && newValue > 0) { return { newValue, attr: fallbackAttr || null }; } } return null; } function wrapXhr(handle) { if (XMLHttpRequest.prototype.send.__tatWrapped) return; const origOpen = XMLHttpRequest.prototype.open; const origSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function (method, url) { this.__tatUrl = String(url); return origOpen.apply(this, arguments); }; XMLHttpRequest.prototype.send = function () { this.addEventListener('load', () => { try { handle(this.responseText, this.__tatUrl); } catch { /* noop */ } }); return origSend.apply(this, arguments); }; XMLHttpRequest.prototype.send.__tatWrapped = true; } function wrapFetch(handle) { const origFetch = window.fetch; if (origFetch.__tatWrapped) return; window.fetch = async function (...args) { const url = typeof args[0] === 'string' ? args[0] : args[0]?.url || ''; const res = await origFetch.apply(this, args); try { const text = await res.clone().text(); handle(text, String(url)); } catch { /* noop */ } return res; }; window.fetch.__tatWrapped = true; }