· 3 months ago · Jun 26, 2025, 09:25 PM
1// ==UserScript==
2// @name Torn War Timer Sidebar
3// @namespace https://www.torn.com/
4// @version 1.2
5// @description Display live ranked war timer in the sidebar using Torn API v2, with smart polling
6// @author Cypher-[2641265]
7// @match https://www.torn.com/*\
8// @icon https://www.google.com/s2/favicons?sz=64&domain=torn.com
9// @grant GM_xmlhttpRequest
10// @connect api.torn.com
11// ==/UserScript==
12
13(function () {
14 'use strict';
15
16 function getApiKey() {
17 return localStorage.getItem('rw_api_key') || '';
18 }
19
20 const timerContainerID = 'warTimerSidebar';
21 const LS_KEY = 'rw_last_war_info'; // localStorage key
22
23 function getStoredWarInfo() {
24 try {
25 return JSON.parse(localStorage.getItem(LS_KEY)) || {};
26 } catch {
27 return {};
28 }
29 }
30
31 function setStoredWarInfo(obj) {
32 localStorage.setItem(LS_KEY, JSON.stringify(obj));
33 }
34
35 function fetchRankedWars() {
36 GM_xmlhttpRequest({
37 method: "GET",
38 url: `https://api.torn.com/v2/faction/rankedwars?sort=DESC&key=${getApiKey()}`,
39 onload: function (response) {
40 let data;
41 try {
42 data = JSON.parse(response.responseText);
43 } catch (e) {
44 return;
45 }
46 if (!data?.rankedwars || !Array.isArray(data.rankedwars)) return;
47
48 const now = Math.floor(Date.now() / 1000);
49
50 // Find the first war with "end": 0 and "start" in the future (pending war)
51 const pendingWar = data.rankedwars.find(war => war.end === 0 && war.start > now);
52 if (pendingWar) {
53 setStoredWarInfo({
54 warId: pendingWar.id,
55 start: pendingWar.start,
56 nextCheck: pendingWar.start + 5 // check again 5s after start
57 });
58 const left = Math.max(0, pendingWar.start - now);
59 updateSidebar(formatTimer(left));
60 scheduleNextCheck(pendingWar.start - now + 5); // run again just after war starts
61 return;
62 }
63
64 // If no pending war, show active war timer as zeros and wait 48 hours
65 const activeWar = data.rankedwars.find(war => war.end === 0 && war.start <= now);
66 if (activeWar) {
67 setStoredWarInfo({
68 warId: activeWar.id,
69 start: activeWar.start,
70 nextCheck: now + 48 * 3600 // check again in 48 hours
71 });
72 updateSidebar("0d 0h 0m");
73 scheduleNextCheck(48 * 3600);
74 return;
75 }
76
77 // If no war, set next check for 6 hours later
78 setStoredWarInfo({
79 warId: null,
80 start: null,
81 nextCheck: now + 6 * 3600
82 });
83 updateSidebar("0d 0h 0m");
84 scheduleNextCheck(6 * 3600); // 6 hours
85 }
86 });
87 }
88
89 function formatTimer(secs) {
90 if (typeof secs !== "number" || isNaN(secs) || secs < 0) return "0d 0h 0m";
91 const d = Math.floor(secs / 86400);
92 const h = Math.floor((secs % 86400) / 3600);
93 const m = Math.floor((secs % 3600) / 60);
94 // Show as "Xd Yh Zm"
95 let out = "";
96 if (d > 0) out += `${d}d `;
97 if (h > 0 || d > 0) out += `${h}h `;
98 out += `${m}m`;
99 return out.trim();
100 }
101
102 function showApiKeyPopup(callback) {
103 if (document.getElementById('rw_api_input')) return; // Prevent multiple popups
104 let popup = document.createElement('div');
105 popup.style.position = 'fixed';
106 popup.style.top = '50%';
107 popup.style.left = '50%';
108 popup.style.transform = 'translate(-50%, -50%)';
109 popup.style.background = '#222';
110 popup.style.color = '#fff';
111 popup.style.padding = '20px';
112 popup.style.border = '2px solid #888';
113 popup.style.zIndex = 9999;
114 popup.style.borderRadius = '8px';
115 popup.innerHTML = `
116 <div style="margin-bottom:10px;">Enter your Torn public API key:</div>
117 <input type="text" id="rw_api_input" style="width:300px;" maxlength="16" value="${getApiKey() || ''}">
118 <div style="margin-top:10px;">
119 <button id="rw_api_save" style="background:#444;color:#fff;border:1px solid #aaa;padding:6px 18px;margin-right:10px;border-radius:4px;cursor:pointer;">Save</button>
120 <button id="rw_api_cancel" style="background:#444;color:#fff;border:1px solid #aaa;padding:6px 18px;border-radius:4px;cursor:pointer;">Cancel</button>
121 </div>
122 `;
123 document.body.appendChild(popup);
124 document.getElementById('rw_api_input').focus();
125
126 document.getElementById('rw_api_save').onclick = function() {
127 let key = document.getElementById('rw_api_input').value.trim();
128 if (key.length === 16) {
129 localStorage.setItem('rw_api_key', key);
130 document.body.removeChild(popup);
131 if (callback) callback(key);
132 } else {
133 alert('Please enter a valid 16-character Torn public API key.');
134 }
135 };
136 document.getElementById('rw_api_cancel').onclick = function() {
137 document.body.removeChild(popup);
138 };
139 }
140
141 function updateSidebar(timerText) {
142 let sidebar = document.querySelector('.tt-sidebar-information');
143 if (!sidebar) return;
144
145 let section = document.getElementById(timerContainerID);
146 if (!section) {
147 section = document.createElement('section');
148 section.id = timerContainerID;
149 section.style.order = 2;
150 section.innerHTML = `<a class="title" href="https://www.torn.com/factions.php?step=your&type=1#/war/rank" target="_blank">RW:</a> <span id="war-timer-value" class="countdown"></span>`;
151 sidebar.insertBefore(section, sidebar.children[1]);
152 }
153 const timerSpan = section.querySelector('#war-timer-value');
154 if (timerSpan) {
155 if (!getApiKey()) {
156 timerSpan.textContent = "Enter public key";
157 timerSpan.style.cursor = "pointer";
158 timerSpan.onclick = function(e) {
159 showApiKeyPopup(() => {
160 timerSpan.onclick = null;
161 runCheck();
162 });
163 };
164 } else {
165 timerSpan.textContent = timerText;
166 timerSpan.style.cursor = "";
167 timerSpan.onclick = null;
168 }
169 }
170 }
171
172 let nextTimeout = null;
173 function scheduleNextCheck(seconds) {
174 if (nextTimeout) clearTimeout(nextTimeout);
175 nextTimeout = setTimeout(runCheck, Math.max(1000, seconds * 1000));
176 }
177
178 function runCheck() {
179 if (!getApiKey()) {
180 updateSidebar("Enter public key");
181 return;
182 }
183 const now = Math.floor(Date.now() / 1000);
184 const info = getStoredWarInfo();
185 if (info.nextCheck && now < info.nextCheck) {
186 scheduleNextCheck(info.nextCheck - now);
187 return;
188 }
189 fetchRankedWars();
190 }
191
192 // Show stored timer immediately if available
193 (function showStoredTimer() {
194 const info = getStoredWarInfo();
195 let timerText = "0d 0h 0m";
196 if (info.start && info.nextCheck && info.start > Math.floor(Date.now() / 1000)) {
197 // Pending war
198 const left = Math.max(0, info.start - Math.floor(Date.now() / 1000));
199 timerText = formatTimer(left);
200 } else if (info.start && info.nextCheck && info.nextCheck > Math.floor(Date.now() / 1000)) {
201 // Active war (approximate)
202 const left = Math.max(0, info.nextCheck - Math.floor(Date.now() / 1000));
203 timerText = formatTimer(left);
204 }
205 updateSidebar(timerText);
206 })();
207
208 setTimeout(runCheck, 3000);
209})();