Adds Torn Spy integration to Torn.
// ==UserScript==
// @name Torn Spy Integration
// @namespace tornspy-integration
// @version 0.9.3
// @description Adds Torn Spy integration to Torn.
// @author Neodork
// @match https://www.torn.com/*
// @connect www.tornspy.com
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_deleteValue
// @grant GM_listValues
// @grant GM_addStyle
// @license GNU GPLv3
// ==/UserScript==
(function() {
'use strict';
const DOMAIN = 'https://www.tornspy.com';
const TORN_SPY_KEY = 'tspy-key';
const MINI_PROFILE_BREAKDOWN = 'tspy-mini-bd';
const PAYMENT_TRACKER = 'tspy-ptrack';
const SPY_TRACKER = 'tspy-strack';
/**
* Global functions
*/
const setPermissions = (permissions) => {
GM_setValue('permissions', permissions)
}
const hasPermission = (permission) => {
return (GM_getValue('permissions')?.[permission]) ?? false;
}
const loaded_state = {}
const has_loaded = (subject) => {
return loaded_state[subject] ?? false;
}
const set_loaded = (subject) => {
loaded_state[subject] = true
}
const isMobile = () => {
return window.innerWidth < 785;
}
const search = (query, callback) => {
getAction({
type: "post",
action: "/autocompleteHeaderAjaxAction.php",
data: {
q: query,
option: 'player'
},
success: callback
})
};
const purchase = (step, id, money, tag, theanon, callback) => {
getAction({
type: "post",
action: "/sendcash.php",
data: {
step: step,
ID: id,
money: money,
tag: tag,
theanon: theanon
},
success: callback
})
};
const spy = (id, specialId, amount, usersId, callback) => {
ajaxWrapper({
url: 'companies.php?step=specialgo',
type: 'POST',
data: [
{name: 'ID', value: id},
{name: 'specialid', value: specialId},
{name: 'amount', value: amount},
{name: 'usersID', value: usersId},
],
oncomplete: callback
});
};
const waitForElement = (selector) => {
return new Promise(resolve => {
if (document.querySelector(selector)) {
return resolve(document.querySelector(selector));
}
const observer = new MutationObserver(mutations => {
if (document.querySelector(selector)) {
observer.disconnect();
resolve(document.querySelector(selector));
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
});
}
const initializeTornTooltip = (tooltipElements) => {
initializeTooltip(tooltipElements, 'white-tooltip');
}
const initializeTornItemInfo = (withListener) => {
document.querySelectorAll('.item-list>.item-weapon, .item-list>.item-armour').forEach((itemPlate) =>{
let lastOpen = null;
if(itemPlate.dataset.trigger === false){
return
}
if(withListener){
itemPlate.addEventListener("click", () => {
document.querySelectorAll('[data-itemId]').forEach((itemDescription) =>{
if(itemDescription.dataset.itemid != itemPlate.dataset.armoury && itemDescription.classList.contains('d-none') == false){
itemDescription.classList.add('d-none')
}
})
if(document.querySelector('[data-itemId="'+itemPlate.dataset.armoury+'"]').classList.contains('d-none')){
document.querySelector('[data-itemId="'+itemPlate.dataset.armoury+'"]').classList.remove('d-none')
}else{
document.querySelector('[data-itemId="'+itemPlate.dataset.armoury+'"]').classList.add('d-none')
}
});
}
});
}
const detective_svg = () => {
return `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16px" height="16px" viewBox="0 0 512 512" xml:space="preserve" fill="#777">
<path class="st0" d="M479.031,184.313l5.531-11.25h-94.453c-0.141-0.813-0.344-1.875-0.594-3.234 c-1.828-9.766-6.172-32.969-10.797-57.594v-0.016c-3.047-16.188-6.219-32.984-8.875-47c-1.328-7-2.531-13.313-3.531-18.5 s-1.797-9.25-2.313-11.781c-0.563-2.688-1.391-5.25-2.453-7.688c-1.078-2.438-2.391-4.75-3.922-6.875 c-4.563-6.469-10.906-11.516-18.156-15C332.203,1.906,324.016,0,315.531,0c-5.188,0-10.469,0.719-15.688,2.25 c-5.203,1.516-10.328,3.875-15.172,7.094c-6.141,4.109-11.813,6.219-16.688,7.375c-2.453,0.563-4.703,0.891-6.703,1.063 c-2,0.188-3.75,0.219-5.281,0.219c-1.469,0-2.719,0.063-3.922,0.063c-1.297,0-2.516-0.063-3.891-0.234 c-2.063-0.266-4.563-0.797-7.969-2.109c-3.438-1.281-7.75-3.344-13.219-6.594c-9.891-5.875-20.625-8.531-30.875-8.531 c-5.625,0-11.109,0.813-16.281,2.344c-7.75,2.281-14.813,6.188-20.438,11.609c-2.813,2.703-5.25,5.766-7.188,9.203 c-1.938,3.422-3.391,7.172-4.203,11.188h0.016c-0.531,2.531-1.313,6.594-2.313,11.781c-3,15.563-7.859,41.219-12.438,65.516 c-4.625,24.625-8.953,47.828-10.781,57.594c-0.25,1.359-0.469,2.422-0.609,3.234H27.438l5.531,11.25 c0.281,0.563,3,5.953,9.844,13.719c10.219,11.656,29.688,28.625,63.5,42.578c9.672,3.969,20.5,7.703,32.594,11.016v4.156 L176,298.656c0,0,42.906,8.125,46.391,6.969c3.469-1.156,33.609-31.313,33.609-31.313s30.156,30.156,33.625,31.313 S336,298.656,336,298.656l37.094-42.875v-4.156c40.391-11.031,66.484-26.875,82.625-40.469 C472.688,196.875,478.641,185.125,479.031,184.313z M148.656,115.125c3.031-16.188,6.219-33,8.859-46.984 c1.328-7,2.547-13.297,3.531-18.453c1-5.172,1.797-9.219,2.297-11.656l0,0c0.625-3.125,1.891-5.953,3.719-8.563 c2.75-3.875,6.844-7.219,11.875-9.563s10.969-3.688,17.188-3.672c7.563,0,15.5,1.953,22.906,6.328 c8.094,4.813,14.469,7.625,19.875,9.219c2.688,0.781,5.141,1.281,7.344,1.563c2.219,0.281,4.156,0.344,5.828,0.344 c1.563,0,2.859-0.063,3.922-0.063c1.781,0,4.063-0.047,6.688-0.281c3.938-0.344,8.688-1.125,13.906-2.813s10.906-4.281,16.75-8.188 c3.516-2.344,7.188-4.031,10.906-5.125s7.531-1.594,11.281-1.594c4.063,0,8.094,0.625,11.813,1.766 c5.609,1.703,10.578,4.609,14.281,8.203c1.828,1.781,3.375,3.75,4.563,5.844c1.188,2.078,2.031,4.281,2.5,6.594l0,0 c0.5,2.438,1.281,6.484,2.281,11.656c2.969,15.484,7.844,41.141,12.391,65.422c0.609,3.219,1.203,6.391,1.797,9.547H146.859 C147.453,121.5,148.047,118.328,148.656,115.125z M395.375,227.906c-31.469,12.25-76.313,21.969-139.375,21.969 c-44.016,0-79.156-4.75-107.063-11.781c-41.859-10.531-67.469-26.188-82.578-38.891c-4.563-3.828-8.156-7.422-10.953-10.516 h401.203c-0.547,0.594-1.109,1.219-1.703,1.844C445.031,200.906,426.844,215.656,395.375,227.906z"/> <path class="st0" d="M94.25,345.125c5.828-6.141,10.969-10.828,14.625-13.969c1.813-1.547,3.266-2.719,4.234-3.5 c0.484-0.375,0.859-0.656,1.109-0.844c0.109-0.078,0.203-0.141,0.25-0.188l0.047-0.031h0.016l-0.094-0.109l-0.031-0.047 l-9.094-12.484c-0.281,0.219-9.891,7.234-22.406,20.422c-12.5,13.203-27.938,32.594-39.719,57.625l-3.156,6.688l49,26.438 c-7.438,5.516-18.063,13.797-29.313,23.766c-10.375,9.203-21.234,19.859-30.594,31.234c-4.656,5.672-8.938,11.531-12.594,17.531 c-2.844,4.703-5.313,9.484-7.219,14.344h17.125c1-2.063,2.172-4.141,3.453-6.219c5.531-9.125,13.203-18.516,21.609-27.344 c12.625-13.266,26.844-25.281,37.859-33.922c5.516-4.328,10.234-7.813,13.563-10.203c1.672-1.188,2.984-2.109,3.875-2.75 c0.453-0.313,0.781-0.531,1-0.688c0.109-0.078,0.203-0.125,0.25-0.172l0.063-0.047h0.016l10.688-7.219l-58.203-31.406 C70.969,372.094,83.719,356.234,94.25,345.125z"/> <path class="st0" d="M495.469,497.656c-6.375-10.5-14.719-20.594-23.656-30c-13.391-14.063-28.109-26.469-39.531-35.438 c-3.438-2.703-6.594-5.094-9.313-7.094l49-26.438L468.828,392c-11.797-25.031-27.219-44.422-39.734-57.625 c-12.5-13.188-22.125-20.203-22.422-20.422l-9.031,12.422l-0.156,0.219c0.266,0.203,9.266,6.828,20.828,19.125 c10.406,11.063,22.906,26.719,33.094,46.313l-58.219,31.406l10.703,7.219c0.047,0.031,4.656,3.156,11.734,8.469 c7.063,5.313,16.594,12.828,26.5,21.656c9.938,8.813,20.219,18.938,28.781,29.375c4.266,5.219,8.094,10.5,11.25,15.688 c1.25,2.063,2.406,4.125,3.438,6.156h17.094C500.781,507.141,498.328,502.359,495.469,497.656z"/> <rect x="235.906" y="417.969" class="st0" width="40.188" height="28.406"/> <polygon class="st0" points="240.469,461.313 234.234,512 277.781,512 271.547,461.313 "/>
</svg>`
}
const toggle_display = (element) => {
if(element.classList.contains('d-none')){
element.classList.remove('d-none')
}else{
element.classList.add('d-none')
}
}
const set_display = (element, bool) => {
if(bool){
element.classList.remove('d-none')
}else{
element.classList.add('d-none')
}
}
const set_loading = (element, disabled, text) => {
if(!element){
return
}
element.disabled = disabled;
const titleElement = element.querySelector('[class^="title"], [class^="linkTitle"]')
if(titleElement){
titleElement.innerHTML = text
}else{
element.innerHTML = text
}
}
const attach_purchase_flow = () => {
document.querySelectorAll('.tspy-sale').forEach((saleWrapper) => {
if(saleWrapper.querySelector(".tspy-buy")){
saleWrapper.querySelector(".tspy-buy").addEventListener("click", () => {
set_display(saleWrapper.querySelector(".tspy-product"), false)
set_display(saleWrapper.querySelector(".tspy-purchase"), true)
});
}
if(saleWrapper.querySelector(".tspy-cancel-btn")){
saleWrapper.querySelector(".tspy-cancel-btn").addEventListener("click", () => {
set_display(saleWrapper.querySelector(".tspy-purchase"), false)
set_display(saleWrapper.querySelector(".tspy-product"), true)
});
}
if(saleWrapper.querySelector(".tspy-purchase-btn")) {
if(hasPaymentTracked(saleWrapper.querySelector('.tspy-purchase-btn').dataset.tag)) {
set_display(saleWrapper.querySelector(".tspy-product"), false)
set_display(saleWrapper.querySelector(".tspy-complete"), true)
}
saleWrapper.querySelector(".tspy-purchase-btn").addEventListener("click", (event) => {
set_loading(saleWrapper.querySelector(".tspy-purchase-btn"), true, 'Sending payment...')
set_display(saleWrapper.querySelector(".tspy-cancel-btn"), false)
purchase('cash1', event.target.dataset.target, event.target.dataset.price, event.target.dataset.tag, false, (response) => {
response = JSON.parse(response);
if(response.error){
saleWrapper.querySelector(".tspy-error").innerHTML = response.error.replace("area", '').replace('This', 'Buying reports');
set_display(saleWrapper.querySelector(".tspy-purchase"), false)
set_display(saleWrapper.querySelector(".tspy-failure"), true)
}else if(response.success === false){
saleWrapper.querySelector(".tspy-error").innerHTML = response.text.replace('<b>', '<b class="t-red">').replace('<b>', '<b class="t-green">');
set_display(saleWrapper.querySelector(".tspy-purchase"), false)
set_display(saleWrapper.querySelector(".tspy-failure"), true)
}else{
set_display(saleWrapper.querySelector(".tspy-purchase"), false)
set_display(saleWrapper.querySelector(".tspy-complete"), true)
if(getPaymentTracker()){
GM_setValue(`${PAYMENT_TRACKER}_${event.srcElement.dataset.tag}`, Date.now())
}
}
});
});
}
});
}
const attach_copy = () => {
document.querySelectorAll('[data-copy]').forEach(element => {
element.addEventListener("click", (event) => {
set_loading(element, true, 'Copying...')
GM_xmlhttpRequest(
{
method: 'POST',
url: `${DOMAIN}${element.dataset.copy}`,
responseType: 'text',
headers: { 'TORNSPY-KEY': getApiKey() },
onload: (response) => {
navigator.clipboard.writeText(response.response ?? response.responseText);
set_loading(element, false, 'Copy')
}
}
);
})
})
}
const cachedSpyRecord = (userId, element) => {
var record = GM_getValue(`${SPY_TRACKER}_${userId}`)
if(!record){
return;
}
record = JSON.parse(record);
const difference = record.timestamp / 1000 - element.querySelector('.tspy-spy-details').dataset.timestamp;
if(difference < 1){
GM_deleteValue(`${SPY_TRACKER}_${userId}`);
return;
}
updateSpyRecordView(record, element);
};
const updateSpyRecordError = (error, element) => {
element.querySelector('.tspy-spy-btn').innerHTML = 'Oops!';
element.querySelector('.tspy-spy-btn').classList.add('t-red');
element.querySelector('.tspy-spy-btn').classList.remove('t-yellow');
element.querySelector('.tspy-spy-btn').classList.remove('t-green');
element.querySelector('.tspy-record-head').innerHTML = `<span class="t-red">Oops!</span> Something went wrong...`;
element.querySelector('.tspy-record-body').innerHTML = `<span>${error}</span`;
element.querySelector('.tspy-record-head-mobile').innerHTML = `<span class="t-red">Oops!</span>`;
element.querySelector('.tspy-record-body-mobile').innerHTML = `<span class="t-red">${error}</span>`;
element.querySelector('.tspy-record').style.display = 'flex';
element.querySelector('.tspy-spy-details').style.display = 'none';
};
const updateSpyRecordView = (record, element) => {
if(Object.keys(record.exposed).length > 3){
element.querySelector('.tspy-spy-btn').classList.add("t-green")
element.querySelector('.tspy-spy-btn').classList.remove("t-yellow")
element.querySelector('.tspy-spy-btn').disabled = true;
element.querySelector('.tspy-spy-btn').innerHTML = 'Full';
element.querySelector('.tspy-record-head').innerHTML = `<span class="t-green">Full</span>`;
element.querySelector('.tspy-record-body').innerHTML = `<span>${Object.keys(record.exposed).join(', ')}</span>`;
element.querySelector('.tspy-record-head-mobile').innerHTML = `<span class="t-green">Full </span><div><strong>JP: </strong> ${record.pointsUsed}</div>`;
}else{
element.querySelector('.tspy-spy-btn').classList.add("t-yellow")
element.querySelector('.tspy-spy-btn').classList.remove("t-green")
element.querySelector('.tspy-record-head').innerHTML = `<span class="t-yellow">Partial</span>`;
element.querySelector('.tspy-record-body').innerHTML = `<span>${Object.keys(record.exposed).join(', ')}</span>`;
element.querySelector('.tspy-record-head-mobile').innerHTML = `<span class="t-yellow">Partial</span><div><strong>JP: </strong> ${record.pointsUsed}</div>`;
}
element.querySelector('.tspy-record').style.display = 'flex';
//element.querySelector('.tspy-spy-details').style.display = 'none';
element.querySelector('.tspy-record-head').innerHTML = element.querySelector('.tspy-record-head').innerHTML + `<span> (${new Date(record.timestamp).toLocaleString('en-GB', { timeZone: 'UTC' })})<span> <strong>JP used: </strong> ${record.pointsUsed}`;
element.querySelector('.tspy-record-body-mobile').innerHTML = `<span>${new Date(record.timestamp).toLocaleString('en-GB', { timeZone: 'UTC' })}<span>`;
};
const updateSpyRecord = (spyDetails, user, amount) => {
var record = GM_getValue(`${SPY_TRACKER}_${user.userID}`);
if(!record){
record = {
timestamp: Date.now(),
exposed: {},
pointsUsed: 0
}
}else{
record = JSON.parse(record);
}
record.timestamp = Date.now();
for (const [key, value] of Object.entries(spyDetails)) {
if(['money', 'moneyshow'].includes(key)){
continue;
}
if(value !== 'N/A'){
record.exposed[key] = value;
}
}
record.pointsUsed += parseInt(amount);
GM_setValue(`${SPY_TRACKER}_${user.userID}`, JSON.stringify(record));
return record;
};
const capitalize = s => s && s[0].toUpperCase() + s.slice(1);
/**
* Settings controls
*/
const hasApiKey = () => {
return GM_getValue(TORN_SPY_KEY)?.length > 16;
}
const getApiKey = () => {
return GM_getValue(TORN_SPY_KEY);
}
const getMiniBreakdown = () => {
return GM_getValue(MINI_PROFILE_BREAKDOWN) ?? true;
}
const getPaymentTracker = () => {
return GM_getValue(PAYMENT_TRACKER) ?? true;
}
const hasPaymentTracked = (tag) => {
return GM_getValue(`${PAYMENT_TRACKER}_${tag}`) ?? false;
}
/**
* Profile page support: https://www.torn.com/profiles.php?XID=*
*/
if(window.location.pathname === '/profiles.php' && hasApiKey()) {
waitForElement('.medals-wrapper').then(()=> {
profile(profile_id());
});
}
const profile = (id) => {
GM_xmlhttpRequest(
{
method: 'POST',
url: `${DOMAIN}/userscripts/profile`,
data: JSON.stringify({'xid': id}),
responseType: 'json',
headers: { 'TORNSPY-KEY': getApiKey() },
onload: (response) => {
response = response.response ?? JSON.parse(response.responseText);
if(response.style){
GM_addStyle(response.style);
}
setPermissions(response.permissions)
profile_page(response)
initializeTornTooltip($('.bonus-attachment-icons, .view-info'))
initializeTornItemInfo()
}
}
);
};
const profile_page = (response) => {
if(document.querySelector('.buttons-list')) {
response.buttons.forEach((button) =>{document.querySelector('.buttons-list').insertAdjacentHTML('beforeend', button)});
}
document.getElementById('profileroot').querySelector('.medals-wrapper').insertAdjacentHTML('beforeBegin', response.loadout);
document.getElementById('profileroot').querySelector('.medals-wrapper').insertAdjacentHTML('beforeBegin', response.statBar);
attach_purchase_flow()
}
const profile_id = () => {
return (new URLSearchParams(window.location.search)).get('XID');
}
/**
* Mini profile support.
*/
if(hasApiKey() && getMiniBreakdown()){
waitForElement('.profile-mini-root').then(()=> {
waitForElement('div[class^="profile-mini-_userImage"]').then(() => {
mini_profile(mini_profile_id());
const observer = new MutationObserver((mutationList, observer) => {
if(document.getElementById('profile-mini-root').querySelector('div[class^="profile-mini-"]')){
waitForElement('div[class^="profile-mini-_userImage"]').then(() => {
mini_profile(mini_profile_id());
});
}
});
observer.observe(document.getElementById('profile-mini-root'), { childList: true });
});
});
}
const mini_profile = (id) => {
GM_xmlhttpRequest({
method: 'POST',
url: `${DOMAIN}/userscripts/mini-profile`,
data: JSON.stringify({'xid': id}),
responseType: 'json',
headers: { 'TORNSPY-KEY': getApiKey() },
onload: (response) => {
response = response.response ?? JSON.parse(response.responseText);
setPermissions(response.permissions)
const height = getComputedStyle(document.getElementById('profile-mini-root').querySelector('div[class^="profile-mini-_userProfileWrapper"]')).height;
if(document.getElementById("profile-mini-root").querySelector('.icons') && response.statBar) {
document.getElementById("profile-mini-root").querySelector('.icons').insertAdjacentHTML('beforebegin', response.statBar);
}
if(document.getElementById("profile-mini-root").querySelector('.buttons-list')){
response.buttons.forEach((button) =>{document.getElementById("profile-mini-root").querySelector('.buttons-list').insertAdjacentHTML('beforeend', button)});
}
if(document.getElementById("profile-mini-root").querySelector('div[class^="profile-mini-_wrapper___"]')) {
// Allow the mini profile to scale larger than the set height.
document.getElementById("profile-mini-root").querySelector('div[class^="profile-mini-_wrapper___"]').style.maxHeight = 'fit-content';
}
// Adjust height of the mini profile.
if(getComputedStyle(document.getElementById('profile-mini-root').querySelector('div[class^="profile-mini-_arrow"]')).bottom === '-8px' && response.statBar){
const adjustedHeight = getComputedStyle(document.getElementById('profile-mini-root').querySelector('div[class^="profile-mini-_userProfileWrapper"]')).height;
const heightDiff = adjustedHeight.replace('px', '') - height.replace('px', '');
const top = document.getElementById('profile-mini-root').querySelector('div[class^="profile-mini-_wrapper"]').style.top.replace('px', '')
document.getElementById('profile-mini-root').querySelector('div[class^="profile-mini-_wrapper"]').style.top = `${(top - heightDiff)}px`;
}
}
});
};
const mini_profile_id = () => {
return document.querySelector('a[class^="profile-mini-_linkWrap___"][class*=" profile-mini-_flexCenter___"][href^="/profiles.php?XID="]').href.replace('https://www.torn.com/profiles.php?XID=', '');
};
/**
* Faction page support: https://www.torn.com/factions.php
*/
if(hasApiKey() && window.location.pathname === '/factions.php' && !document.getElementById('tspy-view-bazaar')) {
waitForElement('.content-title').then(()=> {
waitForElement('.faction-profile').then(() => {
waitForElement('.f-war-list.members-list').then(()=> {
faction_profile();
});
});
});
}
var purchase_overview_loaded = false;
var spy_overview_loaded = false;
var activeTable = 'default';
const faction_profile = (ids) => {
GM_xmlhttpRequest({
method: 'POST',
url: `${DOMAIN}/userscripts/faction/profile`,
data: JSON.stringify({'fid': faction_id()}),
responseType: 'json',
headers: { 'TORNSPY-KEY': getApiKey() },
onload: (response) => {
response = response.response ?? JSON.parse(response.responseText);
setPermissions(response.permissions)
if(response.style){
GM_addStyle(response.style);
}
document.querySelector('.faction-info-wrap.restyle.another-faction').insertAdjacentHTML('beforeBegin', response.actions);
document.getElementById('tspy-spy-purchase-icon').addEventListener("click", (event) => {
if(purchase_overview_loaded){
toggle_active_table('purchase');
toggle_purchase_overview();
activeTable = activeTable!=='purchase'?'purchase':'default';
}
if(!purchase_overview_loaded){
purchase_overview_loaded = true;
document.getElementById('tspy-spy-purchase').innerHTML = 'Loading...';
purchase_overview_load();
}
});
document.getElementById('tspy-spy-spy-icon').addEventListener("click", (event) => {
if(spy_overview_loaded){
toggle_active_table('spy');
toggle_spy_overview();
activeTable = activeTable!=='spy'?'spy':'default';
}
if(!spy_overview_loaded){
spy_overview_loaded = true;
document.getElementById('tspy-spy-spy').innerHTML = 'Loading...';
spy_overview_load();
}
});
}
});
};
const toggle_purchase_overview = () => {
const tspyPurchaseHead = document.getElementById('tspy-purchase-thead');
if(tspyPurchaseHead){
tspyPurchaseHead.style.display = (tspyPurchaseHead.style.display ==='none')?'flex':'none';
}
document.querySelector('.f-war-list.members-list').querySelector('.table-body').querySelectorAll('.table-row').forEach((element) => {
const spyPurchaseRow = element.querySelector('.tspy-purchase-row');
element.querySelectorAll('.tspy-purchase-row').forEach((element)=>{
element.style.display = (element.style.display)==='none'?'flex':'none';
});
});
};
const purchase_overview_load = () => {
GM_xmlhttpRequest({
method: 'POST',
url: `${DOMAIN}/userscripts/faction/purchase`,
data: JSON.stringify({'mids': member_ids()}),
responseType: 'json',
headers: { 'TORNSPY-KEY': getApiKey() },
onload: (response) => {
response = response.response ?? JSON.parse(response.responseText);
setPermissions(response.permissions)
purchase_overview(response);
purchase_overview_loaded = true;
}
});
};
const purchase_overview = (response) => {
toggle_active_table('purchase');
activeTable = activeTable!=='purchase'?'purchase':'default';
document.getElementById('tspy-spy-purchase').innerHTML = 'Purchase reports';
document.querySelector('.f-war-list.members-list').querySelector('ul.table-header').insertAdjacentHTML('beforeEnd', response.heading);
document.querySelector('.f-war-list.members-list').querySelector('.table-body').querySelectorAll('.table-row').forEach((row) => {
const responseRow = response.spyRows[row.querySelector('a[href^="/profiles.php?XID="]').href.replace('https://www.torn.com/profiles.php?XID=', '')]
row.insertAdjacentHTML('beforeEnd', responseRow.purchase);
const reportHash = row.querySelector('.tspy-purchase-btn')?.dataset.tag;
if(reportHash && hasPaymentTracked(reportHash)){
row.querySelector(`.tspy-cta`).style.display = 'none';
row.querySelector(`.tspy-purchase-details`).style.display = 'none';
row.querySelector(`.tspy-complete`).style.display = 'block';
}
if(reportHash){
row.querySelector(`.tspy-sale-btn`).addEventListener("click", (event) => {
row.querySelector(`.tspy-cta`).style.display = 'none';
row.querySelector(`.tspy-purchase-details`).style.display = 'none';
row.querySelector(`.tspy-purchase`).style.display = 'block';
});
row.querySelectorAll(`.tspy-cancel-btn`).forEach((element) => {
element.addEventListener('click', (event) => {
row.querySelector(`.tspy-purchase`).style.display = 'none';
row.querySelector(`.tspy-cta`).style.display = 'block';
row.querySelector(`.tspy-purchase-details`).style.display = 'block';
})
});
row.querySelectorAll(`.tspy-purchase-btn`).forEach((element) => {
element.addEventListener('click', (event) => {
event.srcElement.disabled = true;
event.srcElement.innerHTML = 'Sending payment...'
row.querySelectorAll(`.tspy-cancel-btn`).forEach((element) => {
element.style.display = 'none';
});
purchase('cash1', event.srcElement.dataset.target, event.srcElement.dataset.price, event.srcElement.dataset.tag, false, (response) => {
response = JSON.parse(response);
if(response.error){
row.querySelector(`.tspy-purchase`).style.display = 'none';
row.querySelector(`.tspy-error`).innerHTML = response.error.replace("area", '').replace('This', 'Buying reports');
row.querySelector(`.tspy-failure`).style.display = 'block';
}else if(response.success === false){
row.querySelector(`.tspy-error`).innerHTML = response.text.replace('<b>', '<b class="t-red">').replace('<b>', '<b class="t-green">');
row.querySelector(`.tspy-purchase`).style.display = 'none';
row.querySelector(`.tspy-failure`).style.display = 'block';
}else{
row.querySelector(`.tspy-purchase`).style.display = 'none';
row.querySelector(`.tspy-complete`).style.display = 'block';
if(getPaymentTracker()){
GM_setValue(`${PAYMENT_TRACKER}_${event.srcElement.dataset.tag}`, Date.now())
}
}
});
})
});
}
});
};
const spy_overview_load = () => {
GM_xmlhttpRequest({
method: 'POST',
url: `${DOMAIN}/userscripts/faction/spy`,
data: JSON.stringify({'mids': member_ids()}),
responseType: 'json',
headers: { 'TORNSPY-KEY': getApiKey() },
onload: (response) => {
response = response.response ?? JSON.parse(response.responseText);
setPermissions(response.permissions)
spy_overview(response);
spy_overview_loaded = true;
}
});
};
const spy_overview = (response) => {
toggle_active_table('spy');
activeTable = activeTable!=='spy'?'spy':'default';
document.getElementById('tspy-spy-spy').innerHTML = 'Spy';
document.querySelector('.f-war-list.members-list').querySelector('ul.table-header').insertAdjacentHTML('beforeEnd', response.heading);
document.querySelector('.f-war-list.members-list').querySelector('.table-body').querySelectorAll('.table-row').forEach((row) => {
const userId = row.querySelector('a[href^="/profiles.php?XID="]').href.replace('https://www.torn.com/profiles.php?XID=', '');
const responseRow = response.spyRows[userId]
row.insertAdjacentHTML('beforeEnd', responseRow.spy);
cachedSpyRecord(userId, row);
row.querySelector(`.tspy-spy-btn`).addEventListener("click", (event) => {
row.querySelector('.tspy-spy-btn').disabled = true;
row.querySelector('.tspy-spy-btn').innerHTML = '...';
spy(event.srcElement.dataset.id, event.srcElement.dataset.special, event.srcElement.dataset.amount, event.srcElement.dataset.spy, (response) => {
response = JSON.parse(response.responseText);
if(response.success === false){
updateSpyRecordError(response.text.replace("area", '').replace('This', 'Using your job special'), row);
return;
}
if(response.result.error){
updateSpyRecordError(response.result.error, row);
return;
}
row.querySelector('.tspy-spy-btn').disabled = false;
row.querySelector('.tspy-spy-btn').innerHTML = 'Spy';
updateSpyRecordView(
updateSpyRecord(response.result.msg, response.result.user, response.amount),
row
);
document.getElementById('tspy-spy-thead-amount').innerHTML = response.pointsLeft;
document.getElementById('tspy-spy-thead-amount').parentNode.style.display = 'block';
});
});
});
};
const toggle_spy_overview = () => {
const tspySpyHead = document.getElementById('tspy-spy-thead');
if(tspySpyHead){
tspySpyHead.style.display = (tspySpyHead.style.display ==='none')?'flex':'none';
}
document.querySelector('.f-war-list.members-list').querySelector('.table-body').querySelectorAll('.table-row').forEach((element) => {
element.querySelectorAll('.tspy-spy-row').forEach((element)=>{
element.style.display = (element.style.display)==='none'?'flex':'none';
});
});
};
const toggle_active_table = (targetTable) => {
if(activeTable === targetTable){
toggle_table();
return;
}
if(activeTable === 'purchase'){
toggle_purchase_overview();
}else if(activeTable === 'spy'){
toggle_spy_overview();
}else{
toggle_table();
}
};
const toggle_table = () => {
const memberIcons = document.querySelector('.f-war-list.members-list').querySelector('ul.table-header').querySelector('.member-icons');
const position = document.querySelector('.f-war-list.members-list').querySelector('ul.table-header').querySelector('.position');
const days = document.querySelector('.f-war-list.members-list').querySelector('ul.table-header').querySelector('.days');
const status = document.querySelector('.f-war-list.members-list').querySelector('ul.table-header').querySelector('.status');
const lvl = document.querySelector('.f-war-list.members-list').querySelector('ul.table-header').querySelector('.lvl');
position.style.display = (position.style.display ==='none')?'flex':'none';
days.style.display = (days.style.display ==='none')?'flex':'none';
status.style.display = (status.style.display ==='none')?'flex':'none';
if(!memberIcons.classList.contains('hide-mobile')){
memberIcons.classList.add('hide-mobile');
memberIcons.classList.add('hide-desktop');
}else{
memberIcons.classList.remove('hide-mobile');
memberIcons.classList.remove('hide-desktop');
}
if(!lvl.classList.contains('hide-mobile')){
lvl.classList.add('hide-mobile')
}else{
lvl.classList.remove('hide-mobile');
}
const memberRows = document.querySelector('.f-war-list.members-list').querySelector('.table-body').querySelectorAll('.table-row');
memberRows.forEach((element) => {
const memberIconsInline = element.querySelector('.member-icons');
const positionInline = element.querySelector('.position');
const daysInline = element.querySelector('.days');
const statusInline = element.querySelector('.status');
const lvl = element.querySelector('.lvl');
positionInline.style.display = (positionInline.style.display ==='none')?'flex':'none';
daysInline.style.display = (daysInline.style.display ==='none')?'flex':'none';
statusInline.style.display = (statusInline.style.display ==='none')?'flex':'none';
if(!memberIconsInline.classList.contains('hide-mobile')){
memberIconsInline.classList.add('hide-mobile');
memberIconsInline.classList.add('hide-desktop');
}else{
memberIconsInline.classList.remove('hide-mobile');
memberIconsInline.classList.remove('hide-desktop');
}
if(!lvl.classList.contains('hide-mobile')){
lvl.classList.add('hide-mobile')
}else{
lvl.classList.remove('hide-mobile');
}
});
};
const faction_id = () => {
const id = (new URLSearchParams(window.location.search)).get('ID');
if(id){
return id;
}
if(document.getElementById('top-page-links-list').querySelector('a[href^="/page.php?sid=factionWarfare#/ranked/"]')){
return document.getElementById('top-page-links-list').querySelector('a[href^="/page.php?sid=factionWarfare#/ranked/"]').href.replace('https://www.torn.com/page.php?sid=factionWarfare#/ranked/', '')
}
return document.querySelector('.forum-thread').href.replace('https://www.torn.com/forums.php#!p=forums&f=999&b=1&a=', '');
}
const member_ids = () => {
const mids = [];
document.querySelector('.f-war-list.members-list').querySelector('.table-body').querySelectorAll('.table-row').forEach((element)=>{
mids.push(element.querySelector('a[href^="/profiles.php?XID="]').href.replace('https://www.torn.com/profiles.php?XID=', ''));
})
return mids;
};
/**
* Settings page support: https://www.torn.com/preferences.php?tab=tornspy
*/
if(window.location.pathname === '/preferences.php' && (new URLSearchParams(window.location.search)).get('tab') === 'tornspy'){
waitForElement('.ui-tabs-nav').then(()=> {
settings();
});
}
const settings = (id) => {
GM_xmlhttpRequest({
method: 'POST',
url: `${DOMAIN}/userscripts/settings`,
responseType: 'json',
onload: (response) => {
response = response.response ?? JSON.parse(response.responseText);
setPermissions(response.permissions)
if(document.querySelector('.ui-tabs-nav')){
document.querySelector('#prefs-tab-menu').innerHTML = response.page;
document.querySelector('.prefs-tab-title').innerHTML = 'Torn Spy settings'
document.getElementById('tspy-apikey').value = getApiKey() ?? '';
document.getElementById('tspy-mini-bd-on').checked = getMiniBreakdown() ?? true;
document.getElementById('tspy-mini-bd-off').checked = !getMiniBreakdown() ?? false;
document.getElementById('tspy-payment-track-on').checked = getPaymentTracker() ?? true;
document.getElementById('tspy-payment-track-off').checked = !getPaymentTracker() ?? false;
}
if(document.getElementById('tspy-settings')){
document.getElementById('tspy-save-btn').addEventListener("click", () => {
if(document.getElementById('tspy-apikey').value.length <= 16){
document.getElementById('tspy-key-error').innerHTML = 'Oops! your key needs to be longer than 16 characters!';
}else{
document.getElementById('tspy-key-error').style.display = 'none';
GM_setValue(TORN_SPY_KEY, document.getElementById('tspy-apikey').value);
}
GM_setValue(MINI_PROFILE_BREAKDOWN, document.getElementById('tspy-mini-bd-on').checked);
document.getElementById('tspy-success').innerHTML = 'Settings updated!';
});
document.getElementById('tspy-reset-btn').addEventListener("click", () => {
GM_deleteValue(TORN_SPY_KEY);
GM_deleteValue(MINI_PROFILE_BREAKDOWN);
document.getElementById('tspy-apikey').value = '';
document.getElementById('tspy-success').innerHTML = 'Torn Spy integration settings reset!';
});
}
if(document.getElementById('tspy-ptrack-clear-btn')){
document.getElementById('tspy-ptrack-clear-btn').addEventListener("click", () => {
GM_listValues().forEach((value) => {
if(value.startsWith(`${PAYMENT_TRACKER}_`)){
GM_deleteValue(value);
}
})
document.getElementById('tspy-ptrack-message').innerHTML = 'Tracked payments cleared!'
});
}
}
});
};
/**
* Icon support
*/
waitForElement('ul[class^="status-icons"').then(()=> {
const icon = `<li id="tspy-icon" style="background-image:url('https://www.tornspy.com/assets/img/favicon.ico');"><a href="https://www.torn.com/preferences.php?tab=tornspy" aria-label="Torn Spy" tabindex="0" data-is-tooltip-opened="false"></a></li>`;
const existingIcon = document.querySelector('ul[class^="status-icons"]')?.querySelector('#tspy-icon');
if (existingIcon) {
existingIcon.remove();
}
document.querySelector('ul[class^="status-icons"]').insertAdjacentHTML('beforeEnd', icon)
const observer = new MutationObserver((mutationList, observer) => {
if(document.querySelector('ul[class^="status-icons"]')?.querySelector('#tspy-icon')){
return;
}
document.querySelector('ul[class^="status-icons"]').insertAdjacentHTML('beforeEnd', icon)
});
observer.observe(document.getElementById('sidebarroot'), { childList: true, subtree: true });
});
/**
* No API key is set prompt.
*/
if(!hasApiKey()){
document.querySelector('.content-title').insertAdjacentHTML('afterEnd', `<div class="info-msg-cont green border-round m-top10">
<div class="info-msg border-round">
<i class="info-icon"></i>
<div class="delimiter">
<div class="msg right-round" role="alert" aria-live="polite">
<ul><li>Torn Spy API key is not set, click <a class="t-blue h" href="preferences.php?tab=tornspy">here</a> to set your API Key!</li></ul>
</div>
</div>
</div>
</div>`);
}
/**
* Attack / Loadout support
*/
if(hasApiKey() && hasPermission('loadouts') && window.location.pathname === '/page.php' && (new URLSearchParams(window.location.search)).get('sid') === 'attack') {
waitForElement('#react-root').then(()=> {
waitForElement('div[class^="playerWindow"').then(()=> {
if(hasPermission('gun_shop')){
window.addEventListener("fetch-listener", sendLoadoutToTornspy);
}else{
const fightButton = document.getElementById('react-root').querySelector('button[class^="torn-btn"]')
if(fightButton && fightButton.innerText === 'START FIGHT') {
fightButton.addEventListener("click", (event) => {
window.addEventListener("fetch-listener", sendLoadoutToTornspy);
});
}
}
});
});
function addFetchListener() {
const originalFetch = unsafeWindow.fetch;
unsafeWindow.fetch = async (url, init = {}) => {
const response = await originalFetch(url, init);
const clonedResponse = response.clone();
clonedResponse.text().then((text) => {
window.dispatchEvent(new CustomEvent(`fetch-listener`, {
detail: {
url,
body: init.body || null,
response: text
}
}));
}).catch(console.error);
return response;
};
}
addFetchListener()
}
function sendLoadoutToTornspy(event) {
const payload = JSON.parse(event.detail.response)
if(["started", "end"].includes(payload.DB?.attackStatus) || payload.DB?.showEnemyItems){
GM_xmlhttpRequest({
method: 'POST',
url: `${DOMAIN}/userscripts/loadout/add`,
data: JSON.stringify(mapAttackData(payload)),
responseType: 'json',
headers: { 'TORNSPY-KEY': getApiKey() },
onload: (response) => {
response = response.response ?? JSON.parse(response.responseText);
setPermissions(response.permissions)
const title = document.querySelector('h4[class^="title"]')
title.insertAdjacentHTML('afterend', response.status);
}
});
window.removeEventListener("fetch-listener", sendLoadoutToTornspy);
}
}
const mapAttackData = (attackDataPayload) => {
return {
fightId: attackDataPayload.DB.fightID,
target: attackDataPayload.DB.defenderUser.userID,
maxLife: attackDataPayload.DB.defenderUser.maxlife,
modifiers: {
strength: attackDataPayload.DB.defenderUser.statsModifiers.strength.value,
speed: attackDataPayload.DB.defenderUser.statsModifiers.speed.value,
dexterity: attackDataPayload.DB.defenderUser.statsModifiers.dexterity.value,
defense: attackDataPayload.DB.defenderUser.statsModifiers.defense.value,
damage: attackDataPayload.DB.defenderUser.statsModifiers.damage.value,
},
loadout: Object.entries(attackDataPayload.DB.defenderItems).flatMap(
([slot, data]) => data.item.map(item => ({
itemId: item.ID,
armoryId: item.armoryID,
ammoType: item.ammotype,
ammoPreference: item.equipSlot == '1' ? attackDataPayload.DB.defenderAmmoPreferences.primary :
item.equipSlot == '2' ? attackDataPayload.DB.defenderAmmoPreferences.secondary : 0,
upgrades: item.currentUpgrades?.map(upgrade => ({
upgradeId: upgrade.upgradeID,
icon: upgrade.icon
}))
}))
)
}
}
/**
* Marketplace support
*/
if(hasApiKey() && window.location.pathname === '/page.php' &&
(new URLSearchParams(window.location.search)).get('sid') === 'ItemMarket' &&
!window.__tspyMarketHooked
) {
window.__tspyMarketHooked = true;
waitForElement('#item-market-root').then(()=> {
waitForElement('[class^="categoriesWrapper"]').then(()=> {
waitForElement('[class^="most-popular"]').then(()=> {
marketButton()
if((new URLSearchParams(window.location.search)).get('tspy') === 'true'){
marketplaceRequest()
}
})
})
});
}
const marketButton = () => {
const existing = document.getElementById('tspy-marketplace-wrapper');
if (existing) {
existing.remove();
}
const mostPopularButton = document.querySelector('[class^="mostPopular"]');
const tornSpyMarketWrapper = mostPopularButton.cloneNode(true)
tornSpyMarketWrapper.id = "tspy-marketplace-wrapper";
[...tornSpyMarketWrapper.attributes].forEach(attr => {
if (['id', 'class'].includes(attr.name) === false) {
tornSpyMarketWrapper.removeAttribute(attr.name);
}
});
tornSpyMarketWrapper.style.borderRadius = "var(--item-market-border-radius)";
tornSpyMarketWrapper.querySelector('[class^="most-popular"]').outerHTML = detective_svg()
const tornSpyMarketButton = tornSpyMarketWrapper.querySelector('button');
tornSpyMarketButton.id = "tspy-marketplace-button";
[...tornSpyMarketButton.attributes].forEach(attr => {
if (['id', 'class'].includes(attr.name) === false) {
tornSpyMarketButton.removeAttribute(attr.name);
}
});
tornSpyMarketButton.classList.forEach((classname) => {
if(classname.includes('selected')){
tornSpyMarketButton.classList.remove(classname);
}
})
tornSpyMarketButton.style.background = "var(--shared-category-icon-background);";
tornSpyMarketButton.style.color = "var(--item-market-color);";
if(tornSpyMarketButton.querySelector('[class^="title"]')){
tornSpyMarketButton.querySelector('[class^="title"]').innerHTML = 'Torn Spy Marketplace'
}
mostPopularButton.insertAdjacentHTML('afterEnd', tornSpyMarketWrapper.outerHTML);
document.getElementById('tspy-marketplace-wrapper').addEventListener("click", (event) => {
if(has_loaded('marketplace')){
set_display(document.querySelector('[class^="marketWrapper"]'), false)
set_display(document.querySelector('[class^="tspy-marketplace-wrapper"]'), true)
set_display(document.querySelector('[class^="appHeaderWrapper"]'), false)
set_display(document.querySelector('[class^="app-header-wrapper"]'), true)
}else{
marketplaceRequest()
}
const currentPanel = get_current_panel()
const url = new URL(window.location.href);
url.searchParams.set('tspy', true);
url.searchParams.set('tspy-panel', currentPanel !== null?currentPanel:'welcome-panel');
window.history.replaceState({}, "", url);
})
};
const get_current_panel = () => {
const panels = document.getElementById('tspy-marketplace')?.querySelectorAll('.panel');
if(!panels){
return null
}
for (const element of panels) {
if (!element.classList.contains('d-none')) {
return element.id;
}
}
return null;
};
const marketplace = (response) => {
set_display(document.querySelector('[class^="marketWrapper"]'), false)
set_display(document.querySelector('[class^="appHeaderWrapper"]'), false)
document.querySelector('[class^="appHeaderWrapper"]').insertAdjacentHTML('beforeBegin', response.header);
document.querySelector('[class^="marketWrapper"]').insertAdjacentHTML('beforeBegin', response.marketplace);
if(isMobile()){
const categories = document.querySelector('.tspy-marketplace-wrapper').querySelector('.tspy-category-wrapper')
document.querySelector('.tspy-marketplace-wrapper').insertAdjacentHTML('afterBegin', categories.outerHTML);
categories.outerHTML = '';
}
load_state_from_url()
global_search_input()
document.querySelectorAll('.tspy-category-wrapper [id^="tspy-marketplace-"]').forEach(el => {
const match = el.id.startsWith('tspy-marketplace-');
if (!match) {
return;
}
const key = el.id.replace('tspy-marketplace-', '') + '-panel';
el.addEventListener("click", () => {
loaders[key]?.();
});
});
document.getElementById('tspy-marketplace-back-button').addEventListener("click", (event) => {
set_display(document.querySelector('[class^="marketWrapper"]'), true)
set_display(document.querySelector('[class^="tspy-marketplace-wrapper"]'), false)
set_display(document.querySelector('[class^="appHeaderWrapper"]'), true)
set_display(document.querySelector('[class^="app-header-wrapper"]'), false)
const url = new URL(window.location.href);
url.searchParams.delete('tspy');
url.searchParams.delete('tspy-query');
url.searchParams.delete('tspy-panel');
window.history.replaceState({}, "", url);
})
attach_create_request_flow()
};
const marketplaceRequest = () => {
set_loading(document.getElementById('tspy-marketplace-button'), true, isMobile()?'...':'Loading...')
GM_xmlhttpRequest(
{
method: 'POST',
url: `${DOMAIN}/userscripts/marketplace`,
responseType: 'json',
headers: { 'TORNSPY-KEY': getApiKey() },
onload: (response) => {
response = response.response ?? JSON.parse(response.responseText);
if(response.style){
GM_addStyle(response.style);
}
setPermissions(response.permissions)
set_loaded('marketplace')
set_loaded('requests-create-overview-panel')
set_loading(document.getElementById('tspy-marketplace-button'), false, isMobile()?detective_svg():'Torn Spy Marketplace')
marketplace(response)
},
}
);
};
const createOverviewLoader = ({panelId, loadFn, loadArgs = [] }) => {
return () => {
const errorPanel = document.getElementById('make-request-error-panel');
if(errorPanel){
errorPanel.outerHTML = ''
}
if (has_loaded(panelId)) {
show_marketplace_panel(panelId)
} else {
loadFn(...(loadArgs.map(arg => typeof arg === 'function' ? arg() : arg)))
}
}
}
const marketplace_request_overview = (data, requestOverviewType) => {
set_loading(document.getElementById(`tspy-marketplace-requests-${requestOverviewType}-overview`), true, 'Loading...')
GM_xmlhttpRequest(
{
method: 'POST',
url: `${DOMAIN}/userscripts/marketplace/request/${requestOverviewType}/overview`,
data: JSON.stringify(data),
responseType: 'json',
headers: { 'TORNSPY-KEY': getApiKey() },
onload: (response) => {
response = response.response ?? JSON.parse(response.responseText);
document.getElementById('tspy-marketplace').insertAdjacentHTML('afterBegin', response.panel)
show_marketplace_panel(`requests-${requestOverviewType}-overview-panel`)
set_loaded(`requests-${requestOverviewType}-overview-panel`)
set_loading(document.getElementById(`tspy-marketplace-requests-${requestOverviewType}-overview`), false, `${capitalize(requestOverviewType)} requests`)
attach_pagination(paginated_requests(requestOverviewType));
if(requestOverviewType === 'available'){
attach_accept_request_flow()
}
if(requestOverviewType === 'created'){
attach_purchase_flow()
}
if(requestOverviewType === 'accepted'){
attach_spy_request_flow()
}
},
}
);
}
const reports_overview = (data, reportType) => {
const reportSelector = reportType.replace('_', '-')
set_loading(document.getElementById(`tspy-marketplace-${reportSelector}-overview`), true, 'Loading...')
GM_xmlhttpRequest(
{
method: 'POST',
url: `${DOMAIN}/userscripts/marketplace/reports/${reportType}`,
data: JSON.stringify(data),
responseType: 'json',
headers: { 'TORNSPY-KEY': getApiKey() },
onload: (response) => {
response = response.response ?? JSON.parse(response.responseText);
if(document.getElementById(`${reportSelector}-overview-panel`)){
document.getElementById(`${reportSelector}-overview-panel`).outerHTML = response.panel
}else{
document.getElementById('tspy-marketplace').insertAdjacentHTML('beforeEnd', response.panel)
}
show_marketplace_panel(`${reportSelector}-overview-panel`)
set_loaded(`${reportType.replace('_', '-')}-overview-panel`)
set_loading(document.getElementById(`tspy-marketplace-${reportSelector}-overview`), false, `${capitalize(reportType.replace('_', ' '))}s`)
attach_purchase_flow()
attach_copy()
attach_pagination(paginated_reports(reportType));
initializeTornItemInfo(false)
},
}
);
}
const loaders = {
'requests-create-overview-panel': createOverviewLoader({
panelId: 'requests-create-overview-panel',
loadFn: marketplace_request_overview,
loadArgs: [{}, 'create']
}),
'requests-created-overview-panel': createOverviewLoader({
panelId: 'requests-created-overview-panel',
loadFn: marketplace_request_overview,
loadArgs: [{}, 'created']
}),
'requests-accepted-overview-panel': createOverviewLoader({
panelId: 'requests-accepted-overview-panel',
loadFn: marketplace_request_overview,
loadArgs: [{}, 'accepted']
}),
'requests-available-overview-panel': createOverviewLoader({
panelId: 'requests-available-overview-panel',
loadFn: marketplace_request_overview,
loadArgs: [{}, 'available']
}),
'requests-failed-overview-panel': createOverviewLoader({
panelId: 'requests-failed-overview-panel',
loadFn: marketplace_request_overview,
loadArgs: [{}, 'failed']
}),
'requests-archived-overview-panel': createOverviewLoader({
panelId: 'requests-archived-overview-panel',
loadFn: marketplace_request_overview,
loadArgs: [{}, 'archived']
}),
'stat-report-overview-panel': createOverviewLoader({
panelId: 'stat-report-overview-panel',
loadFn: reports_overview,
loadArgs: [() => ({ query: document.getElementById('tspy-global-search-input').value }), 'stat_report']
}),
'loadout-report-overview-panel': createOverviewLoader({
panelId: 'loadout-report-overview-panel',
loadFn: reports_overview,
loadArgs: [() => ({ query: document.getElementById('tspy-global-search-input').value }), 'loadout_report']
}),
'references-report-overview-panel': createOverviewLoader({
panelId: 'references-report-overview-panel',
loadFn: reports_overview,
loadArgs: [() => ({ query: document.getElementById('tspy-global-search-input').value }), 'references_report']
}),
'friendorfoe-report-overview-panel': createOverviewLoader({
panelId: 'friendorfoe-report-overview-panel',
loadFn: reports_overview,
loadArgs: [() => ({ query: document.getElementById('tspy-global-search-input').value }), 'friendorfoe_report']
})
}
const load_state_from_url = () => {
const params = new URLSearchParams(window.location.search)
const query = params.get('tspy-query')
document.getElementById('tspy-global-search-input').value = query || ''
const panel = params.get('tspy-panel')
if (loaders[panel]) {
loaders[panel]()
}
}
const attach_create_request_flow = () => {
set_display(document.getElementById('tspy-spy-report-select'), false)
document.getElementById('tspy-report-type-select').addEventListener("change", (event) => {
if(document.getElementById('tspy-report-type-select').value === 'stat_report'){
set_display(document.getElementById('tspy-spy-report-select'), true)
return
}
set_display(document.getElementById('tspy-spy-report-select'), false)
})
document.getElementById('finalise-button').addEventListener("click", (event) => {
const spyReportValue = document.getElementById('tspy-spy-report-type-select').value === 'Select spy report type...'?null:document.getElementById('tspy-spy-report-type-select').value
const deadline = document.getElementById('tspy-deadline-select').value === 'Select deadline...'?null:document.getElementById('tspy-deadline-select').value
validateRequest({
reportType: document.getElementById('tspy-report-type-select').value,
spyReportType: spyReportValue,
deadline: deadline,
pricePerReport: document.getElementById('tspy-price-per-report-input').value,
targets: [...document.getElementById('tspy-targets').querySelectorAll('[data-id]')].map(el => el.dataset.id)
});
})
search_input()
}
const reset_create_request_flow = () => {
document.getElementById("tspy-report-type-select").selectedIndex = 0;
document.getElementById("tspy-spy-report-type-select").selectedIndex = 0;
document.getElementById("tspy-deadline-select").selectedIndex = 0;
document.getElementById("tspy-price-per-report-input").value = '';
document.getElementById("tspy-search-input").value = '';
document.getElementById("tspy-targets").innerHTML = '';
}
const paginated_reports = (reportType) => {
return function (page, query){
reports_overview({
page: page,
query: query
}, reportType)
}
}
const paginated_requests = (requestOverviewType) => {
return function (page, query){
marketplace_request_overview({
page: page,
query: query
}, requestOverviewType)
}
}
const attach_pagination = (callback) => {
const query = document.getElementById('tspy-global-search-input').value
document.querySelectorAll('.page-number')?.forEach(element => {
element.addEventListener("click", (event) => {
const page = parseInt(event.currentTarget.querySelector('.page-nb').innerHTML)
const currentPage = parseInt(document.querySelector('.page-number.active').querySelector('.page-nb').innerHTML)
if(Number.isInteger(page) && page != currentPage){
callback(page, query)
}
event.preventDefault()
})
})
document.querySelector('.pagination-left')?.addEventListener("click", (event) => {
const page = parseInt(document.querySelector('.page-number.active').querySelector('.page-nb').innerHTML)
if(Number.isInteger(page) && page != 1){
callback(page - 1, query)
}
event.preventDefault()
})
document.querySelector('.pagination-right')?.addEventListener("click", (event) => {
const page = parseInt(document.querySelector('.page-number.active').querySelector('.page-nb').innerHTML)
const lastPage = parseInt(document.querySelector('.page-number.last')?.querySelector('.page-nb').innerHTML)
if(Number.isInteger(page) && Number.isInteger(lastPage) && page != lastPage){
callback(page + 1, query)
}
event.preventDefault()
})
}
const attach_spy_request_flow = () => {
document.querySelectorAll('.tspy-spy-row').forEach((row) => {
const button = row.querySelector('.tspy-spy-details .tspy-spy-btn')
const cacheId = button.dataset.cacheId;
cachedSpyRecord(cacheId, row);
button.addEventListener("click", (event) => {
set_loading(button, true, '...')
spy(button.dataset.id, button.dataset.special, button.dataset.amount, button.dataset.spy, (response) => {
response = JSON.parse(response.responseText);
if(response.success === false) {
updateSpyRecordError(response.text.replace("area", '').replace('This', 'Using your job special'), row);
return;
}
if(response.result.error) {
updateSpyRecordError(response.result.error, row);
return;
}
set_loading(button, false, 'Spy')
updateSpyRecordView(
updateSpyRecord(response.result.msg, response.result.user, response.amount),
row
);
});
});
});
document.querySelectorAll('[data-toggle]').forEach((toggle) => {
toggle.addEventListener("click", (event) => {
toggle_display(document.querySelector('[data-toggle-id="'+event.srcElement.dataset.toggle+'"]'))
});
})
}
const attach_accept_request_flow = () => {
document.querySelectorAll('.request').forEach((request) => {
request.querySelector('.buy-button')?.addEventListener("click", (event) => {
set_display(request.querySelector('.request-item-overview'), false)
set_display(request.querySelector('.request-item-accept'), true)
})
request.querySelector('.tspy-accept-btn')?.addEventListener("click", (event) => {
acceptRequest(request, event.target.dataset.requestId)
})
request.querySelector('.tspy-cancel-btn')?.addEventListener("click", (event) => {
set_display(request.querySelector('.request-item-overview'), true)
set_display(request.querySelector('.request-item-accept'), false)
})
});
}
const acceptRequest = (requestElement, requestId) => {
GM_xmlhttpRequest(
{
method: 'POST',
url: `${DOMAIN}/userscripts/marketplace/request/${requestId}/accept`,
responseType: 'json',
headers: { 'TORNSPY-KEY': getApiKey() },
onload: (response) => {
response = response.response ?? JSON.parse(response.responseText);
requestElement.insertAdjacentHTML('beforeEnd', response.result)
set_display(requestElement.querySelector('.request-item-overview'), false)
set_display(requestElement.querySelector('.request-item-accept'), false)
requestElement.querySelector('.tspy-okay-btn')?.addEventListener("click", (event) => {
set_display(requestElement.querySelector('.request-item-overview'), true)
set_display(requestElement.querySelector('.request-item-accept'), false)
requestElement.querySelector('.request-item-accept-error').outerHTML = '';
})
requestElement.querySelector('.tspy-success-okay-btn')?.addEventListener("click", (event) => {
requestElement.remove()
})
},
}
);
}
const validateRequest = (data) => {
GM_xmlhttpRequest(
{
method: 'POST',
url: `${DOMAIN}/userscripts/marketplace/request/validate`,
data: JSON.stringify(data),
responseType: 'json',
headers: { 'TORNSPY-KEY': getApiKey() },
onload: (response) => {
response = response.response ?? JSON.parse(response.responseText);
set_display(document.getElementById('requests-create-overview-panel'), false)
if(response.error){
document.getElementById('tspy-marketplace').insertAdjacentHTML('afterBegin', response.error)
error_panel()
}
if(response.success){
document.getElementById('tspy-marketplace').insertAdjacentHTML('afterBegin', response.success)
success_panel()
reset_create_request_flow()
}
},
}
);
}
const success_panel = () => {
document.getElementById('success-button').addEventListener("click", (event) => {
loaders['requests-available-overview-panel']()
})
}
const error_panel = () => {
document.getElementById('error-back-button').addEventListener("click", (event) => {
document.getElementById('make-request-error-panel').remove()
set_display(document.getElementById('requests-create-overview-panel'), true)
})
}
const global_search_input = () => {
let searchTimer;
const doneTypingDelay = 500;
const input = document.getElementById('tspy-global-search-input')
input.addEventListener('keyup', () => {
clearTimeout(searchTimer)
searchTimer = setTimeout(handle_global_search, doneTypingDelay)
});
input.addEventListener('keydown', () => {
clearTimeout(searchTimer)
});
}
const handle_global_search = () => {
const current_panel = get_current_panel();
const reportType = current_panel.replace('-overview-panel','').replace('-','_')
const query = document.getElementById('tspy-global-search-input').value
reports_overview({
page: document.getElementById(current_panel).querySelector('.page-number.active')?.querySelector('.page-nb').innerHTML,
query: query
}, reportType)
if(query.trim() === ''){
const url = new URL(window.location.href);
url.searchParams.delete('tspy-query');
window.history.replaceState({}, "", url);
}else{
const url = new URL(window.location.href);
url.searchParams.set('tspy-query', query);
window.history.replaceState({}, "", url);
}
}
const search_input = () => {
let searchTimer;
const doneTypingDelay = 500;
const input = document.getElementById('tspy-search-input')
input.addEventListener('keyup', () => {
clearTimeout(searchTimer)
searchTimer = setTimeout(handle_search, doneTypingDelay)
});
input.addEventListener('keydown', () => {
clearTimeout(searchTimer)
});
document.addEventListener('click', function(event) {
if(event.target.id !== 'tspy-search-input' && event.target.classList.contains('search-item') === false){
set_display(document.getElementById('tspy-search-result'), false)
}
});
['click', 'focus', 'input'].forEach(event => {
document.getElementById('tspy-search-input').addEventListener(event, function(event) {
set_display(document.getElementById('tspy-search-result'), true)
})
})
};
const handle_search = () => {
search(document.getElementById('tspy-search-input').value, (response) => {
response = JSON.parse(response);
document.getElementById('tspy-search-result').innerHTML = "";;
response.list.forEach((result) => {
document.getElementById('tspy-search-result').insertAdjacentHTML('afterBegin', "<div class=\"search-item center\" data-id=\""+result.id+"\">"+result.name+" ["+result.id+"]</div>");
})
document.querySelectorAll('.search-item').forEach((item) => {
item.addEventListener("click", () => {
if(document.getElementById('tspy-targets').querySelector(`[data-id="${item.dataset.id}"`)){
document.getElementById('tspy-targets').querySelector(`[data-id="${item.dataset.id}"`).outerHTML = '';
}else{
const selection = item.cloneNode(true);
selection.classList.add('character-item')
selection.setAttribute('title', 'Click to remove target');
document.getElementById('tspy-targets').insertAdjacentHTML('beforeEnd', selection.outerHTML);
initializeTornTooltip(document.getElementById('tspy-targets').querySelectorAll('.character-item'))
document.getElementById('tspy-targets').querySelector('[data-id="'+item.dataset.id+'"]').addEventListener("click", (event) => {
event.target.remove()
document.querySelector('.ui-tooltip-content').outerHTML = '';
})
item.remove()
}
})
})
});
}
const show_marketplace_panel = (panelId) => {
document.getElementById('tspy-marketplace').querySelectorAll('.panel').forEach(element => {
if(element.id !== panelId){
set_display(element, false)
}
});
if(document.getElementById(panelId).dataset.search){
set_display(document.getElementById('tspy-global-search'), true)
}else{
set_display(document.getElementById('tspy-global-search'), false)
}
set_display(document.getElementById(panelId), true)
const url = new URL(window.location.href);
url.searchParams.set('tspy', true);
url.searchParams.set('tspy-panel', panelId);
window.history.replaceState({}, "", url);
}
/**
* Events page support
*/
if(hasApiKey() && window.location.pathname === '/page.php' && (new URLSearchParams(window.location.search)).get('sid') === 'events') {
waitForElement('#events-root').then(()=> {
waitForElement('[class^="appHeaderWrapper"]').then(()=> {
event_button()
if((new URLSearchParams(window.location.search)).get('tspy') === 'true'){
eventRequest()
}
});
});
}
const event_button = () => {
const logButton = document.querySelector('[class^="linksContainer"]')
const tornSpyEventsButton = logButton.cloneNode(true)
tornSpyEventsButton.id = "tspy-events-button";
[...tornSpyEventsButton.attributes].forEach(attr => {
if (['id', 'class'].includes(attr.name) === false) {
tornSpyEventsButton.removeAttribute(attr.name);
}
});
const aElement = tornSpyEventsButton.querySelector('a');
[...aElement.attributes].forEach(attr => {
if (['id', 'class'].includes(attr.name) === false) {
aElement.removeAttribute(attr.name);
}
});
tornSpyEventsButton.querySelector('[class^="iconWrapper"]').innerHTML = detective_svg()
tornSpyEventsButton.querySelector('[class^="linkTitle"]').innerHTML = 'Torn spy'
logButton.insertAdjacentHTML('beforeBegin', tornSpyEventsButton.outerHTML);
document.getElementById('tspy-events-button').addEventListener("click", (event) => {
if(has_loaded('events')){
toggle_display(document.querySelector('[class^="eventsListWrapper"]'))
toggle_display(document.querySelector('[class^="events-list-wrapper"]'))
}else{
eventRequest()
}
})
};
const eventRequest = () => {
set_loading(document.getElementById('tspy-events-button'), true, 'Loading...')
GM_xmlhttpRequest(
{
method: 'POST',
url: `${DOMAIN}/userscripts/events`,
responseType: 'json',
headers: { 'TORNSPY-KEY': getApiKey() },
onload: (response) => {
response = response.response ?? JSON.parse(response.responseText);
if(response.style){
GM_addStyle(response.style);
}
set_loaded('events')
set_loading(document.getElementById('tspy-events-button'), true, 'Torn Spy')
events(response)
},
}
);
};
const events = (response) => {
set_display(document.querySelector('[class^="eventsListWrapper"]'), false)
document.querySelector('[class^="eventsListWrapper"]').insertAdjacentHTML('beforeBegin', response.events);
}
})();