Torn Log Pickpocketing

Logs detailed info about all pickpocketing attempts.

// ==UserScript==
// @name         Torn Log Pickpocketing
// @namespace    https://github.com/SOLiNARY
// @version      0.4
// @description  Logs detailed info about all pickpocketing attempts.
// @author       Ramin Quluzade, Silmaril [2665762]
// @license      MIT License
// @match        https://www.torn.com/loader.php?sid=crimes*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=torn.com
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @grant        GM_listValues
// @grant        GM_setClipboard
// @grant        GM_addStyle
// ==/UserScript==

(function() {
    'use strict';

    const viewPortWidthPx = window.innerWidth;
    const isMobileView = viewPortWidthPx <= 784;
    const isTampermonkeyEnabled = typeof unsafeWindow !== 'undefined';
    const levelsImagePxGap = 35;
    const levelsArray = [[1], generateNumberRange(2, 24), generateNumberRange(25, 49), generateNumberRange(50, 74), generateNumberRange(75, 99)];
    const activities = ['Cycling', 'Distracted', 'Talking?', 'Listening to music', 'Loitering', 'On Phone', 'Running/Jogging', 'Soliciting', 'Stumbling', 'Walking', 'Begging'];
    let attached = false;
    let userId = 'N/A';

    if (!isTampermonkeyEnabled) {
        console.error('[TornLogPickpocketing] Please, install Tampermonkey first!');
        alert('[TornLogPickpocketing] Please, install Tampermonkey first!');
        return;
    }

    const styles = `
    div.crimeSlider___uha7d > div.previousCrime___gxp9r, div.currentCrime___KNKYQ, div.nextCrime___uKnJr {
        transition-duration: 0ms !important;
    }`;
    GM_addStyle(styles);

    try {
        if (isMobileView) {
            const inputString = document.querySelector('img.mini-avatar-image').src;
            const regex = /(\d+)\.jpg$/;
            const match = inputString.match(regex);
            if (match) {
                userId = match[1];
            }
        } else {
            const userLink = document.querySelector('#sidebarroot div.user-information___VBSOk p.menu-info-row___YG31c > a').href;
            userId = userLink.split("=")[1];
        }
    } catch (e) {
        userId = 'N/A';
    }

    addExportButton(document.querySelector('div.crimes-app div.topSection___U7sVi div.titleContainer___QrlWP'));

    const targetNode = document.querySelector("div.crimes-app");
    const observerConfig = { childList: true, subtree: true };
    const observer = new MutationObserver(async (mutationsList, observer) => {
        const crimesDiv = document.querySelectorAll("div.crimes-app");
        const currentCrimeDiv = document.querySelectorAll("div.currentCrime___KNKYQ");
        for (const mutation of mutationsList) {
            let mutationTarget = mutation.target;
            if (!attached && mutation.type === 'childList' && mutationTarget.className == 'crime-root pickpocketing-root') {
                if (document.querySelector('div.currentCrime___KNKYQ div.title___MqBua') == null) {
                    continue;
                }
                attached = true;
                crimesDiv.forEach((div) => {
                    div.addEventListener("click", function (event) {
                        if (event.target.matches("div.topSection___U7sVi div.linksContainer___LiOTN")) {
                            handleSwitchPage();
                        }
                    });
                });
                currentCrimeDiv.forEach((div) => {
                    div.addEventListener("click", function (event) {
                        if (event.target.matches("div.topSection___HchKK div.crimeBanner___LiKtj div.crimeSliderArrowButtons___N_y4N button.arrowButton___gYTVW")) {
                            handleSwitchPage();
                        }
                    });
                });
                const pickpocketingRoot = document.querySelector('div.crime-root.pickpocketing-root');
                $(mutationTarget).on("click", "button.commitButton___uX7k_:not(.silmaril-log-pickpocketing)", async function(event){
                    const now = document.querySelector('span.server-date-time').textContent.split(' ');
                    const date = now[3];
                    const time = now[1];
                    const skillLevelBefore = getCurrentSkillLevelValue();
                    const button = findNearestButtonParent(event.target);
                    const row = button.parentNode.parentNode;
                    const markName = row.querySelector('div.titleAndProps___DdeVu div').innerText;
                    const markBuildText = row.querySelector('div.titleAndProps___DdeVu button').innerText;
                    const markBuildArray = markBuildText.split(' ');
                    const markBuild = markBuildArray[0];
                    let markHeight, markWeight;
                    if (markBuildText.indexOf(' kg') >= 0) {
                        markHeight = `${markBuildArray[1]} ${markBuildArray[2]}`;
                        markWeight = `${markBuildArray[3]} ${markBuildArray[4]}`;
                    } else {
                        markHeight = markBuildArray[1];
                        markWeight = `${markBuildArray[2]} ${markBuildArray[3]}`;
                    }
                    let markActivity = 'N/A';
                    try {
                        markActivity = isMobileView
                            ? activities[Math.abs(parseInt(row.querySelector('div.activity___e7mdA div.icon___VfCk2').style.backgroundPositionY, 10)) / 34]
                        : getTextValueWithoutChildren(row.querySelector('div.activity___e7mdA'));
                    } catch (e) {
                        console.error('[TornLogPickpocketing] Error while parsing activity:', row, e);
                    }

                    while (row.nextElementSibling.querySelector('div.outcomeReward___E34U7') == null) {
                        await sleep(20);
                    }

                    const outcomeRewardDiv = row.nextElementSibling.querySelector('div.outcomeReward___E34U7');
                    const result = outcomeRewardDiv.querySelector('p.title___IrNe6').textContent;
                    const gained = [];
                    if (outcomeRewardDiv.querySelector('div.rewards___oRCyz span.reward___dCLvW') != null) {
                        const money = outcomeRewardDiv.querySelector('div.rewards___oRCyz span.reward___dCLvW').textContent;
                        gained.push(money);
                    }
                    outcomeRewardDiv
                        .querySelectorAll('div.rewards___oRCyz div.reward___P7K87 img')
                        .forEach(item => {
                        try {
                            gained.push(item.alt);
                        } catch (e) {
                            console.error('[TornLogPickpocketing] Error while parsing non-ammo rewards:', item, e);
                        }
                    });
                    outcomeRewardDiv
                        .querySelectorAll('div.rewards___oRCyz div.reward___oQH1h')
                        .forEach(item => {
                        try {
                            let ammoQuantity = item.querySelector('div.itemCell___aZaUE.countCell___XzhYQ span.count___hBmtm').textContent;
                            let ammoCodeX = (Math.abs(parseInt(item.querySelector('div.ammoImage___tebhe').style.backgroundPositionX))/70).toFixed(0);
                            let ammoCodeY = (Math.abs(parseInt(item.querySelector('div.ammoImage___tebhe').style.backgroundPositionY))/70).toFixed(0);
                            gained.push(`${ammoQuantity} ammo of type ${ammoCodeX}-${ammoCodeY}`);
                        } catch (e) {
                            console.error('[TornLogPickpocketing] Error while parsing ammo rewards:', item, e);
                        }
                    });
                    const skillLevelAfter = getCurrentSkillLevelValue();
                    const progressionBonus = document.querySelector('div.statsColumn___ATsSP > button.statistic___jgFf2:nth-child(3) > span.value___FdkAT').textContent;
                    const version = GM_info.script.version;

                    const pickPocketAttempt = new PickPocketAttempt(userId, date, time, skillLevelBefore, skillLevelAfter, markName, markBuild, markHeight, markWeight, markActivity, result, gained, progressionBonus, version);
                    const dateSplit = date.split('/');
                    const timeStampFormatted = `${dateSplit[2]}${dateSplit[1]}${dateSplit[0]}${time.replaceAll(':','')}`;
                    const newGuid = generateGuid();
                    GM_setValue(`silmaril-torn-log-pickpocketing-attempt-${timeStampFormatted}-${newGuid}`, pickPocketAttempt);
                    updateLogsCount();
                });
                observer.disconnect();
            }
        }
    });
    setUpObserver();

    function handleSwitchPage() {
        attached = false;
        setUpObserver();
    }

    function addExportButton(root) {
        const outerExportDiv = document.createElement('div');
        outerExportDiv.className = 'title___MqBua';
        const exportButton = document.createElement('button');
        exportButton.className = 'torn-btn commitButton___uX7k_ silmaril-log-pickpocketing silmaril-log-pickpocketing-export';
        exportButton.ariaLabel = 'Copy PickPocket logs';
        const exportButtonLabel = document.createElement('span');
        exportButtonLabel.className = 'title___KAQ9m';
        exportButtonLabel.textContent = isMobileView ? 'Copy logs' : 'Copy PickPocket logs';
        const exportButtonDivider = document.createElement('span');
        exportButtonDivider.className = 'divider___RGx7a';
        const exportButtonLogCount = document.createElement('span');
        exportButtonLogCount.className = 'nerveCost___OoGfz';
        exportButtonLogCount.textContent = getAllLogsCount();
        exportButton.append(exportButtonLabel, exportButtonDivider, exportButtonLogCount);
        exportButton.addEventListener('click', exportLogs);

        const outerClearDiv = document.createElement('div');
        outerClearDiv.className = 'title___MqBua';
        const clearButton = document.createElement('button');
        clearButton.className = 'torn-btn commitButton___uX7k_ silmaril-log-pickpocketing silmaril-log-pickpocketing-clear';
        clearButton.ariaLabel = 'Clear PickPocket logs';
        const clearButtonLabel = document.createElement('span');
        clearButtonLabel.textContent = isMobileView ? 'Clear logs' : 'Clear PickPocket logs';
        clearButton.appendChild(clearButtonLabel);
        clearButton.addEventListener('click', clearLogs);

        outerExportDiv.append(exportButton);
        outerClearDiv.append(clearButton);

        root.append(outerExportDiv, outerClearDiv);

        const backButton = document.querySelector('div.topSection___U7sVi > div.linksContainer___LiOTN');
        backButton.addEventListener('click', function(){
            attached = false;
            setUpObserver();
        });
    }

    function setUpObserver() {
        observer.observe(targetNode, observerConfig);
    }

    function updateLogsCount(reset = false) {
        const countSpan = document.querySelector('button.silmaril-log-pickpocketing-export > span.nerveCost___OoGfz');
        const count = reset ? 0 : parseInt(countSpan.textContent) + 1;
        countSpan.textContent = count;
    }

    function exportLogs() {
        let logs = getAllLogs();
        let logsRaw = JSON.stringify(logs);
        GM_setClipboard(logsRaw, 'json');
        const exportText = document.querySelector('button.silmaril-log-pickpocketing-export > span.title___KAQ9m');
        exportText.textContent = isMobileView ? 'Copied!' : 'Copied to clipboard!';
        setTimeout(function(){exportText.textContent = isMobileView ? 'Copy logs' : 'Copy PickPocket logs';}, 2500);
    }

    function clearLogs() {
        deleteAllLogs();
        updateLogsCount(true);
        const clearText = document.querySelector('button.silmaril-log-pickpocketing-clear > span');
        clearText.textContent = isMobileView ? 'Cleared!' : 'Cleared all logs!';
        setTimeout(function(){clearText.textContent = isMobileView ? 'Clear logs' : 'Clear PickPocket logs';}, 2500);
    }

    function getAllLogs() {
        const prefix = 'silmaril-torn-log-pickpocketing-attempt-';
        const filteredRecords = {};
        const allKeys = GM_listValues();
        allKeys.forEach((key) => {
            if (key.startsWith(prefix)) {
                filteredRecords[key] = GM_getValue(key);
            }
        });
        return filteredRecords;
    }

    function getAllLogsCount() {
        const allLogsCount = Object.keys(GM_listValues()).length;
        return allLogsCount;
    }

    function deleteAllLogs() {
        const allKeys = GM_listValues();
        allKeys.forEach((key) => {
            GM_deleteValue(key);
        });
    }

    function getCurrentSkillLevelValue() {
        let currentSkillLevel = -1;
        if (document.querySelector('div.maxLevel___jTooz') == null) {
            const skillLevelDiv = document.querySelector('div.level___tAlsk');
            const levelFirstIndex = Math.abs(parseInt(skillLevelDiv.style.backgroundPositionX, 10)) / levelsImagePxGap;
            const levelSecondIndex = Math.abs(parseInt(skillLevelDiv.style.backgroundPositionY, 10)) / levelsImagePxGap;
            const skillLevelBefore = levelsArray[levelFirstIndex][levelSecondIndex];
            const skillProgressBar = document.querySelector('div.progressFill___ksrq5');
            const skillPercentageBefore = parseInt(skillProgressBar.style.width, 10) + (parseInt(skillProgressBar.style.left, 10) ?? -100);
            currentSkillLevel = skillLevelBefore + skillPercentageBefore / 100;
        } else {
            currentSkillLevel = 100;
        }
        return parseFloat(currentSkillLevel.toFixed(2));
    }

    function findNearestButtonParent(element) {
        let currentElement = element;
        while (currentElement && currentElement.tagName !== 'BUTTON') {
            currentElement = currentElement.parentNode;
        }
        return currentElement;
    }

    function getTextValueWithoutChildren(element) {
        let text = '';
        for (let node of element.childNodes) {
            if (node.nodeType === Node.TEXT_NODE) {
                text += node.textContent;
            }
        }
        return text.trim();
    }

    function generateGuid() {
        const crypto = window.crypto || window.msCrypto;

        if (!crypto) {
            return Math.floor(Math.random() * (999999 - 100000) + 100000).toString();
        }

        const data = new Uint16Array(8);
        crypto.getRandomValues(data);

        data[3] = (data[3] & 0x0fff) | 0x4000; // Version 4
        data[4] = (data[4] & 0x3fff) | 0x8000; // Variant 10

        const guid = Array.from(data)
        .map((byte) => byte.toString(16).padStart(2, '0'))
        .join('');

        return (
            guid.substr(0, 8) + '-' +
            guid.substr(8, 4) + '-' +
            guid.substr(12, 4) + '-' +
            guid.substr(16, 4) + '-' +
            guid.substr(20, 12)
        );
    }

    function generateNumberRange(start, end) {
        return Array.from({ length: end - start + 1 }, (_, index) => start + index);
    }

    function sleep(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }

    class PickPocketAttempt {
        constructor(playerId, date, time, skillBefore, skillAfter, markName, markBuild, markHeight, markWeight, markActivity, result, gained, progressionBonus, version) {
            this.playerId = playerId;
            this.date = date;
            this.time = time;
            this.skillBefore = skillBefore;
            this.skillAfter = skillAfter;
            this.markName = markName;
            this.markBuild = markBuild;
            this.markHeight = markHeight;
            this.markWeight = markWeight;
            this.markActivity = markActivity;
            this.result = result;
            this.gained = gained;
            this.progressionBonus = progressionBonus;
            this.version = version;
        }
    }
})();