您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Gather CPR data when visiting OC pages in Torn for iziom.com : TornPDA and Chrome
// ==UserScript== // @name Iziom.com CPR — Organised crime CPR Tracker // @namespace http://tampermonkey.net/ // @version 0.0.6 // @description Gather CPR data when visiting OC pages in Torn for iziom.com : TornPDA and Chrome // @author Gaskarth // @license MIT // @match https://www.torn.com/factions.php* // @grant GM_xmlhttpRequest // @run-at document-idle // @connect iziom.com // ==/UserScript== (function() { 'use strict'; console.log('Iziom CPR script ===================================='); // --- CONFIGURATION --- const TORN_OC_DATA_URL = 'page.php?sid=organizedCrimesData&step=crimeList'; const SERVER_ENDPOINT_URL = 'https://iziom.com/cpr-update.php'; // // --- SCRIPT INITIALIZATION --- const isTornPDA = typeof window.flutter_inappwebview !== 'undefined'; // Use the correct window object. `unsafeWindow` is needed in some TamperMonkey environments // to bypass the sandbox and access the page's native `fetch` function. const win = isTornPDA ? window : (typeof unsafeWindow !== 'undefined' ? unsafeWindow : window); // --- COMPATIBILITY LAYER --- // This section ensures the script's HTTP requests work in both // a standard browser (with TamperMonkey) and the TornPDA app. const customXmlHttpRequest = (details) => { if (isTornPDA) { // In TornPDA, we use a custom handler provided by the app's webview. window.flutter_inappwebview.callHandler('PDA_httpPost', details.url, details.headers, details.data) .then(response => { if (details.onload) { details.onload({ status: response.status, responseText: response.data }); } }) .catch(err => { if (details.onerror) { details.onerror(err); } }); } else { // In a standard browser, we use the grant-enabled GM_xmlhttpRequest. GM_xmlhttpRequest(details); } }; // --- CORE FUNCTIONS --- /** * Extracts the current user's ID. * @returns {object|null} An object with { userId } or null if not found. */ function getUserInfo() { // Primary method: Use the global torn object if available. This is the most reliable way. if (win.torn?.user?.player_id) { const userId = String(win.torn.user.player_id); console.log(`CPR Tracker: User ID found via global object: ${userId}`); return { userId }; } // Fallback method: Scrape the DOM if the global object is not available. console.log("CPR Tracker: Global object not found, falling back to DOM scraping for user ID."); try { const profileLink = document.querySelector('a[href*="profiles.php?XID="]'); if (!profileLink) { console.error("CPR Tracker: Could not find user profile link (DOM fallback)."); return null; } const href = profileLink.href; const userId = href.match(/XID=(\d+)/)[1]; if (userId) { console.log(`CPR Tracker: User ID found via DOM: ${userId}`); return { userId }; } } catch (error) { console.error("CPR Tracker: Error parsing user ID from DOM.", error); } return null; } /** * Processes the intercepted OC data to extract CPRs. * @param {Array} scenarios - The array of crime scenarios from the game's API response. * @returns {Array} An array of objects, each representing a single CPR. */ function processCPRs(scenarios) { const extractedCPRs = []; scenarios.forEach(scenario => { const scenarioName = String(scenario.scenario.name); scenario.playerSlots.forEach(slot => { // The key logic: If a slot has no player, `successChance` is the user's CPR. if (slot.player === null) { extractedCPRs.push({ scenario: scenarioName, role: String(slot.name), cpr: slot.successChance }); } }); }); return extractedCPRs; } /** * Sends the collected CPR data to your server, or logs it if no server is configured. * @param {object} payload - The final data object to be sent. */ function submitDataToServer(payload) { console.log("CPR Tracker: Preparing to submit data..."); /* * ================================================================== * SERVER API SPECIFICATION * ================================================================== * * This script will send an HTTP POST request to your SERVER_ENDPOINT_URL. * * HEADERS: * - Content-Type: application/json * * BODY (raw JSON): * The body will be a JSON object with the following structure: * * { * "userId": "123456", // String: The player's Torn User ID. * "cprData": [ // Array: A list of all CPRs found on the page. * { * "scenario": "Thorough Robbery", // String: The name of the OC scenario. * "role": "Driver", // String: The name of the role within the scenario. * "cpr": 85 // Integer: The Checkpoint Pass Rate for this role. * } * // ... more CPR objects * ] * } * * The server will parse this JSON structure and use the `userId` as the primary key for storing/updating the data. * * ================================================================== */ // If a server URL is defined, send the data. if (SERVER_ENDPOINT_URL) { console.log("CPR Tracker: Sending data to endpoint:", SERVER_ENDPOINT_URL); customXmlHttpRequest({ method: 'POST', url: SERVER_ENDPOINT_URL, headers: { 'Content-Type': 'application/json' }, data: JSON.stringify(payload), onload: (response) => { if (response.status >= 200 && response.status < 300) { console.log('CPR Tracker: Data submitted successfully.', response.responseText); } else { console.error('CPR Tracker: Server responded with an error.', response.status, response.responseText); } }, onerror: (err) => { console.error('CPR Tracker: Failed to send data.', err); } }); } else { // If no server URL is defined, log the payload to the console for debugging. console.log("CPR Tracker: No server endpoint configured. Logging payload to console:"); console.log(JSON.stringify(payload, null, 4)); } } // --- FETCH INTERCEPTION --- // Store the original fetch function and replace it with a wrapper const originalFetch = win.fetch; win.fetch = async function(resource, config) { const url = typeof resource === 'string' ? resource : resource.url; // 1. Check if this is the specific POST request we want to intercept. if (config?.method?.toUpperCase() !== 'POST' || !url.includes(TORN_OC_DATA_URL)) { // If not, just call the original fetch and do nothing else. return originalFetch.apply(this, arguments); } // 2. Check if the request is for the "Recruiting" group. let isRecruitingGroup = false; if (config?.body) { // The body can be a FormData object or a string, so we check for both. const bodyAsString = config.body.toString(); isRecruitingGroup = bodyAsString.includes('group=Recruiting') || (config.body instanceof FormData && config.body.get('group') === 'Recruiting'); } if (!isRecruitingGroup) { // If it's not the recruiting group, we're not interested. return originalFetch.apply(this, arguments); } // --- This is the request we want! --- // First, let the original request complete so the game page loads normally. const response = await originalFetch.apply(this, arguments); try { // Clone the response so we can read its body without affecting the game. const responseText = await response.clone().text(); const json = JSON.parse(responseText); // Check if the response was successful and contains the data we need. if (json.success && json.data && Array.isArray(json.data)) { console.log("CPR Tracker: Intercepted OC data."); const userInfo = getUserInfo(); const cprData = processCPRs(json.data); if (userInfo && cprData.length > 0) { // Construct the final payload object. const payload = { userId: userInfo.userId, cprData: cprData }; submitDataToServer(payload); } else { if (!userInfo) console.warn("CPR Tracker: Could not find user info to send with data."); if (cprData.length === 0) console.log("CPR Tracker: No empty player slots found, nothing to report."); } } } catch (err) { console.error('CPR Tracker: Error processing intercepted response.', err); } // Finally, return the original response to the game's code. return response; }; })();