Keep MillenniumBCP Session Active

A script that observes the pages via MutationObserver and quickly captures and activates the button to extend the session and not be signed off.

// ==UserScript==
// @name         Keep MillenniumBCP Session Active
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  A script that observes the pages via MutationObserver and quickly captures and activates the button to extend the session and not be signed off.
// @author       JoaoWorkspace
// @icon         https://ptpimg.me/hy2wdn.png
// @grant        none
// @match        https://corp.millenniumbcp.pt/*
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // Updated number revealing how many times the session has been extended.
    let revivalCount = 0;

      /*______________*/
    /* Common tools */
    /*______________*/

    // The number of times the mutation observer executes (meaning also, how many mutations detected), variable to be incremented by the code.
    let executionCount = 0;
    const debug = false;

    async function incrementExecutionNumber(mutations){
        executionCount ++;
        if(executionCount > 999){
            observer.disconnect();
            logWithTimestamp(`Execution Count: ${executionCount} has reached a huge number. Stopping program...`);
        }
        // This formats number 1 to 001 and 23 to 023.
        // It predicts the execution count (meaning number of mutations) in the same page doesn't surpass 999 executions.
        // A very realistic number, safe for edge cases, like videogames.
        const formattedNumber = executionCount.toString().padStart(3, '0');
        console.log('####################################################');
        console.log(`############### Execution Number ${formattedNumber} ###############`);
        console.log('####################################################');
        if (mutations) {
            // Count each type of mutation
            const mutationCounts = mutations.reduce(
                (counts, mutation) => {
                    counts[mutation.type] = (counts[mutation.type] || 0) + 1;
                    return counts;
                },
                { attributes: 0, characterData: 0, childList: 0 }
            );
            console.log('################## Mutation Stats ##################');
            console.log(`# Attribute Mutations: ${mutationCounts.attributes}`);
            console.log(`# Character Data Mutations: ${mutationCounts.characterData}`);
            console.log(`# Child List Mutations: ${mutationCounts.childList}`);
        }
        console.log('####################################################');
    }

    async function giveStatusUpdate(section){
        const formattedNumber = executionCount.toString().padStart(3, '0');
        const formattedTimestamp = new Date()
        .toISOString()
        .replace('T', ' ')
        .replace(/\.\d+/, match => match.substring(0, 2)) // Keep only the first decimal of milliseconds
        .replace('Z', ''); // Remove trailing 'Z'
        console.log('####################################################');
        console.log(`############### Execution ${formattedNumber} Result ###############`);
        console.log(`############### ${formattedTimestamp} ##############`);
        console.log('####################################################');
        console.log(`# Section ${section} complete...`);
        console.log('####################################################');
        console.log(`# Session Revivals: ${revivalCount}`);
        console.log('####################################################');
    }

    // Utility function for logging with a timestamp
    function logWithTimestamp(message, ...optionalParams) {
        const formattedTimestamp = new Date()
        .toISOString()
        .replace('T', ' ')
        .replace(/\.\d+/, match => match.substring(0, 2)) // Keep only the first decimal of milliseconds
        .replace('Z', ''); // Remove trailing 'Z'
        console.log(`${formattedTimestamp} | ${message}`, ...optionalParams);
    }

        // Debug function for adding varied listeners to the target element
    async function injectEventListeners(targetElement) {
        targetElement.addEventListener('focus', (e) => logWithTimestamp('Focused!', e));
        targetElement.addEventListener('click', (e) => logWithTimestamp('Clicked!', e));
        targetElement.addEventListener('blur', (e) => logWithTimestamp('Blurred!', e));
        targetElement.addEventListener('change', (e) => logWithTimestamp('Changed!', e));
        targetElement.addEventListener('input', (e) => logWithTimestamp('Received Input!', e));
        targetElement.addEventListener('keyup', (e) => logWithTimestamp('Key Up!', e));
        targetElement.addEventListener('keydown', (e) => logWithTimestamp('Key Down!', e));
        targetElement.addEventListener('keypress', (e) => logWithTimestamp('Key Pressed!', e));
        targetElement.addEventListener('mousedown', (e) => logWithTimestamp('Mouse Down!', e));
        targetElement.addEventListener('mouseup', (e) => logWithTimestamp('Mouse Up!', e));
    }

    // Waits for a single element matching the selector
    async function waitForElement(selector, source, timeout = 5000) {
        return new Promise((resolve, reject) => {
            const interval = setInterval(() => {
                const element = source.querySelector(selector);
                if (element) {
                    clearInterval(interval);
                    resolve(element);
                }
            }, 100);

            setTimeout(() => {
                clearInterval(interval);
                if(debug) logWithTimestamp(`Element with selector \"${selector}\" not found within timeout.`);
                resolve(null);
            }, timeout);
        });
    }

    // Waits for at least a single element matching the selector
    async function waitForElements(selector, source, timeout = 5000) {
        return new Promise((resolve, reject) => {
            const interval = setInterval(() => {
                const elements = source.querySelectorAll(selector);
                if (elements) {
                    clearInterval(interval);
                    resolve(elements);
                }
            }, 100);

            setTimeout(() => {
                clearInterval(interval);
                if(debug) logWithTimestamp(`Elements with selector \"${selector}\" not found within timeout.`);
                resolve(null);
            }, timeout);
        });
    }

    // Sets the value of the input element and ensures changes are registered
    async function setInputValue(inputElement, value) {
        if (inputElement) {
            const valueSetter = Object.getOwnPropertyDescriptor(
                Object.getPrototypeOf(inputElement),
                'value'
            )?.set;

            if (valueSetter) {
                valueSetter.call(inputElement, value);
            } else {
                inputElement.value = value;
            }
            dispatchInputChange(inputElement);
        } else {
            logWithTimestamp("Input element is null or undefined.", "warn");
        }
    }

    // Dispatches input and change events for the input element
    async function dispatchInputChange(inputElement) {
        logWithTimestamp(`Dispatching Change Event to ${inputElement.name}`);
        if (inputElement) {
            ['change', 'input'].forEach((eventType) => {
                const event = new Event(eventType, { bubbles: true });
                inputElement.dispatchEvent(event);
            });
        }
    }

    // Finds and clicks the submit button
    async function findAndClickButton(submitButton) {
        if (submitButton) {
            if (submitButton.getAttribute('aria-disabled') === 'true') {
                logWithTimestamp("Submit button is disabled. Removing disabled attribute.");
            }
            submitButton.removeAttribute('aria-disabled');
            submitButton.click();
            logWithTimestamp("Submit button clicked.");
        } else {
            logWithTimestamp("Submit button not found.", "warn");
        }
    }

    // To control debounce timing
    let debounceTimeout = null;
    // The DEBOUNCE_DELAY prevents the script from executing actions too frequently by grouping mutation events within a set interval (1 second by default).
    const DEBOUNCE_DELAY = 1000;

    /*
     * Use MutationObserver with Debounce and log the many times it gets triggered
     * Guarantees that when there's a rapid successive number of mutations on the page, it ignores it
     * Only "processes mutations" after at least 1 second without mutations occurs.
    */
    const observer = new MutationObserver((mutations) => {
        if(debug) logWithTimestamp(`Observer Triggered... Clearing timeout of ${debounceTimeout} ms`);
        if (debounceTimeout) clearTimeout(debounceTimeout); // Clear the previous timeout
        if(debug) logWithTimestamp(`Setting a timeout of ${DEBOUNCE_DELAY} ms`);
        debounceTimeout = setTimeout(() => processMutations(mutations), DEBOUNCE_DELAY);
    });

    /*_____________________*/
    /* End of Common Tools */
    /*_____________________*/

    async function processMutations(mutations) {
        console.clear();
        if(debug) logWithTimestamp(`Found the following mutations`,mutations);
        await incrementExecutionNumber(mutations);
        if(await locateKeepAliveAndExtendSession()){
            revivalCount++;
        }
        giveStatusUpdate("Keep-Alive");
    }

    // Define a function to locate the Keep-Alive button and press it to extend it
    async function locateKeepAliveAndExtendSession() {
        const sessionButtons = await waitForElements('button.cf-button', document);
        for (const button of sessionButtons) {
            // Check if the button's text content is "Sim"
            if (button.textContent.trim() === 'Sim') {
                logWithTimestamp('Session keep-alive button found:', button);
                await findAndClickButton(button); // Your existing function to click the button
                return true; // Indicate success
            }
        }
        logWithTimestamp('Session keep-alive button not found!')
        return false; // Indicate failure
    }

    console.log(`JoaoWorkspace's Userscript injected! Starting observer on this page's document: ${window.location.href}`);
    // Start observing only after the entire page is fully loaded
    window.addEventListener('load', () => {
        observer.observe(document.body, { childList: true, subtree: true });
        console.log('Observer started after window load.');
    });

})();