Dibs

Claim dibs in territory wars to avoid wasting E

// ==UserScript==
// @name         Dibs
// @namespace    https://tornicorn.rocks/
// @version      0.8.1
// @description  Claim dibs in territory wars to avoid wasting E
// @author       sullengenie [1946152], Kessica [440210]
// @run-at       document-start
// @match        https://www.torn.com/factions.php?step=your
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_xmlhttpRequest
// @grant        unsafeWindow
// @connect      tornicorn.rocks
// ==/UserScript==

(function() {
    'use strict';

    const joinTimestamps = {};
    const dibsCalls = {};
    const currentDibs = {};

    function uuidv4() {
        return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c => (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16));
    }

    const getRequesterId = () => {
        let storedId = GM_getValue('requesterId');
        if (storedId === undefined) {
            storedId = uuidv4();
            GM_setValue('requesterId', storedId);
        }
        return storedId;
    };

    const requesterId = getRequesterId();

    const createHtml = (html) => document.createRange().createContextualFragment(html);
    const insertBefore = (nodes, target) => {
        target.parentNode.insertBefore(nodes, target);
        return target.previousSibling;
    };
    const hide = (element) => { element.style.display = 'none'; };
    const show = (element) => { element.style.display = 'inline'; };

    const getPlayerIdFromRow = (rowNode) => {
        let nameButton = rowNode.querySelector('a.name');
        return parseInt(nameButton.getAttribute('href').split('=', 2)[1]);
    };

    const clearCurrentDibs = () => {
        delete currentDibs.id;
        if (currentDibs.timeoutId) {
            clearInterval(currentDibs.timeoutId);
            delete currentDibs.timeoutId;
        }
    };

    const showAttackButton = (attackButton, dibsButton) => {
        show(attackButton);
        hide(dibsButton);
    };

    const hideAttackButton = (attackButton, dibsButton) => {
        hide(attackButton);
        show(dibsButton);
    };

    const dibsSuccess = (dibsCalls, id) => {
        if (currentDibs.id) {
            hide(dibsCalls[currentDibs.id].attackButton);
            show(dibsCalls[currentDibs.id].dibsButton);
            clearCurrentDibs();
        }
        currentDibs.id = id;
        show(dibsCalls[id].attackButton);
        hide(dibsCalls[id].dibsButton);
        let timeoutId = setTimeout(() => {
            hide(dibsCalls[id].attackButton);
            show(dibsCalls[id].dibsButton);
            clearCurrentDibs();
        }, 30000);
        currentDibs.timeoutId = timeoutId;
    };

    const createDibsFragment = () => {
        let dibsFragment = createHtml(`<a class="c-pointer">Dibs!</a>`);
        return dibsFragment;
    };

    const createCountdownFragment = (timer) => {
        return createHtml(`<span style="color: #666; display: none">${timer}</span>`);
    };

    const addCountdownFragment = (countdownFragment, dibsButton) => {
        return insertBefore(countdownFragment, dibsButton);
    };

    const replaceWithDibsButton = (attackButton, id) => {
        let dibsFragment = createDibsFragment();
        let dibsButton = insertBefore(dibsFragment, attackButton);
        dibsButton.addEventListener('click', () => {
            let joinTimestamp = joinTimestamps[id];
            console.debug('Clicked on enemy with id ' + id + ' and timestamp ' + joinTimestamp);

            let start = Date.now();
            GM_xmlhttpRequest({
                method: 'POST',
                url: `https://tornicorn.rocks/dibs?target-id=${id}&timestamp=${joinTimestamp}&user-id=${requesterId}`,
                timeout: 5000,
                onload: (response) => {
                    if (response.status === 200) {
                        dibsSuccess(dibsCalls, id);
                        console.debug('Lock acquired', Date.now() - start);
                    } else if (response.status === 400) {
                        setDibsTimer(dibsCalls, id);
                    } else {
                        console.error('Unexpected response', response);
                    }
                }
            });
        });
        hide(attackButton);
        return dibsButton;
    };

    const clearDibsTimer = (dibsCalls, id) => {
        clearInterval(dibsCalls[id].intervalId);
        if (dibsCalls[id].countdownNode) {
            hide(dibsCalls[id].countdownNode);
        }
        if (dibsCalls[id].dibsButton) {
            show(dibsCalls[id].dibsButton);
        }
        delete dibsCalls[id].intervalId;
        delete dibsCalls[id].timer;
    };

    const rejectionMessage = 'Nope!';

    const updateDibsTimer = (dibsCalls, id) => {
        let secsRemaining = dibsCalls[id].timer;
        if (secsRemaining <= 1) {
            clearDibsTimer(dibsCalls, id);
        } else if (secsRemaining >= 1) {
            dibsCalls[id].timer -= 1;
            if (dibsCalls[id].countdownNode) {
                dibsCalls[id].countdownNode.innerText = (secsRemaining > 25 ? rejectionMessage : secsRemaining - 1);
            }
        } else {
            console.error('Unexpected dibs state');
            console.error(dibsCalls);
        }
    };

    const startCountdown = (dibsCalls, id) => {
        const dibs = dibsCalls[id];
        dibs.countdownNode.innerText = rejectionMessage;
        show(dibs.countdownNode);
        hide(dibs.dibsButton);
        dibs.intervalId = setInterval(() => updateDibsTimer(dibsCalls, id), 1000);
    };

    const setDibsTimer = (dibsCalls, id) => {
        dibsCalls[id].timer = 30;
        startCountdown(dibsCalls, id);
    };

    const resumeDibsTimer = (dibsCalls, id) => {
        startCountdown(dibsCalls, id);
    };

    const handleAddedEnemy = (rowNode) => {
        let attackButton = rowNode.querySelector('.attack a');
        let id = getPlayerIdFromRow(rowNode);
        let dibsButton = replaceWithDibsButton(attackButton, id);
        let countdownNode = addCountdownFragment(createCountdownFragment(30), dibsButton);
        // Clear old timer updater if it exists
        if (dibsCalls[id] && dibsCalls[id].intervalId) {
            clearInterval(dibsCalls[id].intervalId);
        }
        let {timer,joinTimestamp} = dibsCalls[id] || {};
        dibsCalls[id] = {attackButton, dibsButton, countdownNode, joinTimestamp: joinTimestamps[id]};
        if (timer && joinTimestamp === joinTimestamps[id]) {
            // If someone else still has dibs
            dibsCalls[id].timer = timer;
            resumeDibsTimer(dibsCalls, id);
        } else if (currentDibs[id] === id) {
            // If we have dibs
            hide(dibsButton);
            show(attackButton);
        }
    };

    const handleRemovedEnemy = (rowNode) => {
        // NOTE: We do not remove timer or joinTimestamp so they persist for
        // when this tab is reopened
        let attackButton = rowNode.querySelector('.attack a');
        let id = getPlayerIdFromRow(rowNode);
        delete dibsCalls[id].attackButton;
        delete dibsCalls[id].dibsButton;
        delete dibsCalls[id].countdownNode;
        if (dibsCalls[id].intervalId) {
            clearInterval(dibsCalls[id].intervalId);
        }
        if (currentDibs.id === id) {
            clearCurrentDibs();
        }
    };

    const waitForPageLoad = () => {
        return new Promise(function(resolve, reject) {
            console.debug('Waiting for page load');
            let target = document.getElementById('war-react-root').firstChild;
            console.debug(target);
            console.debug(target.childNodes.length);
            if (document.querySelector('.f-war-list')) {
                resolve(true);
            }
            let observer = new MutationObserver(function(mutations) {
                for (let mutation of mutations) {
                    console.debug(mutation);
                    if (mutation.addedNodes.length > 0) {
                        resolve(true);
                        return;
                    }
                }
            });
            observer.observe(target, {childList: true});
        });
    };

    const watchMembersList = (memberList) => {
        let observer = new MutationObserver(function(mutations) {
            for (let mutation of mutations) {
                console.debug('Member mutation');
                console.debug(mutation);
                for (let node of mutation.addedNodes) {
                    if (node.classList.contains('enemy')) {
                        console.debug('New enemy spotted!');
                        console.debug(node);
                        handleAddedEnemy(node);
                        // let attackButton = node.querySelector('.attack a');
                        // let nameButton = node.querySelector('a.name');
                        // let id = parseInt(nameButton.getAttribute('href').split('=', 2)[1]);
                        // let dibsButton = replaceWithDibsButton(attackButton, id, joinTimestamps[id]);
                        // console.debug('Enemy id: ' + id);
                    }
                }
                for (let node of mutation.removedNodes) {
                    if (node.classList.contains('enemy')) {
                        console.debug('Enemy off the wall!');
                        console.debug(node);
                        handleRemovedEnemy(node);
                        // let nameButton = node.querySelector('a.name');
                        // let id = parseInt(nameButton.getAttribute('href').split('=', 2)[1]);
                        // console.debug('Enemy id: ' + id);
                    }
                }
            }
        });
        observer.observe(memberList, {childList: true});
    };

    const waitForDescriptionLoad = () => {
        return new Promise(function(resolve, reject) {
            let target = document.querySelector('.desc-wrap');
            console.debug(target);
            console.debug('War list');
            console.debug(document.querySelector('.war-list'));
            console.debug('Descriptions');
            console.debug(document.querySelector('.desc-wrap'));
            console.debug('Faction war');
            console.debug(document.querySelector('.faction-war'));
            const factionWar = document.querySelector('.faction-war');
            if (factionWar) {
                console.debug('Faction war found!');
                console.debug(factionWar.querySelector('.members-list'));
                watchMembersList(factionWar.querySelector('.members-list'));
                for (let enemyNode of factionWar.querySelectorAll('.members-list .enemy:not(.row-animation-new)')) {
                    console.debug('Enemy on the wall!', enemyNode);
                    handleAddedEnemy(enemyNode);
                    // let attackButton = enemyNode.querySelector('.attack a');
                    // let nameButton = enemyNode.querySelector('a.name');
                    // let id = parseInt(nameButton.getAttribute('href').split('=', 2)[1]);
                    // let dibsButton = replaceWithDibsButton(attackButton, id, joinTimestamps[id]);
                    // console.debug('Group enemy id: ' + id);
                }
            }
            let observer = new MutationObserver(function(mutations) {
                for (let mutation of mutations) {
                    console.debug('Description load mutation');
                    console.debug(mutation);
                    for (let node of mutation.addedNodes) {
                        console.debug(node);
                        if (node.classList.contains('faction-war')) {
                            console.debug('Faction war!');
                            console.debug(node.querySelector('.members-list'));
                            watchMembersList(node.querySelector('.members-list'));
                            for (let enemyNode of node.querySelectorAll('.members-list .enemy:not(.row-animation-new)')) {
                                console.debug('Enemy on the wall!', enemyNode);
                                handleAddedEnemy(enemyNode);
                                // let attackButton = enemyNode.querySelector('.attack a');
                                // let nameButton = enemyNode.querySelector('a.name');
                                // let id = parseInt(nameButton.getAttribute('href').split('=', 2)[1]);
                                // let dibsButton = replaceWithDibsButton(attackButton, id, joinTimestamps[id]);
                                // console.debug('Group enemy id: ' + id);
                            }
                        }
                    }
                    for (let node of mutation.removedNodes) {
                        console.debug(node);
                        if (node.classList.contains('faction-war')) {
                            console.debug('Faction war!');
                            console.debug(node.querySelector('.members-list'));
                            for (let enemyNode of node.querySelectorAll('.members-list .enemy:not(.row-animation-new)')) {
                                console.debug('Enemy on the wall!', enemyNode);
                                handleRemovedEnemy(enemyNode);
                            }
                        }
                    }
                }
            });
            observer.observe(target, {childList: true});
        });
    };

    const watchForDescriptionChanges = () => {
        console.debug('Watching for description changes');
        let warList = document.querySelector('.f-war-list');
        console.debug(warList);
        let observer = new MutationObserver(function(mutations) {
            for (let mutation of mutations) {
                console.debug('Description change');
                console.debug(mutation);
                for (let node of mutation.addedNodes) {
                    if (node.classList.contains('descriptions')) {
                        waitForDescriptionLoad();
                    }
                }
            }
        });
        observer.observe(warList, {childList: true});
    };

    const noop = () => {};

    const customFetch = (fetch, {
        onrequest = noop,
        onresponse = noop,
        onresult = noop,
        onbody = [],
    }) => async (input, init) => {
        onrequest(input, init);
        const response = await fetch(input, init);
        onresponse(response);

        for (const handler of onbody) {
            if (handler.match(response)) {
                Promise.resolve(handler.execute(response.clone()))
                    .then((result) => onresult(result));
            }
        }

        return response;
    };

    const interceptFetch = (options) => (unsafeWindow.fetch = customFetch(fetch, options));

    // usage

    interceptFetch({
        //onrequest: (input, init) => console.debug('FETCH CALL', input, init),
        //onresponse: (response) => console.debug('FETCH RESPONSE', response),

        onbody: [{
            match: (response) => response.url.startsWith('https://www.torn.com/faction_wars.php'),
            execute: (response) => response.json().then((json) => {
                if (json.warDesc && json.warDesc.members) {
                    for (let member of json.warDesc.members) {
                        if (member !== 0) {
                            joinTimestamps[member.userID] = member.joinTimestamp;
                        }
                    }
                }
            })
        }],
    });

    var pollId;
    pollId = setInterval(() => {
        console.debug('polling');
        if (document && document.getElementById('war-react-root') && document.getElementById('war-react-root').firstChild !== null) {
            console.debug('done polling');
            console.debug(document.getElementById('war-react-root').firstChild);
            clearInterval(pollId);
            waitForPageLoad().then(() => {
                watchForDescriptionChanges();
                console.debug('Moving along');
                console.debug(document.querySelector('.desc-wrap'));
                if (document.querySelector('.desc-wrap') !== null) {
                    waitForDescriptionLoad().then(() => console.debug('Description Loaded!'));
                }
            });
        }
    }, 100);
})();