// ==UserScript==
// @name Torn — Recent Mug Warning
// @namespace https://torn.com/
// @version 1.92
// @description shows mug warnings. settings accessible from items.
// @match https://www.torn.com/*
// @grant none
// @run-at document-idle
// ==/UserScript==
(async function() {
'use strict';
// --------------------
// Own Profile Highlight
// --------------------
const OWN_API_KEY_STORAGE = 'torn_self_highlight_api_key';
const API_USER_URL = 'https://api.torn.com/user/?selections=basic&key=';
async function getOwnId() {
let apiKey = localStorage.getItem(OWN_API_KEY_STORAGE);
if (!apiKey) {
apiKey = prompt("Enter your Torn API key for self-profile highlight:");
if (!apiKey) return null;
localStorage.setItem(OWN_API_KEY_STORAGE, apiKey);
}
try {
const resp = await fetch(API_USER_URL + encodeURIComponent(apiKey), { cache: 'no-store' });
if (!resp.ok) return null;
const data = await resp.json();
return data && (data.player_id || data.user_id) ? String(data.player_id || data.user_id) : null;
} catch { return null; }
}
function getProfileIdFromUrl() {
try {
const u = new URL(window.location.href);
return u.searchParams.get('XID') || u.searchParams.get('ID') || null;
} catch { return null; }
}
const ownId = await getOwnId();
const profileId = getProfileIdFromUrl();
if (profileId && ownId && profileId === ownId) {
// Own profile code: badge and STOP
const badge = document.createElement('div');
badge.innerText = '✅ This is you';
Object.assign(badge.style, {
backgroundColor: 'green',
color: 'white',
fontWeight: '700',
padding: '4px 8px',
borderRadius: '6px',
position: 'absolute',
top: '10px',
right: '10px',
zIndex: '999999',
fontSize: '14px'
});
document.body.appendChild(badge);
return; // STOP here: do not run mug warning script
}
// --------------------
// Mug Warning Script (runs only if NOT on own profile)
// --------------------
const STORE_KEY='torn_api_mug_warn_v3';
const IGNORED_KEY='torn_api_mug_ignore_profiles';
const MUG_INFO_KEY='torn_api_mug_info';
const COLOR_BG_KEY='torn_api_mug_color_bg';
const COLOR_TXT_KEY='torn_api_mug_color_txt';
const HOURS_KEY='torn_api_mug_hours';
const DEFAULT_HOURS=24;
// Settings menu colors
const SETTINGS_BG_KEY='torn_settings_bg';
const SETTINGS_TXT_KEY='torn_settings_txt';
const BUTTON_TXT_KEY='torn_settings_btn_txt';
const API_MUG_URL='https://api.torn.com/user/?selections=attacks&key=';
// ---- Helper functions ----
function getStoredKey(){ try{return localStorage.getItem(STORE_KEY);}catch{return null;} }
function getIgnoredProfiles(){ try{ return JSON.parse(localStorage.getItem(IGNORED_KEY))||{}; }catch{return {};} }
function ignoreProfile(id){ const s=getIgnoredProfiles(); s[id]=true; localStorage.setItem(IGNORED_KEY,JSON.stringify(s)); }
function unignoreProfile(id){ const s=getIgnoredProfiles(); delete s[id]; localStorage.setItem(IGNORED_KEY,JSON.stringify(s)); }
function isIgnored(id){ return !!getIgnoredProfiles()[id]; }
function safeParseInt(n){ const v=parseInt(n); return Number.isFinite(v)?v:null; }
function getAttackTargetIdFromUrl(){ try{const u=new URL(window.location.href);return u.searchParams.get('user2ID')||null;}catch{return null;} }
function isProfilePage(){ return /profiles\.php|profile\.php|pda\.php/.test(location.pathname+location.search); }
function isAttackPage(){ return /loader\.php/.test(location.pathname) && getAttackTargetIdFromUrl(); }
function getColorSettings(){
return {
bg: localStorage.getItem(COLOR_BG_KEY) || '#ff4d4d',
txt: localStorage.getItem(COLOR_TXT_KEY) || '#ffffff'
};
}
function getHoursSetting(){ return parseInt(localStorage.getItem(HOURS_KEY)) || DEFAULT_HOURS; }
function getSettingsMenuColors(){
return {
bg: localStorage.getItem(SETTINGS_BG_KEY) || '#2b2b2b',
txt: localStorage.getItem(SETTINGS_TXT_KEY) || '#ffffff',
btnTxt: localStorage.getItem(BUTTON_TXT_KEY) || '#ffffff'
};
}
// ---- Modal Warning ----
function showModalWarning(profileId, whenIso){
if(!profileId || isIgnored(profileId)) return;
if(document.getElementById('api-mug-modal')) return;
const { bg, txt } = getColorSettings();
const HOURS=getHoursSetting();
const overlay=document.createElement('div');
overlay.id='api-mug-modal';
Object.assign(overlay.style,{position:'fixed',top:0,left:0,width:'100%',height:'100%',background:'rgba(0,0,0,0.7)',display:'flex',alignItems:'center',justifyContent:'center',zIndex:9999999,padding:'10px',boxSizing:'border-box'});
const modal=document.createElement('div');
Object.assign(modal.style,{background:bg,color:txt,padding:'16px',borderRadius:'10px',textAlign:'center',width:'90%',maxWidth:'400px',fontWeight:'700',boxShadow:'0 6px 20px rgba(0,0,0,0.6)',lineHeight:'1.4'});
const now=Date.now();
let timeText='unknown';
let timeColor='#000000';
if(whenIso){
const mugTime=Date.parse(whenIso);
if(!isNaN(mugTime)){
const diffMs=Math.max(0,now-mugTime);
const diffHrs=Math.floor(diffMs/3600000);
const diffMins=Math.floor((diffMs%3600000)/60000);
timeText=`${diffHrs}h ${diffMins}m ago`;
if(diffHrs>=12 && diffHrs<HOURS) timeColor='#00ff00';
}
}
modal.innerHTML=`
<div style="font-size:18px;margin-bottom:8px">⚠️ WARNING</div>
<div style="font-size:14px;font-weight:600;margin-bottom:10px">You mugged this player within the last ${HOURS} hours</div>
<div style="font-size:14px">Time since last mug: <span style="font-weight:700;color:${timeColor}">${timeText}</span></div>
`;
const buttons=document.createElement('div');
Object.assign(buttons.style,{display:'flex',justifyContent:'center',marginTop:'16px',gap:'10px'});
const b1=document.createElement('button'); b1.innerText='Set MugTarget';
const b2=document.createElement('button'); b2.innerText='Dismiss';
[b1,b2].forEach(b=>{
const btnColors=getSettingsMenuColors();
Object.assign(b.style,{padding:'8px 14px',border:'2px solid '+btnColors.btnTxt,borderRadius:'6px',background:'transparent',color:btnColors.btnTxt,fontWeight:'700',cursor:'pointer'});
b.onmouseover=()=>{b.style.background=btnColors.btnTxt;b.style.color=btnColors.bg;};
b.onmouseout=()=>{b.style.background='transparent';b.style.color=btnColors.btnTxt;};
});
b1.onclick=()=>{ignoreProfile(profileId);overlay.remove();};
b2.onclick=()=>overlay.remove();
buttons.append(b1,b2);
modal.appendChild(buttons);
overlay.appendChild(modal);
document.body.appendChild(overlay);
}
// ---- Mug detection ----
function attackIndicatesMug(a,targetId,selfId){
if(!a)return false;
const tid=String(targetId);
try{
const text=JSON.stringify(a).toLowerCase();
if(/mug(g?ed|ging)/.test(text)&&text.includes(tid))return true;
}catch{}
return false;
}
function extractTimestampFromAttack(a){
try{
if(!a)return null;
const fields=['time','timestamp','timestamp_ended'];
for(const f of fields){if(a[f]!==undefined){const t=safeParseInt(a[f]);if(t)return new Date(t*1000).toISOString();}}
}catch{}
return null;
}
async function checkProfileWithApi(targetIdOptional){
const key=getStoredKey(); if(!key)return;
const HOURS=getHoursSetting();
const MS_THRESHOLD=HOURS*3600*1000;
const profileId=targetIdOptional||getProfileIdFromUrl(); if(!profileId)return;
try{
const resp=await fetch(API_MUG_URL+encodeURIComponent(key),{cache:'no-store'});
if(!resp.ok)return;
const data=await resp.json();
if(!data||data.error)return;
const selfId=String(data.player_id||data.user_id||'');
const attacks=data.attacks?Object.values(data.attacks):[];
const cutoff=Date.now()-MS_THRESHOLD;
for(const a of attacks){
const iso=extractTimestampFromAttack(a);
const ts=iso?Date.parse(iso):null;
if(ts && ts>=cutoff && attackIndicatesMug(a,profileId,selfId)){
showModalWarning(profileId,iso);
return;
}
}
}catch{}
}
// ---- Mug Targets Modal (matches settings modal styling) ----
function showMugTargetsModal(onClose){
const { bg, txt, btnTxt } = getSettingsMenuColors();
const overlay=document.createElement('div');
Object.assign(overlay.style,{
position:'fixed',top:0,left:0,width:'100%',height:'100%',
background:'rgba(0,0,0,0.6)',display:'flex',alignItems:'center',
justifyContent:'center',zIndex:9999999
});
const box=document.createElement('div');
Object.assign(box.style,{
background:bg,
color:txt,
padding:'20px',
borderRadius:'12px',
width:'350px',
fontSize:'14px',
lineHeight:'1.5',
boxShadow:'0 6px 20px rgba(0,0,0,0.5)',
textAlign:'center'
});
box.innerHTML='<h3 style="margin-top:0;">🗂️ Mug Targets</h3>';
const list=document.createElement('div');
Object.assign(list.style,{maxHeight:'200px',overflowY:'auto',marginBottom:'10px',textAlign:'left'});
function refreshList(){
list.innerHTML='';
const data=getIgnoredProfiles();
const ids = Object.keys(data);
if(ids.length === 0){
const empty = document.createElement('div');
empty.innerText = 'No mug targets saved.';
list.appendChild(empty);
return;
}
ids.forEach(id=>{
const row = document.createElement('div');
row.style.margin = '6px 0';
// link
const link = document.createElement('a');
link.href = 'https://www.torn.com/profiles.php?XID='+id;
link.target = '_blank';
link.innerText = id;
link.style.color = txt;
link.style.fontWeight = '700';
// remove button
const removeBtn = document.createElement('button');
removeBtn.innerText = 'Remove';
Object.assign(removeBtn.style,{
marginLeft:'8px',padding:'4px 8px',cursor:'pointer',
color:btnTxt,border:'2px solid '+btnTxt,borderRadius:'6px',background:'transparent'
});
removeBtn.onmouseover = ()=>{ removeBtn.style.background = btnTxt; removeBtn.style.color = bg; };
removeBtn.onmouseout = ()=>{ removeBtn.style.background = 'transparent'; removeBtn.style.color = btnTxt; };
removeBtn.onclick = ()=>{ unignoreProfile(id); refreshList(); };
row.append(link, removeBtn);
list.appendChild(row);
});
}
refreshList();
box.appendChild(list);
const addInput=document.createElement('input');
addInput.placeholder='Add ID manually';
addInput.style.width='100%';
addInput.style.marginBottom='8px';
addInput.style.boxSizing='border-box';
box.appendChild(addInput);
const addBtn=document.createElement('button');
addBtn.innerText='Add';
const closeBtn=document.createElement('button');
closeBtn.innerText='Close';
closeBtn.style.marginLeft='6px';
[addBtn, closeBtn].forEach(b=>{
Object.assign(b.style,{
padding:'6px 12px',border:'2px solid '+btnTxt,borderRadius:'6px',
background:'transparent',color:btnTxt,cursor:'pointer',marginTop:'8px'
});
b.onmouseover=()=>{b.style.background=btnTxt;b.style.color=bg;};
b.onmouseout=()=>{b.style.background='transparent';b.style.color=btnTxt;};
});
addBtn.onclick=()=>{
const val=addInput.value.trim();
if(!val) return;
const s = getIgnoredProfiles();
s[val] = true;
localStorage.setItem(IGNORED_KEY, JSON.stringify(s));
addInput.value = '';
refreshList();
};
closeBtn.onclick=()=>{
overlay.remove();
if(typeof onClose === 'function') onClose();
};
box.append(addBtn, document.createElement('br'), closeBtn);
overlay.appendChild(box);
document.body.appendChild(overlay);
}
// ---- Settings ----
function styleButtons(btns, color='#ffffff'){
btns.forEach(b=>{
Object.assign(b.style,{
padding:'6px 12px',
border:'2px solid '+color,
borderRadius:'6px',
cursor:'pointer',
color:color,
background:'transparent'
});
b.onmouseover=()=>{b.style.background=color;b.style.color='#2b2b2b';};
b.onmouseout=()=>{b.style.background='transparent';b.style.color=color;};
});
}
function showSettingsModal(){
const { bg, txt, btnTxt } = getSettingsMenuColors();
const HOURS=getHoursSetting();
const currentKey=getStoredKey()||'';
const overlay=document.createElement('div');
Object.assign(overlay.style,{position:'fixed',top:0,left:0,width:'100%',height:'100%',background:'rgba(0,0,0,0.6)',display:'flex',alignItems:'center',justifyContent:'center',zIndex:9999999});
const box=document.createElement('div');
Object.assign(box.style,{background:bg,color:txt,padding:'20px',borderRadius:'12px',width:'350px',fontSize:'14px',lineHeight:'1.5',boxShadow:'0 6px 20px rgba(0,0,0,0.5)',textAlign:'center'});
box.innerHTML=`
<h3 style="margin-top:0;">⚙️ Mug Warning Settings</h3>
<label>API Key:</label><br>
<input type="text" id="apiKeyInput" value="${currentKey}" style="width:100%;margin-bottom:8px;"><br>
<label>Hours Threshold:</label><br>
<input type="number" id="hoursInput" value="${HOURS}" style="width:100%;margin-bottom:8px;"><br>
<label>Modal Background Color:</label><br>
<input type="color" id="bgColorInput" value="${localStorage.getItem(COLOR_BG_KEY)||'#ff4d4d'}" style="width:100%;margin-bottom:8px;"><br>
<label>Modal Text Color:</label><br>
<input type="color" id="txtColorInput" value="${localStorage.getItem(COLOR_TXT_KEY)||'#ffffff'}" style="width:100%;margin-bottom:8px;"><br>
<label>Settings Menu Background:</label><br>
<input type="color" id="settingsBgInput" value="${bg}" style="width:100%;margin-bottom:8px;"><br>
<label>Settings Menu Text:</label><br>
<input type="color" id="settingsTxtInput" value="${txt}" style="width:100%;margin-bottom:8px;"><br>
<label>Button Text Color:</label><br>
<input type="color" id="buttonTxtInput" value="${btnTxt}" style="width:100%;margin-bottom:8px;"><br>
<hr style="margin:10px 0;">
<button id="manageTargetsBtn" style="margin-bottom:8px;">Manage Mug Targets</button><br>
<button id="saveSettingsBtn" style="margin-right:10px;">Save</button>
<button id="closeSettingsBtn">Close</button>
`;
styleButtons(box.querySelectorAll('button'), btnTxt);
overlay.appendChild(box);
document.body.appendChild(overlay);
box.querySelector('#saveSettingsBtn').onclick=()=>{
const key=box.querySelector('#apiKeyInput').value.trim();
if(key) localStorage.setItem(STORE_KEY,key); else localStorage.removeItem(STORE_KEY);
localStorage.setItem(HOURS_KEY,box.querySelector('#hoursInput').value);
localStorage.setItem(COLOR_BG_KEY,box.querySelector('#bgColorInput').value);
localStorage.setItem(COLOR_TXT_KEY,box.querySelector('#txtColorInput').value);
localStorage.setItem(SETTINGS_BG_KEY, box.querySelector('#settingsBgInput').value);
localStorage.setItem(SETTINGS_TXT_KEY, box.querySelector('#settingsTxtInput').value);
localStorage.setItem(BUTTON_TXT_KEY, box.querySelector('#buttonTxtInput').value);
alert('Settings saved!');
overlay.remove();
};
box.querySelector('#closeSettingsBtn').onclick=()=>overlay.remove();
box.querySelector('#manageTargetsBtn').onclick=()=>{
// hide settings overlay, open targets modal; restore settings overlay when targets closed
overlay.style.display = 'none';
showMugTargetsModal(()=>{ overlay.style.display = 'flex'; });
};
}
function insertItemPageSettings(){
if(!/item\.php/i.test(window.location.href)) return;
if(document.getElementById('torn-item-settings')) return;
const btn=document.createElement('button');
btn.id='torn-item-settings';
btn.innerText='⚙️ Mug Settings';
Object.assign(btn.style,{position:'fixed',right:'14px',bottom:'70px',zIndex:9999999,background:'#222',color:'#fff',border:'2px solid #fff',borderRadius:'6px',padding:'6px 12px',cursor:'pointer',fontSize:'14px'});
btn.onmouseover=()=>{btn.style.background='#fff';btn.style.color='#000';};
btn.onmouseout=()=>{btn.style.background='#222';btn.style.color='#fff';};
btn.onclick=showSettingsModal;
document.body.appendChild(btn);
}
(function ensureSettingsButton(){
try{ insertItemPageSettings(); }catch{}
try{
const mo=new MutationObserver(()=>{ insertItemPageSettings(); });
mo.observe(document.body,{childList:true,subtree:true});
}catch{}
window.addEventListener('popstate',()=>insertItemPageSettings());
window.addEventListener('hashchange',()=>insertItemPageSettings());
let lastHref=location.href;
setInterval(()=>{ if(location.href!==lastHref){ lastHref=location.href; insertItemPageSettings(); lastHref=location.href; } },1000);
})();
// ---- Bootstrap Mug Detection ----
try{
if(isProfilePage()){
const observer=new MutationObserver((mutations,obs)=>{
if(getProfileIdFromUrl()){ checkProfileWithApi(); obs.disconnect(); }
});
observer.observe(document.body,{childList:true,subtree:true});
} else if(isAttackPage()){
const targetId=getAttackTargetIdFromUrl();
if(targetId) setTimeout(()=>checkProfileWithApi(targetId),300);
}
}catch{}
})();