Shows current faction members without their required OC item on the CRIMES page. Includes links to IM, and includes custom message with easy copy paste for sending!
// ==UserScript==
// @name Torn OC ItemEye
// @namespace http://tampermonkey.net/
// @version 1.1
// @description Shows current faction members without their required OC item on the CRIMES page. Includes links to IM, and includes custom message with easy copy paste for sending!
// @author HeyItzWerty [3626448]
// @match https://www.torn.com/factions.php*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_addStyle
// @grant GM_setClipboard
// @connect api.torn.com
// ==/UserScript==
(function() {
'use strict';
// --- Native Torn UI Styling ---
GM_addStyle(`
#oc-item-tool {
background-color: var(--default-bg-panel-color, #f2f2f2);
color: var(--default-color, #333);
border-radius: 5px;
margin-bottom: 10px;
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
border: 1px solid var(--default-panel-border-color, #cccccc);
overflow: hidden;
}
.oc-header {
background: var(--default-panel-header-background, linear-gradient(to bottom, #eeeeee 0%, #cccccc 100%));
color: var(--default-panel-header-color, #333);
padding: 6px 10px;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid var(--default-panel-border-color, #ccc);
font-weight: 700;
text-shadow: var(--default-text-shadow, none);
}
.oc-header-title { display: flex; align-items: center; }
.oc-header-title img {
height: 24px;
width: auto;
max-width: 150px;
object-fit: contain;
}
.oc-body { padding: 10px; background: var(--default-bg-panel-color, #fff); }
.oc-input {
padding: 4px 8px;
border-radius: 3px;
border: 1px solid var(--default-panel-border-color, #ccc);
background: var(--default-bg-panel-active-color, #fff);
color: var(--default-color, #333);
font-size: 11px;
outline: none;
}
.oc-btn {
cursor: pointer;
padding: 4px 10px;
border: 1px solid var(--default-panel-border-color, #ccc);
border-radius: 3px;
font-weight: bold;
font-size: 11px;
background: var(--default-bg-panel-active-color, #e0e0e0);
color: var(--default-color, #333);
transition: all 0.2s ease;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 4px;
}
.oc-btn:hover { filter: brightness(0.9); }
.btn-primary { border-color: #1976D2; color: #fff; background: #2196F3; }
.btn-secondary { border-color: #388E3C; color: #fff; background: #4CAF50; }
.icon-btn { cursor: pointer; background: transparent; border: none; color: var(--default-color, #666); padding: 2px; display: flex; align-items: center; opacity: 0.7; transition: opacity 0.2s; }
.icon-btn:hover { opacity: 1; color: var(--default-blue-color, #005eb8); }
.oc-table { width: 100%; border-collapse: collapse; font-size: 12px; }
.oc-table th {
background: var(--default-table-header-bg, #f2f2f2);
padding: 8px 10px;
text-align: left;
font-weight: bold;
border-bottom: 1px solid var(--default-panel-border-color, #ccc);
color: #85b200;
text-shadow: 1px 1px 0px rgba(0,0,0,0.05);
}
.dark-mode .oc-table th { color: #85b200; }
.oc-table td {
padding: 8px 10px;
border-bottom: 1px solid var(--default-panel-border-color, #eee);
vertical-align: middle;
}
.oc-table tr:nth-child(even) td { background: var(--default-bg-panel-active-color, #f9f9f9); }
.dark-mode .oc-table tr:nth-child(even) td { background: var(--default-bg-panel-active-color, #2a2a2a); }
.highlight-text { font-weight: 600; color: var(--default-color, #333); }
.oc-link { color: var(--default-blue-color, #005eb8); text-decoration: none; font-weight: 600; }
.oc-link:hover { text-decoration: underline; }
`);
// SVG Icons
const ICONS = {
copy: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>`,
msg: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></svg>`,
refresh: `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"></polyline><polyline points="1 20 1 14 7 14"></polyline><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path></svg>`
};
const LOGO_URL = "https://i.ibb.co/Zpy216QB/dawdawd.png";
// --- API Handlers ---
async function getTornData(endpoint, version = "v1") {
const key = GM_getValue("oc_api_key", "");
if (!key) throw new Error("API Key required.");
let baseUrl = version === "v2" ? "https://api.torn.com/v2" : "https://api.torn.com/v1";
// ADDED CACHE BUSTER (_t) to prevent mobile browsers from recycling old data
let url = `${baseUrl}/${endpoint}${endpoint.includes('?') ? '&' : '?'}key=${key}&_t=${Date.now()}`;
let res = await fetch(url);
let data = await res.json();
if (data.error) throw new Error(data.error.error || "API Error");
return data;
}
async function runCheck() {
const tbody = document.querySelector("#oc-tool-tbody");
const status = document.querySelector("#oc-status");
const apiKey = GM_getValue("oc_api_key", "");
if (!apiKey) {
status.innerHTML = "<em>Awaiting API Key...</em>";
tbody.innerHTML = `
<tr>
<td colspan='4' style='text-align:center; padding:30px;'>
<div style="margin-bottom:10px; font-weight:bold; color:var(--default-color, #333);">
Please enter a <span style="color:#1976D2;">Limited Access</span> API Key to use Torn OC ItemEye.
</div>
<input type="password" id="oc-inline-api-input" class="oc-input" placeholder="Paste API Key here..." style="width:250px; margin-right:8px;">
<button id="oc-inline-api-save" class="oc-btn btn-primary">Save Key</button>
</td>
</tr>
`;
document.getElementById('oc-inline-api-save').addEventListener('click', () => {
const newKey = document.getElementById('oc-inline-api-input').value.trim();
if(newKey) {
GM_setValue("oc_api_key", newKey);
runCheck();
}
});
return;
}
status.innerHTML = "<em>Scanning planned OCs...</em>";
tbody.innerHTML = "<tr><td colspan='4' style='text-align:center; padding:20px;'>Pulling live faction data...</td></tr>";
try {
const itemsData = await getTornData("torn/?selections=items");
const items = itemsData.items;
const factionData = await getTornData("faction/?selections=basic");
const members = factionData.members || {};
const crimesData = await getTornData("faction/crimes?cat=planning", "v2");
// Check to ensure they didn't input a Public key (which strips out crime slots)
if (crimesData.crimes === undefined) {
throw new Error("Cannot view Faction Crimes. Are you sure you are using a Limited Access key?");
}
const crimes = crimesData.crimes || [];
let missingList = [];
for (let crime of crimes) {
if (!crime.slots) continue; // Safety check
for (let slot of crime.slots) {
if (slot.user && slot.item_requirement && slot.item_requirement.is_available === false) {
missingList.push({
crimeName: crime.name,
userId: slot.user.id,
userName: members[slot.user.id] ? members[slot.user.id].name : `User`,
itemId: slot.item_requirement.id,
itemName: items[slot.item_requirement.id]?.name || "Unknown Item"
});
}
}
}
tbody.innerHTML = "";
if (missingList.length === 0) {
tbody.innerHTML = "<tr><td colspan='4' style='text-align:center; padding: 20px; font-weight:bold; color: var(--default-green-color, #4CAF50);'>All assigned members are fully equipped!</td></tr>";
status.innerText = "Scan complete. No missing items found.";
return;
}
let msgTemplate = GM_getValue("oc_custom_msg", "Hey {user}, noticed you're missing a {item} for the upcoming {crime} OC. Let me know if you need me to send one over!");
for (let item of missingList) {
let tr = document.createElement("tr");
let prefillMsg = msgTemplate
.replace(/{user}/g, item.userName)
.replace(/{item}/g, item.itemName)
.replace(/{crime}/g, item.crimeName);
tr.innerHTML = `
<td class="highlight-text">${item.crimeName}</td>
<td>
<div style="display:flex; align-items:center; gap:8px;">
<a class="oc-link" href="https://www.torn.com/profiles.php?XID=${item.userId}" target="_blank">${item.userName} [${item.userId}]</a>
<button class="icon-btn copy-btn" title="Copy Name" data-copy="${item.userName} [${item.userId}]">${ICONS.copy}</button>
<button class="icon-btn copy-msg-btn" title="Copy message" data-msg="${prefillMsg}">${ICONS.msg}</button>
</div>
</td>
<td class="highlight-text">${item.itemName}</td>
<td>
<div style="display:flex; gap:6px;">
<button class="oc-btn btn-primary btn-buy" data-itemid="${item.itemId}">Market</button>
<button class="oc-btn btn-secondary btn-items" title="Go to Items Page">Send Item</button>
</div>
</td>
`;
tbody.appendChild(tr);
}
status.innerHTML = `<em>Scan complete. Found ${missingList.length} missing item(s).</em>`;
attachListeners();
} catch (e) {
status.innerHTML = `<span style="color:red; font-weight:bold;">Error: ${e.message}</span>`;
tbody.innerHTML = `<tr><td colspan='4' style='text-align:center; padding:20px; color:red;'>${e.message}</td></tr>`;
}
}
function attachListeners() {
document.querySelectorAll('.copy-btn').forEach(btn => {
btn.addEventListener('click', function() {
GM_setClipboard(this.getAttribute('data-copy'));
this.innerHTML = `<span style="color:green; font-weight:bold;">✓</span>`;
setTimeout(() => this.innerHTML = ICONS.copy, 1000);
});
});
document.querySelectorAll('.copy-msg-btn').forEach(btn => {
btn.addEventListener('click', function() {
GM_setClipboard(this.getAttribute('data-msg'));
this.innerHTML = `<span style="color:green; font-weight:bold;">✓</span>`;
setTimeout(() => this.innerHTML = ICONS.msg, 1000);
});
});
document.querySelectorAll('.btn-buy').forEach(btn => {
btn.addEventListener('click', function() {
window.open(`https://www.torn.com/page.php?sid=ItemMarket#/market/view=search&itemID=${this.dataset.itemid}`, '_blank');
});
});
document.querySelectorAll('.btn-items').forEach(btn => {
btn.addEventListener('click', () => window.open('https://www.torn.com/item.php', '_blank'));
});
}
function injectUI() {
if (document.getElementById('oc-item-tool')) return;
const target = document.querySelector('.factions-crimes-wrap') || document.querySelector('#factions');
if (!target) return;
const container = document.createElement('div');
container.id = 'oc-item-tool';
container.innerHTML = `
<div class="oc-header">
<div class="oc-header-title">
<img src="${LOGO_URL}" alt="ItemEye Logo" onerror="this.outerHTML=''">
<span style="color: #85b200; font-weight: bold; margin-left: 8px; font-size: 13px; letter-spacing: 0.5px;">Torn OC ItemEye</span>
</div>
<button id="oc-refresh-btn" class="oc-btn">${ICONS.refresh} Refresh</button>
</div>
<div class="oc-body">
<div style="display:flex; justify-content: space-between; align-items:flex-end; margin-bottom: 8px;">
<span id="oc-status" style="font-size:11px; color: var(--default-color, #666);">Initializing...</span>
<div style="display:flex; gap: 12px;">
<button id="oc-edit-msg" style="background:none; border:none; color:var(--default-blue-color, #005eb8); font-size:10px; cursor:pointer; text-decoration:underline;">Edit Copy Message</button>
<button id="oc-api-reset" style="background:none; border:none; color:var(--default-blue-color, #005eb8); font-size:10px; cursor:pointer; text-decoration:underline;">Reset API Key</button>
</div>
</div>
<table class="oc-table">
<thead><tr><th>Crime</th><th>Member</th><th>Item Needed</th><th>Actions</th></tr></thead>
<tbody id="oc-tool-tbody"></tbody>
</table>
</div>
`;
target.parentNode.insertBefore(container, target);
document.getElementById('oc-refresh-btn').addEventListener('click', runCheck);
document.getElementById('oc-api-reset').addEventListener('click', () => {
GM_setValue("oc_api_key", "");
runCheck(); // Will instantly show the inline input box again
});
document.getElementById('oc-edit-msg').addEventListener('click', () => {
let currentMsg = GM_getValue("oc_custom_msg", "Hey {user}, noticed you're missing a {item} for the upcoming {crime} OC. Let me know if you need me to send one over!");
let promptText = "Customize your copy message button for easy pasting when sending an item!\\n" +
"{user} -> Member's name\\n" +
"{item} -> Missing item name\\n" +
"{crime} -> Current OC";
let newMsg = prompt(promptText, currentMsg);
if (newMsg !== null && newMsg.trim() !== "") {
GM_setValue("oc_custom_msg", newMsg.trim());
runCheck();
}
});
runCheck();
}
setInterval(() => {
if (location.href.includes('tab=crimes') && !document.getElementById('oc-item-tool')) injectUI();
}, 1500);
})();