Easy Property Rent Extension

Easily extend the rental property based on the previous rental agreement from your activity log

// ==UserScript==
// @name         Easy Property Rent Extension
// @namespace    easy.property.rent.extension
// @version      v2.2.0
// @description  Easily extend the rental property based on the previous rental agreement from your activity log
// @author       IceBlueFire [776]
// @license      MIT
// @match        https://www.torn.com/properties.php*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=torn.com
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @require      https://code.jquery.com/jquery-1.8.2.min.js
// ==/UserScript==

/******************** CONFIG SETTINGS ********************/
const apikey = "#######"; // Full access API key required to pull historical activity log data.
const days_remaining = 3; // Number of days remaining or less to be included in reminders.
const default_days = 7; // Default number of days to extend the lease if data can't be found.
const default_cost = 5600000; // Default cost of lease extension if data can't be found.
const properties = [13]; // Array of property types allowed for renting. Ex: [12, 13] for Castles and Private Islands. Reference property IDs below as necessary.
const hex_color = "#8ABEEF"; // Hexcode to apply to the box.
const debug = 0; // Leave alone unless you want console logs.
/****************** END CONFIG SETTINGS *******************/

/* Property IDs
 * 1 = Trailer;
 * 2 = Apartment;
 * 3 = Semi-Detached House;
 * 4 = Detached House;
 * 5 = Beach House;
 * 6 = Chalet;
 * 7 = Villa;
 * 8 = Penthouse;
 * 9 = Mansion;
 * 10 = Ranch;
 * 11 = Palace;
 * 12 = Castle;
 * 13 = Private Island;
*/



/******************** DO NOT TOUCH ANYTHING BELOW THIS ********************/
$(document).ready(function() {
    log("Ready");
    let debounceTimeout;
    let rentalHistoryCache = null;
    let propertyListCache = null;
    let current_page = null;
    let hex_darker = "#53728f";
    let player_id = $('#sidebarroot a[href*="profiles.php?XID="]').attr('href')?.match(/XID=(\d+)/)?.[1];

    function drawNavigation() {
        let valid_properties = [];
        let total_properties = 0;

        const navigation_div = `
    <div id="icey-container" style="display:none;">
    <div id="icey-property-rentals" class="m-top10">
    <div class="icey-head">
        <span class="icey-title">Easy Rental Extensions</span>
    </div>
    <div class="icey-content">
    <a id="previous-button" class="torn-btn">Previous</a>
    <div id="info-div" class="center-info"><p><strong>Rental Property Information</strong></p>
    <p><span class="rentals-count">0</span> / <span class="total-count">0</span> properties requiring attention.</p>
    <p class="viewing-rental"><span class="current-index">0</span> / <span class="rentals-count"></p>
    </div>
    <a id="next-button" class="torn-btn">Next</a>

    </div>
    <hr class="page-head-delimiter m-top10 m-bottom10">
    </div>
    </div>
    `;
        if ($('#icey-property-rentals').length === 0) {
            log('Insert navigation div');
            $('#properties-page-wrap .content-title.m-bottom10').after(navigation_div);
            const propertyList = propertyListCache ? Promise.resolve(propertyListCache) : getPropertyList();
            propertyList.then(function(result) {
                if (!propertyListCache) {
                    propertyListCache = result;
                }
                $.each(result.properties, function(key, value) {
                    if (value.owner_id == player_id && properties.includes(value.property_type)) {
                        total_properties++;
                        if(value.rented && value.rented.days_left <= days_remaining) {
                            valid_properties.push(key);
                        }
                    }
                });


                // Get the current property ID from the URL
                const current_property_id = getParam('ID');
                let current_index = valid_properties.indexOf(current_property_id);

                if (current_index > 0) {
                    const previous_property_id = valid_properties[current_index - 1];
                    $('#previous-button').attr('href', `#/p=options&ID=${previous_property_id}&tab=offerExtension`);
                } else {
                    $('#previous-button').attr('href', '#').addClass('disabled'); // Disable if no previous
                }

                // Handle the "Next" button
                if (current_index < valid_properties.length - 1) {
                    const next_property_id = valid_properties[current_index === -1 ? 0 : current_index + 1];
                    $('#next-button').attr('href', `#/p=options&ID=${next_property_id}&tab=offerExtension`);
                } else {
                    $('#next-button').attr('href', '#').addClass('disabled'); // Disable if no next
                }
                $('#icey-container .total-count').html(total_properties);
                $('#icey-container .rentals-count').html(valid_properties.length);
                if(current_index === -1 || current_page == 'properties') {
                    $('#icey-container .viewing-rental').hide();
                } else {
                    $('#icey-container .viewing-rental').show();
                    $('#icey-container .current-index').html(current_index + 1);
                }
                setDynamicGradient();
                $('.icey-head').css('background', `linear-gradient(180deg, ${hex_color}, ${hex_darker})`);
                $('#icey-container').show();
            });
        }
    }



    function checkTabAndRunScript() {
        // Do the stuff!
        drawNavigation();
        if (getParam('tab') === 'offerExtension') {
            let current_renter = null;
            let link = $('.offerExtension-form').find('a.h.t-blue');
            if (link.length > 0) {
                current_renter = link.attr('href')?.match(/XID=(\d+)/)?.[1] || null;
                log("Renter: " + current_renter);
                getPreviousValues(current_renter);
            } else {
                log("No renter found.");
            }
        } else if(getParam('tab') === 'lease') {
            log("Add to rental market");
            setDefaultLeaseFields();
        } else {
            log("Wrong properties tab.");
        }
    }

    function setDefaultLeaseFields()
    {
        const checkVisibility = setInterval(function() {
            const marketDiv = $('#market');
            if (marketDiv.is(':visible')) {
                // Access the input field and set its value
                $('#market').find('input.lease.input-money[data-name="money"]').val(default_cost);
                $('#market').find('input.lease.input-money[data-name="days"]').val(default_days);

                clearInterval(checkVisibility); // Stop the interval once the value is set
            }
        }, 100); // Check every 100 milliseconds

        $('#market input[type="submit"]').prop('disabled', false);
    }

    function getPreviousValues(current_renter) {
        // Look for previous rental agreements and auto-fill input boxes
        var duration = default_days || 0;
        var cost = default_cost || 0;
        const property_id = getParam('ID');
        const activity = rentalHistoryCache ? Promise.resolve(rentalHistoryCache) : getRentalHistory();
        activity.then(function(result) {
            // Cache the result if not already cached
            if (!rentalHistoryCache) {
                rentalHistoryCache = result;
            }

            $.each(result.log, function(key, value) {
                if(value.data.property_id == property_id && value.data.renter == current_renter) {
                    duration = value.data.days;
                    cost = value.data.rent;
                    return false;
                }
            });
            
            // Target the input fields
            let costInput = $('.offerExtension.input-money[data-name="offercost"]');
            let daysInput = $('.offerExtension.input-money[data-name="days"]');

            // Set the values
            costInput.val(cost);
            daysInput.val(duration);
            // costInput.tornInputMoney({buttonElement: null, skipBlurCheck: true});

            // Trigger events to mimic manual input
            // costInput.trigger('input').trigger('change').trigger('keyup'); // Attempt to simulate input event for Torn
            // daysInput.trigger('input').trigger('change').trigger('keyup'); // Attempt to simulate input event for Torn

            $('.offerExtension-form input[type="submit"]').prop('disabled', false);
        });
    }

    async function getRentalHistory() {
        // Get the activity log for both rental extensions and new rental agreements
        return new Promise(resolve => {
            const request_url = `https://api.torn.com/user/?selections=log&key=`+apikey+`&log=5943,5937&comment=EasyRentalExtensions`;
            GM_xmlhttpRequest ({
                method:     "GET",
                url:        request_url,
                headers:    {
                    "Content-Type": "application/json"
                },
                onload: response => {
                    try {
                        const data = JSON.parse(response.responseText);
                        if(!data) {
                            log('No response from Torn API');
                        } else {
                            log('Log data fetched.');
                            return resolve(data);
                        }
                    }
                    catch (e) {
                        console.error(e);
                    }

                },
                onerror: (e) => {
                    console.error(e);
                }
            })
        });
    }

    async function getPropertyList() {
        // Get the activity log for both rental extensions and new rental agreements
        return new Promise(resolve => {
            const request_url = `https://api.torn.com/user/?selections=properties&key=`+apikey+`&comment=EasyRentalExtensions`;
            GM_xmlhttpRequest ({
                method:     "GET",
                url:        request_url,
                headers:    {
                    "Content-Type": "application/json"
                },
                onload: response => {
                    try {
                        const data = JSON.parse(response.responseText);
                        if(!data) {
                            log('No response from Torn API');
                        } else {
                            log('Property data fetched.');
                            return resolve(data);
                        }
                    }
                    catch (e) {
                        console.error(e);
                    }

                },
                onerror: (e) => {
                    console.error(e);
                }
            })
        });
    }

    function getParam(name) {

       // Extract the part of the URL after the #
        const fragment = window.location.href.split('#')[1];
        if (!fragment) return null; // No fragment present

        // Treat the fragment like a query string
        const results = new RegExp(name + '=([^&#]*)').exec(fragment);
        return results ? decodeURIComponent(results[1]) : null; // Decode and return the value, or null if not found
    }

    function log(message) {
        if(debug){
            console.log("[RentExtension] "+message);
        }
    }

    function setDynamicGradient() {
        // Convert base hex to RGB
        const baseRgb = hexToRgb(hex_color);

        // Create a darker shade by reducing brightness
        const darkerRgb = darkenRgb(baseRgb, 0.7); // 0.7 is the factor for darkening

        // Convert the darker RGB back to hex
        hex_darker = rgbToHex(darkerRgb.r, darkerRgb.g, darkerRgb.b);
    }

    // Helper: Convert hex to RGB
    function hexToRgb(hex) {
        const bigint = parseInt(hex.replace('#', ''), 16);
        return {
            r: (bigint >> 16) & 255,
            g: (bigint >> 8) & 255,
            b: bigint & 255,
        };
    }

    // Helper: Darken an RGB color
    function darkenRgb(rgb, factor) {
        return {
            r: Math.max(0, Math.min(255, Math.floor(rgb.r * factor))),
            g: Math.max(0, Math.min(255, Math.floor(rgb.g * factor))),
            b: Math.max(0, Math.min(255, Math.floor(rgb.b * factor))),
        };
    }

    // Helper: Convert RGB to hex
    function rgbToHex(r, g, b) {
        return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase()}`;
    }

    // Create an observer for the properties page to watch for page changes
    // Select the target node
    const targetNode = document.getElementById('properties-page-wrap');

    if (targetNode) {
        // Create a MutationObserver to watch for changes
        const observer = new MutationObserver((mutationsList) => {
            clearTimeout(debounceTimeout); // Reset the debounce timeout on every change
            debounceTimeout = setTimeout(() => {
                log("Content changed in #properties-page-wrap");
                current_page = getParam('p');
                checkTabAndRunScript(); // Run your script when content settles
            }, 500); // Debounce for 500ms
        });

        // Start observing the target node for configured mutations
        observer.observe(targetNode, {
            childList: true, // Watch for added/removed child nodes
            subtree: true,  // Watch the entire subtree of the target node
        });

        //console.log("MutationObserver is set up with debouncing.");
    } else {
        console.error("Target node #properties-page-wrap not found.");
    }

    // Run the script initially in case the page is already on the correct tab
    checkTabAndRunScript();


});
GM_addStyle(`
#icey-container {
    width: 100%;
}
.icey-head {
    border-radius: 5px 5px 0 0;
    height: 30px;
    line-height: 30px;
    width: 100%;
    background: linear-gradient(180deg, #8ABEEF, #53728f);
    color: white;
}
.icey-title {
    width: 100%;
    font-size: 13px;
    letter-spacing: 1px;
    font-weight: 700;
    line-height: 16px;
    flex-grow: 2;
    padding: 5px;
    margin: 5px;
    text-transform: capitalize;
    text-shadow: rgba(0, 0, 0, 0.65) 1px 1px 2px;
}
body.dark-mode .icey-content {
    background: #333333;
}
body.dark-mode .icey-content td {
    color: #c0c0c0;
}
.icey-content {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 5px;
    background: #f2f2f2;
    border-radius: 0 0 5px 5px;
    box-shadow: 0 1px 3px #06060680;
}
.icey-content .torn-btn {
    flex: 0 0 auto;
}

.center-info {
    flex: 1 1 auto;
    text-align: center;
}

`);