Auto Claim with Monitor/Manager UI: StormGain Miner + 15 Faucets + Promo Codes processing

------------------------------------------------------------------------------------------------------------------------------------------------

目前為 2021-05-06 提交的版本,檢視 最新版本

// ==UserScript==
// @name         Auto Claim with Monitor/Manager UI: StormGain Miner + 15 Faucets + Promo Codes processing
// @namespace    satology.onrender.com
// @version      1.0.5
// @description  ------------------------------------------------------------------------------------------------------------------------------------------------
// @description  MAIN FEATURES:
// @description  > Automatic hourly rolls for 15 faucets (ADA, BNB, BTC, DASH, DOGE, ETH, LINK, LTC, NEO, STEAM, TRX, USDC, USDT, XEM, XRP)
// @description  > Automatic activation of StormGain Miner (free BTC every 4 hours)
// @description  > Accepts promotion codes (http://twitter.com/cryptosfaucets, free roll shortlinks) for the 15 faucets
// @description  > Simple Monitor UI on top of a website to track progress (claims, next rolls, promo codes)
// @description  ------------------------------------------------------------------------------------------------------------------------------------------------
// @description  > The idea is to release future versions with more faucets & PTC (some for FaucetPay/ExpressCrypto) and user-friendly configurations
// @description  ------------------------------------------------------------------------------------------------------------------------------------------------
// @description  IMPORTANT CONSIDERATIONS:
// @description  0. You need to enable popups on the Manager UI website to be able to open the faucets
// @description  1. Promo codes (for now) must be manually added through the Manager UI:
// @description     You can add multiple codes and will be processed for each faucet after rolling.
// @description     After adding a new promo code, it will take a minute or so to save it. Then will
// @description     try to activate it right AFTER the next scheduled roll for each faucet and roll again.
// @description     For a smoother perfomance, once in a while click Remove ALL to delete the codes if the faucets already processed them.
// @description  2. FAUCETS WEBSITES MUST OPEN IN ENGLISH TO BE ABLE TO RECOGNIZE IF THE PROMO CODE WAS ACCEPTED
// @description     In case you don't want to have them in English, you need to change the 3 strings the code uses for validation
// @description     (Search for localeStrings in the code and replace them)
// @description  3. Autorolls will trigger ONLY when the faucet was opened by the Manager UI.
// @description     This is to allow users to navigate the websites to get the ShortLinks extra rolls, for example,
// @description     without having to stop the script.
// @description  4. No AutoLogin implemented yet, so YOU MUST BE LOGGED IN
// @description  5. You can disable faucets from the script in case you need to or you are not registered yet.
// @description     It would be great if you could use my referral links listed below if you need an account.
// @description     To disable them, just set enabled: false in the webList array & refresh the manager
// @description  6. All data stored for tracking and to be displayed is stored locally in your environment. Nothing is uploaded.
// @description
// @description  Always opened to feedback. I'd be glad to hear from you if you find any bugs, have suggestions or new enhancements/features you'd like to see
// @description  ------------------------------------------------------------------------------------------------------------------------------------------------
// @description  About the code:
// @description  Manager UI (monitor):
// @description  - Let's you keep track of last claimed amount, accumulated claims, total balance and next rolls.
// @description  - Controls the 'flow'. Opens a new tab to roll when needed and reads the results.
// @description  - Let's you add the promo codes and shows you the status of them for each faucet
// @description  - Everything is stored locally, but the Manager UI runs on top of a personal website with some ads
// @description    You can, of course, replace it with another URL, but please consider keeping it as a 'thank you' if you find the script useful/helpful
// @description  - HTML and CSS are basic/simple as the goal is only to show status data and I'm not a UI/UX expert
// @description  SGProcessor:
// @description  - Works on StormGain website
// @description  - Activates the miner whenever is stopped (every 4 hours)
// @description  - Saves the balance to be displayed on the Manager UI
// @description  CFProcessor:
// @description  - Works on the 15 faucets (.../free)
// @description  - Creates some random 'interaction'. You can disable interactions or adjust them a little.
// @description    Search for RandomInteractionLevel
// @description  - After clicking the roll button, waits for the countdown or reloads the page if the invisible captcha validation fails (waits around 90 seconds)
// @description  - Stores the claimed amount, balance and time for the Manager UI to update itself
// @description  - If the Roll button is not there, stores the countdown value to adjust the Manager UI next roll time
// @description  ------------------------------------------------------------------------------------------------------------------------------------------------
// @description  For upcoming updates:
// @description  - Keep a second window always opened to do all the navigation
// @description  - Extra antibot random actions like going back and forward from a random page in the website (FAQ, Stats, etc.)
// @description  - Display a message in the faucets UI to let the user know the process current status
// @description  - AutoLogin using local variables to store the credentials
// @description  - Enable/Disable faucet from the Manager UI
// @description  - Code refactor
// @description  - Implement FaucetPay PTC autoclicker (https://faucetpay.io/?r=1140585)
// @description  - Implement Freebitco.in autoclaim (https://freebitco.in/?r=41092365)
// @description  - Implement autonavigator for captcha faucets to automatically open and prompt for input when you can claim
// @description  - Implement auto claim for other faucets with 'weak' captcha validations
// @description  ------------------------------------------------------------------------------------------------------------------------------------------------
// @description  Links to create a new account using my referral:
// @description  https://app.stormgain.com/friend/BNS27140552
// @description  https://freecardano.com/?ref=335463
// @description  https://freebinancecoin.com/?ref=161127
// @description  https://freebitcoin.io/?ref=490252
// @description  https://freedash.io/?ref=124083
// @description  https://free-doge.com/?ref=97166
// @description  https://freeethereum.com/?ref=204076
// @description  https://freechainlink.io/?ref=78652
// @description  https://free-ltc.com/?ref=117042
// @description  https://freeneo.io/?ref=100529
// @description  https://freesteam.io/?ref=117686
// @description  https://free-tron.com/?ref=145047
// @description  https://freeusdcoin.com/?ref=100434
// @description  https://freetether.com/?ref=181230
// @description  https://freenem.com/?ref=295274
// @description  https://coinfaucet.io/?ref=808298
// @description  ------------------------------------------------------------------------------------------------------------------------------------------------
// @description  If you wanna team up or just share some ideas, you can contact me at [email protected]
// @description  ------------------------------------------------------------------------------------------------------------------------------------------------
// @author       satology
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        window.close
// @grant        GM_openInTab
// @icon         https://www.google.com/s2/favicons?domain=stormgain.com
// @require      https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js
// @match        https://satology.onrender.com/faucets/referrals*
// @match        https://app.stormgain.com/crypto-miner/
// @match        https://freecardano.com/free
// @match        https://freebinancecoin.com/free
// @match        https://freebitcoin.io/free
// @match        https://freedash.io/free
// @match        https://free-doge.com/free
// @match        https://freeethereum.com/free
// @match        https://freechainlink.io/free
// @match        https://free-ltc.com/free
// @match        https://freeneo.io/free
// @match        https://freesteam.io/free
// @match        https://free-tron.com/free
// @match        https://freeusdcoin.com/free
// @match        https://freetether.com/free
// @match        https://freenem.com/free
// @match        https://coinfaucet.io/free
// @match        https://freecardano.com/promotion/*
// @match        https://freebinancecoin.com/promotion/*
// @match        https://freebitcoin.io/promotion/*
// @match        https://free-doge.com/promotion/*
// @match        https://freedash.io/promotion/*
// @match        https://freeethereum.com/promotion/*
// @match        https://freechainlink.io/promotion/*
// @match        https://free-ltc.com/promotion/*
// @match        https://freeneo.io/promotion/*
// @match        https://freesteam.io/promotion/*
// @match        https://free-tron.com/promotion/*
// @match        https://freeusdcoin.com/promotion/*
// @match        https://freetether.com/promotion/*
// @match        https://freenem.com/promotion/*
// @match        https://coinfaucet.io/promotion/*
// ==/UserScript==

(function() {
    'use strict';
    /**
      * Specific string values to check if a promotion code was succesfully processed (used via indexOf).
      * Defaults are set for English.
      * If you are viewing the faucets in another language, you will need to change them or
      * switch the faucets to English
      */
    const localeConfig = {
        stringSearches: {
            promoCodeAccepted: 'roll',
            promoCodeUsed: 'already used',
            promoCodeInvalid: 'not found',
            promoCodeInvalid2: 'only alphanumeric'
        }
    };
    const WebType = {
        CRYPTOSFAUCETS: 1,
        STORMGAIN: 2
    };
    const PromoStatus = {
        NOCODE: 0,
        PENDING: 1,
        ACCEPTED: 2,
        USEDBEFORE: 3,
        INVALID: 4,
        UNKNOWNERROR: 5
    };
    const RandomInteractionLevel = {
        NONE: 0,
        LOW: 1,
        MEDIUM: 2,
        HIGH: 3
    };

    let persistence, shared, manager, ui, CFPromotions, interactions, SGProcessor, CFProcessor, CFHistory;

    let helpers = {
        cleanString: function(input) {
            var output = "";
            for (var i=0; i<input.length; i++) {
                if (input.charCodeAt(i) <= 127) {
                    output += input.charAt(i);
                }
            }
            return output;
        },
        shuffle: function (array) {
            let currentIndex = array.length, temporaryValue, randomIndex;

            while (0 !== currentIndex) {
                randomIndex = Math.floor(Math.random() * currentIndex);
                currentIndex -= 1;
                temporaryValue = array[currentIndex];
                array[currentIndex] = array[randomIndex];
                array[randomIndex] = temporaryValue;
            }

            return array;
        },
        getPrintableTime: function (date = new Date()) {
            return ('0' + date.getHours()).slice(-2) + ':' + ('0' + date.getMinutes()).slice(-2) + ':' + ('0' + date.getSeconds()).slice(-2)
        },
        getPrintableDateTime: function (date) {
            return ('0' + date.getDate()).slice(-2) + '/' + ('0' + (date.getMonth() + 1)).slice(-2) + ' ' + ('0' + date.getHours()).slice(-2) + ':' + ('0' + date.getMinutes()).slice(-2);
        },
        getEnumText: function (enm, value) {
            return Object.keys(enm).find(key => enm[key] === value);
        },
        randomMs: function (a, b){
            return a + (b - a) * Math.random();
        },
        addMinutes: function(date, mins) {
            return date.setMinutes(date.getMinutes() + parseInt(mins) + 1);
        },
        randomInt: function(min, max) {
            return Math.floor(Math.random() * (max - min + 1) + min);
        },
        addMilliseconds: function(date, ms) {
            return date.setMilliseconds(date.getMilliseconds() + ms);
        },
        getEmojiForPromoStatus: function(promoStatus) {
            switch (promoStatus) {
                case PromoStatus.NOCODE:
                    return '⚪';
                    break;
                case PromoStatus.PENDING:
                    return '⏳';
                    break;
                case PromoStatus.ACCEPTED:
                    return '✔️';
                    break;
                case PromoStatus.USEDBEFORE:
                    return '🕙';
                    break;
                case PromoStatus.INVALID:
                    return '❌';
                    break;
                case PromoStatus.UNKNOWNERROR:
                    return '❗';
                    break;
            }
        }
    }


    let objectGenerator = {
        createPersistence: function() {
            const prefix = 'autoWeb_';
            function save(key, value, parseIt = false) {
                GM_setValue(prefix + key, parseIt ? JSON.stringify(value) : value);
            };
            function load(key, parseIt = false) {
                let value = GM_getValue(prefix + key);
                if(value && parseIt) {
                    value = JSON.parse(value);
                }
                return value;
            };
            return {
                save: save,
                load: load
            };
        },
        createShared: function() {
            let flowControl;
            function isOpenedByManager(currentUrl) {
                loadFlowControl();
                if(!flowControl) {
                    return false;
                }
                let millisecondsDistance = new Date() - flowControl.requestedTime;
                if(flowControl.opened || flowControl.url != currentUrl || millisecondsDistance > 120000) {
                    return false;
                }
                return true;
            };
            function setFlowControl(id, url, webType, keepFailedAttempts = false) {
                flowControl = {
                    id: id,
                    url: url,
                    type: webType,
                    requestedTime: new Date(),
                    failedAttempts: (keepFailedAttempts && flowControl.failedAttempts) ? flowControl.failedAttempts : 0,
                    opened: false,
                    result: {}
                };
                persistence.save('flowControl', flowControl, true);
            };
            function wasVisited(expectedId) {
                loadFlowControl();
                return flowControl.id == expectedId && flowControl.opened;
            };
            function getResult() {
                return flowControl.result;
            };
            function getCurrent() {
                let current = {};
                current.url = flowControl.url;
                current.type = flowControl.type;
                return current;
            };
            function saveAndclose(runDetails, delay = 0) {
                markAsVisited(runDetails);
                if(delay) {
                    setTimeout(window.close, delay);
                } else {
                    window.close();
                }
            };
            function loadFlowControl() {
                flowControl = persistence.load('flowControl', true);
            };
            function markAsVisited(runDetails) {
                flowControl.opened = true;
                flowControl.result = runDetails;
                persistence.save('flowControl', flowControl, true);
            };
            return {
                setFlowControl: setFlowControl,
                wasVisited: wasVisited,
                isOpenedByManager: isOpenedByManager,
                getCurrent: getCurrent,
                getResult: getResult,
                closeWindow: saveAndclose
            };
        },
        createManager: function() {
            let timestamp = null;
            let promoInterval;
            let webList = [
                { id: '1', name: 'ADA', url: 'https://freecardano.com/free', type: WebType.CRYPTOSFAUCETS, enabled: true },
                { id: '2', name: 'BNB', url: 'https://freebinancecoin.com/free', type: WebType.CRYPTOSFAUCETS, enabled: true },
                { id: '3', name: 'BTC', url: 'https://freebitcoin.io/free', type: WebType.CRYPTOSFAUCETS, enabled: true },
                { id: '4', name: 'DASH', url: 'https://freedash.io/free', type: WebType.CRYPTOSFAUCETS, enabled: true },
                { id: '5', name: 'ETH', url: 'https://freeethereum.com/free', type: WebType.CRYPTOSFAUCETS, enabled: true },
                { id: '6', name: 'LINK', url: 'https://freechainlink.io/free', type: WebType.CRYPTOSFAUCETS, enabled: true },
                { id: '7', name: 'LTC', url: 'https://free-ltc.com/free', type: WebType.CRYPTOSFAUCETS, enabled: true },
                { id: '8', name: 'NEO', url: 'https://freeneo.io/free', type: WebType.CRYPTOSFAUCETS, enabled: true },
                { id: '9', name: 'STEAM', url: 'https://freesteam.io/free', type: WebType.CRYPTOSFAUCETS, enabled: true },
                { id: '10', name: 'TRX', url: 'https://free-tron.com/free', type: WebType.CRYPTOSFAUCETS, enabled: true },
                { id: '11', name: 'USDC', url: 'https://freeusdcoin.com/free', type: WebType.CRYPTOSFAUCETS, enabled: true },
                { id: '12', name: 'USDT', url: 'https://freetether.com/free', type: WebType.CRYPTOSFAUCETS, enabled: true },
                { id: '13', name: 'XEM', url: 'https://freenem.com/free', type: WebType.CRYPTOSFAUCETS, enabled: true },
                { id: '14', name: 'XRP', url: 'https://coinfaucet.io/free', type: WebType.CRYPTOSFAUCETS, enabled: true },
                { id: '15', name: 'StormGain', url: 'https://app.stormgain.com/crypto-miner/', type: WebType.STORMGAIN, enabled: true },
                { id: '16', name: 'DOGE', url: 'https://free-doge.com/free', type: WebType.CRYPTOSFAUCETS, enabled: true }
            ];

            function start(){
                loader.initialize();
                ui.init(getCFlist());
                update();
                promoInterval = setInterval(manager.readNewPromoCode, 5000);
                setTimeout(manager.process, 10000);
            };
            let loader = function() {
                function initialize() {
                    setTimestamp();
                    initializeWebList();
                    initializePromotions();
                    initializeHistory();
                };
                function initializeWebList() {
                    let storedData = persistence.load('webList', true);
                    if(storedData) {
                        let newOnes = addNewOnes(storedData);
                        if (newOnes) {
                            newOnes.forEach( function (element, idx, arr) {
                                storedData.push(element);
                            });
                        }

                        let disabledList = webList.filter( x => !x.enabled ).map( x => x.id );
                        storedData.forEach( function (element, idx, arr) {
                            arr[idx].nextRoll = new Date(element.nextRoll);
                            arr[idx].enabled = !disabledList.includes(element.id);
                        });

                        webList = storedData;
                        setup();
                    } else {
                        setup(true);
                    }
                };
                function addNewOnes(storedData) {
                    let allIds = webList.map( x => x.id );
                    let storedIds = storedData.map( x => x.id );

                    let newOnes = allIds.filter( x => !storedIds.includes(x) );

                    return webList.filter( x => newOnes.includes(x.id) );
                };
                function initializePromotions() {
                    let storedData = persistence.load('CFPromotions', true);
                    if (storedData) {
                        storedData.forEach( function (element, idx, arr) {
                            arr[idx].added = new Date(element.added);
                        });
                        CFPromotions.load(storedData);
                    }
                };
                function initializeHistory() {
                    CFHistory.initOrLoad();
                };
                function setTimestamp() {
                    timestamp = new Date().getTime();
                    persistence.save('timestamp', timestamp);
                };
                function setup(reset = false) {
                    if(reset) {
                        helpers.shuffle(webList);
                    }

                    let timeDistance = 0;
                    webList.forEach( function (element, idx, arr) {
                        if (reset || !element.lastClaim) {
                            arr[idx].lastClaim = 0;
                        }
                        if (reset || !element.aggregate) {
                            arr[idx].aggregate = 0;
                        }
                        if (reset || !element.balance) {
                            arr[idx].balance = 0;
                        }
                        if (reset || !element.nextRoll) {
                            timeDistance += helpers.randomMs(10000, 15000);
                            arr[idx].nextRoll = new Date(helpers.addMilliseconds(new Date(), timeDistance));
                        }
                    });
                };
                return {
                    initialize: initialize
                };
            }();
            function update(sortIt = true) {
                if(sortIt) {
                    webList.sort((a,b) => a.nextRoll.getTime() - b.nextRoll.getTime());
                }
                persistence.save('webList', webList, true);
                ui.refresh(webList, CFPromotions.getAll());
                updateRollStatsSpan();
            };
            function process() {
                if(isObsolete()) {
                    return;
                }
                if(webList[0].nextRoll.getTime() < (new Date()).getTime()) {
                    ui.log('Opening: ' + webList[0].name);
                    open();
                } else {
                    let timeUntilNext = webList[0].nextRoll.getTime() - (new Date()).getTime() + helpers.randomMs(1000, 2000);
                    ui.log('Waiting ' + (timeUntilNext/1000/60).toFixed(2) + ' minutes...');
                    setTimeout(manager.process, timeUntilNext);
                }
            };
            function isObsolete() {
                let savedTimestamp = persistence.load('timestamp');
                if (savedTimestamp && savedTimestamp > timestamp) {
                    ui.log('<b>STOPING EXECUTION!<b> A new Manager UI window was opened. Process should continue there');
                    clearInterval(promoInterval);
                    return true;
                }
                return false;
            };
            function open(promoCode) {
                let navUrl = webList[0].url;
                if(promoCode) {
                    navUrl = getPromoUrl(promoCode);
                }
                shared.setFlowControl(webList[0].id, navUrl, webList[0].type);
                setTimeout(manager.resultReader, 10000);
                GM_openInTab(navUrl, 'loadInBackground');
            };

            function getPromoUrl(promoCode) {
                ui.log('Creating Promo Code URL...');
                let url = webList[0].url;
                url = url.slice(0, url.length - 4);
                return url + "promotion/" + promoCode;
            }

            function resultReader() {
                if(isObsolete()) {
                    return;
                }

                if(shared.wasVisited(webList[0].id)) {
                    let result = shared.getResult();

                    if (result) {
                        updateWebListItem(result);
                        if ( (webList[0].type == WebType.CRYPTOSFAUCETS) &&
                            ( (result.claimed) || (result.promoStatus && result.promoStatus != PromoStatus.ACCEPTED) )) {
                                let promoCode = CFPromotions.hasPromoAvailable(webList[0].id);
                                if (promoCode) {
                                    update(false);
                                    open(promoCode);
                                    return;
                                }
                        }
                    } else {
                        ui.log('Unable to read last run result, for ID: ' + webList[0].id + ' > ' + webList[0].name);
                    }

                    update(true);
                    process();
                    return;
                } else {
                    ui.log('Waiting for ' + webList[0].name + ' results...');
                    setTimeout(manager.resultReader, 10000);
                }
            };

            function updateWebListItem(result) {
                ui.log('Updating data: ' + JSON.stringify(result));
                if (result.claimed) {
                    result.claimed = parseFloat(result.claimed);
                    if(!isNaN(result.claimed)) {
                        webList[0].lastClaim = result.claimed;
                        webList[0].aggregate += result.claimed;
                    }
                }
                if(result.balance) {
                    webList[0].balance = result.balance;
                }
                if(result.nextRoll) {
                    webList[0].nextRoll = new Date(result.nextRoll);
                }
                if(result.promoStatus) {
                    CFPromotions.updateFaucetForCode(result.promoCode, webList[0].id, result.promoStatus);
                }
                if(result.rolledNumber) {
                    CFHistory.addRoll(result.rolledNumber);
                }
            };

            function readNewPromoCode() {
                let promoCodeElement = $('#promo-code-new')[0];
                let promoCode = helpers.cleanString(promoCodeElement.innerText);
                let promoDisplayStatus = $('#promo-display-status')[0];

                if (promoCode == 'REMOVEALLPROMOS' ) {
                    CFPromotions.removeAll();
                    promoCodeElement.innerText = '';
                    promoDisplayStatus.innerHTML = 'Promo codes removed!';
                    ui.refresh(null, CFPromotions.getAll());
                } else if(promoCode != '') {
                    CFPromotions.addNew(promoCode);
                    promoCodeElement.innerText = '';
                    $('#promo-text-input').val('');
                    promoDisplayStatus.innerHTML = 'Code ' + promoCode + ' added!';
                    ui.log('Promo code ' + promoCode + ' added');
                    ui.refresh(null, CFPromotions.getAll());
                }
            };

            function updateRollStatsSpan() {
                let rollsSpanElement = $('#rolls-span')[0];
                rollsSpanElement.innerText = CFHistory.getRollsMeta().join(',');
            };

            function getCFlist() {
                let items;
                items = webList.filter(f => f.type === WebType.CRYPTOSFAUCETS);
                items = items.map(x => {
                    return {
                        id: x.id,
                        name: x.name
                    };});
                items.sort((a, b) => (a.name > b.name) ? 1 : -1);

                return items;
            };
            return{
                init:start,
                process: process,
                resultReader: resultReader,
                getFaucetsForPromotion: getCFlist,
                readNewPromoCode: readNewPromoCode
            };
        },
        createUi: function() {
            let logLines = ['', '', '', '', ''];
            function init(cfFaucets) {
                appendCSS();
                appendJavaScript();
                appendHtml();
                createPromoTable(cfFaucets);
            };
            function appendCSS() {
                let css = '';
                $('head').append(css);
            };
            function appendJavaScript() {
                let js = '';
                js += '<script language="text/javascript">';
                js += 'var myBarChart;';
                js += 'function savePromoCode() {';
                js += 'var promoText = document.getElementById("promo-text-input");';
                js += 'var promoCode = document.getElementById("promo-code-new");';
                js += 'var promoDisplayStatus = document.getElementById("promo-display-status");';
                js += 'promoCode.innerHTML = promoText.value.trim();';
                js += 'promoDisplayStatus.innerHTML = "Adding code&nbsp&quot;<b>" + promoCode.innerHTML + "</b>&quot;... This could take around a minute. Please wait..."';
                js += '}';
                js += 'function removeAllPromos() {';
                js += 'var promoCode = document.getElementById("promo-code-new");';
                js += 'var promoDisplayStatus = document.getElementById("promo-display-status");';
                js += 'promoCode.innerHTML = "REMOVEALLPROMOS";';
                js += 'promoDisplayStatus.innerHTML = "Removing all promotion codes... This could take around a minute. Please wait..."';
                js += '}';
                js += 'function openStatsChart() {';
                js += 'if(myBarChart) { myBarChart.destroy(); }';
                js += 'let statsFragment = document.getElementById("stats-fragment");';
                js += 'if (statsFragment.style.display === "block") { statsFragment.style.display = "none"; document.getElementById("stats-button").innerText = "Lucky Number Stats"; } else {';
                js += 'statsFragment.style.display = "block"; document.getElementById("stats-button").innerText = "Close Stats";';
                js += 'var canvas = document.getElementById("barChart");';
                js += 'var ctx = canvas.getContext("2d");';
                js += 'var dataSpan = document.getElementById("rolls-span");';
                js += 'var data = {';
                js += 'labels: ["0000-9885", "9886-9985", "9986-9993", "9994-9997", "9998-9999", "10000"],';
                js += 'datasets: [ { fill: false, backgroundColor: [ "#990000", "#660066", "#000099", "#ff8000", "#ffff00", "#00ff00"],';
                js += 'data: dataSpan.innerText.split(",") } ] };';
                js += 'var options = { plugins: { legend: { display: false } }, title: { display: true, text: "Rolled Numbers", position: "top" }, rotation: -0.3 * Math.PI };';
                js += 'myBarChart = new Chart(ctx, { type: "pie", data: data, options: options }); } }';
                js += '</script>';

                $('head').append(js);
            };
            function appendHtml() {
                let html ='';
                html += '<pre style="width:100%;" id="console-log"><b>Loading...</b></pre>';
                html += '<section id="stats-fragment" class="fragment" style="display:none;"><div class="container-fluid bg-dark "><div class="container py-1 "><div class="row align-items-center text-center justify-content-center">';
                html += '<div class="col-md-3"><canvas id="barChart"></canvas><span id="rolls-span" style="display:none;"></span></div></div></div></div></div></section>';
                html += '<section id="table-struct" class="fragment "><div class="container-fluid bg-dark "><div class="container py-1 "><div class="row mx-0"><div class="title col-3 px-0 text-white"><h2>Schedule</h2></div>';
                html += '<div class="title col-6 w-100 text-white"></div><div class="title col-3 text-right"><a class="btn  m-2 anchor btn-outline-primary" id="stats-button" onclick="openStatsChart()">Lucky Number Stats</a></div></div>';
                html += '<div class="row align-items-center text-center justify-content-end">';
                html += '<div class="col-12 order-lg-1 text-center"><div class="row justify-content-center"><div class="col table-responsive" id="schedule-container"></div></div></div></div></div></div></section>';
                html +='<section id="table-struct-promo" class="fragment "><div class="container-fluid bg-dark "><div class="container py-1 "><div class="row mx-0">';
                html +='<div class="title col-3 px-0 text-white"><h2>Promo Codes</h2></div><div class="title col-3 w-100 text-white">';
                html +='<div class="input-group my-0"><input type="text" class="form-control py-1" id="promo-text-input" placeholder="Type a Promo code...">';
                html +='<div class="input-group-append"><button class="btn btn-success" id="promo-button" onclick="savePromoCode()"><i class="fa fa-plus"></i></button>';
                html +='</div></div></div><div class="title col-4 text-white justify-content-end"><span id="promo-display-status" class="text-white"></span>';
                html +='<span id="promo-code-new"></span></div><div class="title col-2 text-right"><a class="btn  m-2 anchor btn-outline-danger" id="promo-button" onclick="removeAllPromos()">Remove All</a>';
                html +='</div></div><div class="row align-items-center text-center justify-content-end"><div class="col-12 order-lg-1 text-center">';
                html +='<div class="row justify-content-center"><div class="col table-responsive" id="promo-container"></div></div></div></div></div></div></section>';

                $('#referral-table').before(html);
                $('#schedule-container').append( createScheduleTable() );
            };
            function createPromoTable(faucets) {
                let tableStructure = '';
                tableStructure += '<table class="table table-bordered text-white" id="promo-table">';
                tableStructure += '<caption style="text-align: -webkit-center;">⏳ Pending ✔️ Accepted 🕙 Used Before ❌ Invalid code ❗ Unknown error ⚪ No code</caption>';
                tableStructure += '<thead><tr><th class="">Code</th><th class="">Added</th>';

                for (let i = 0, all = faucets.length; i < all; i++) {
                    tableStructure += '<th data-faucet-id="' + faucets[i].id + '">' + faucets[i].name + '</th>';
                }

                tableStructure += '</tr></thead><tbody id="promo-table-body"></tbody></table>';

                $('#promo-container').append( tableStructure );
            };
            function createScheduleTable() {
                let tableStructure = '';
                tableStructure += '<table class="table table-bordered text-white" id="schedule-table"><thead><tr>';
                tableStructure += '<th class="hide-on-mobile">#</th><th class="">Name</th><th class="">Last Claim</th>';
                tableStructure += '<th class="hide-on-mobile">Aggregate</th><th class="hide-on-mobile">Balance</th><th class="">Next Roll</th>';
                tableStructure += '</tr></thead><tbody id="schedule-table-body"></tbody></table>';

                return tableStructure;
            };
            function loadScheduleTable(data) {
                let tableBody = '';
                for(let i=0, all = data.length; i < all; i++) {
                    tableBody += '<tr class="align-middle" data-id="' + data[i].id + '">';
                    tableBody +='<td class="align-middle hide-on-mobile">' + (i + 1).toString() + '</td>';
                    tableBody +='<td class="align-middle">' + data[i].name + '</td>';
                    tableBody +='<td class="align-middle">' + data[i].lastClaim.toFixed(8) + '</td>';
                    tableBody +='<td class="align-middle hide-on-mobile">' + data[i].aggregate.toFixed(8) + '</td>';
                    tableBody +='<td class="align-middle hide-on-mobile">' + (data[i].balance ? data[i].balance.split(' ')[0] : "") + '</td>';
                    tableBody +='<td class="align-middle">' + helpers.getPrintableTime(data[i].nextRoll) + '</td>';
                    tableBody +='</tr>';
                }

                $('#schedule-table-body').html(tableBody);
            };
            function loadPromotionTable(codes) {
                let tableBody = '';

                for(let c=0; c < codes.length; c++) {
                    let data = codes[c];
                    tableBody += '<tr data-promotiobn-id="' + data.id + '">';
                    tableBody += '<td class="align-middle">' + data.code + '</td>';
                    tableBody +='<td class="align-middle">' + helpers.getPrintableDateTime(data.added) + '</td>';

                    for(let i=0, all = data.statusPerFaucet.length; i < all; i++) {
                        tableBody +='<td class="align-middle">' + helpers.getEmojiForPromoStatus(data.statusPerFaucet[i].status ?? 0) + '</td>';
                    }
                    tableBody +='</tr>';
                }

                $('#promo-table-body').html(tableBody);
            };
            function refresh(scheduleData, promotionData) {
                if (scheduleData) {
                    loadScheduleTable(scheduleData);
                }
                if (promotionData) {
                    loadPromotionTable(promotionData);
                }
            };
            function log(msg) {
                if(msg) {
                    logLines.pop();
                    logLines.unshift(helpers.getPrintableTime() + '&nbsp' + msg);
                    $('#console-log').html(logLines.join('<br>'));
                }
            };
            return {
                init: init,
                refresh: refresh,
                loadPromotionTable: loadPromotionTable,
                log: log
            }
        },
        createCFPromotions: function() {
            let codes = [];

            function PromotionCode(id, code) {
                this.id = id;
                this.code = code;
                this.added = new Date();
                this.statusPerFaucet = [];
            };

            function updateFaucetStatusInPromo(promo, faucetId, newStatus) {
            };

            function getFaucetStatusInPromo(promo, faucetId) {
                let faucet = promo.statusPerFaucet.find(x => x.id == faucetId);
                return faucet.status ?? PromoStatus.NOCODE;
            };

            function addNew(code) {
                let newPromo = new PromotionCode(codes.length, code);
                newPromo.statusPerFaucet = manager.getFaucetsForPromotion();
                newPromo.statusPerFaucet.forEach(function (element, idx, arr) {
                    arr[idx].status = PromoStatus.PENDING;
                });

                codes.push(newPromo);
                codes.sort((a, b) => (a.id < b.id) ? 1 : -1);
                save();
            };

            function getAll() {
                return codes;
            };

            function updateFaucetForCode(code, faucetId, newStatus) {
                let promo = codes.find(x => x.code == code);
                let faucet = promo.statusPerFaucet.find(x => x.id == faucetId);
                if(faucet) {
                    faucet.status = newStatus;
                }
                save();
            };

            function hasPromoAvailable(faucetId) {
                let resp = false;
                codes.forEach(function (promotion, idx, arr) {
                    let status = getFaucetStatusInPromo(promotion, faucetId);
                    if (status == PromoStatus.PENDING) {
                        resp = promotion.code;
                        return;
                    }
                });
                return resp;
            };

            function save() {
                persistence.save('CFPromotions', getAll(), true);
            };

            function load(data) {
                codes = data;
            };

            function removeAll() {
                codes = [];
                save();
            };
            return {
                addNew: addNew,
                removeAll: removeAll,
                getAll: getAll,
                load: load,
                updateFaucetForCode: updateFaucetForCode,
                hasPromoAvailable: hasPromoAvailable
            }
        },
        createInteractions: function(){
            let randomInteractionLevel = RandomInteractionLevel.MEDIUM;
            let maxActions = 0;
            let performedActions = -1;
            let selectableElements;
            let actions = {
                available: [
                    function() {
                        $('html, body').animate({
                            scrollTop: helpers.randomInt(0, $('html, body').get(0).scrollHeight)
                        }, {
                            complete: setTimeout(interactions.addPerformed, helpers.randomMs(100, 3000)),
                            duration: helpers.randomMs(100, 1500)
                        });
                    },
                    function() {
                        let element = interactions.selectableElements[helpers.randomInt(0, interactions.selectableElements.length - 1)];

                        try {
                            if (document.body.createTextRange) {
                                const range = document.body.createTextRange();
                                range.moveToElementText(element);
                                range.select();
                            } else if (window.getSelection) {
                                const selection = window.getSelection();
                                const range = document.createRange();
                                range.selectNodeContents(element);
                                selection.removeAllRanges();
                                selection.addRange(range);
                            }
                        } catch (err) { }

                        interactions.addPerformed();
                    }
                ]
            };

            function start(selectableElements) {
                performedActions = 0;
                switch(randomInteractionLevel) {
                    case RandomInteractionLevel.NONE:
                        maxActions = 0;
                        break;
                    case RandomInteractionLevel.LOW:
                        maxActions = helpers.randomInt(2, 4);
                        break;
                    case RandomInteractionLevel.MEDIUM:
                        maxActions = helpers.randomInt(5, 8);
                        break;
                    case RandomInteractionLevel.HIGH:
                        maxActions = helpers.randomInt(12, 16);
                        break;
                }
                interactions.selectableElements = selectableElements;
                performActions();
            }

            function performActions() {
                if(performedActions >= maxActions) {
                    return;
                }
                let delay = 0;
                for(let i = 0; i < maxActions; i++) {
                    delay += helpers.randomMs(350, 1500);
                    setTimeout(actions.available[helpers.randomInt(0, actions.available.length - 1)], delay);
                }
            }

            function addPerformed() {
                performedActions++;
            }
            function completed() {
                return (performedActions >= maxActions);
            }

            return {
                start: start,
                completed: completed,
                addPerformed: addPerformed,
                selectableElements: selectableElements
            };
        },
        createSGProcessor: function() {
            let timerSpans;
            function run() {
                if(isLoading()) {
                    setTimeout(SGProcessor.run, helpers.randomMs(5000, 10000));
                    return;
                } else {
                    if(isMinerActive()) {
                        processRunDetails();
                    } else {
                        activateMiner();
                    }
                }
            };
            function isLoading() {
                return $('#loader-logo').length;
            };
            function isMinerActive() {
                timerSpans = $('.mb-8 .wrapper .mb-1 span');
                if(timerSpans.length > 0) {
                    return true;
                } else {
                    return false;
                }
                return (timerSpans.length === 0);
            };
            function activateMiner() {
                const activateButton = document.querySelector('.mb-8 .wrapper button');
                if (activateButton) {
                    activateButton.click();
                    setTimeout(SGProcessor.processRunDetails, helpers.randomMs(10000, 20000));
                } else {
                    if(!is404Error()) {
                        SGProcessor.processRunDetails()
                    }
                }
            };

            function is404Error() {
                const h1 = document.getElementsByTagName('h1');
                if (h1.length > 0 && h1[0].innerText.includes('404')) {
                    window.location.reload();
                    return true;
                }
                return false;
            }

            function processRunDetails() {
                let result = {};
                result.nextRoll = helpers.addMinutes(new Date(), readCountdown().toString());
                result.balance = readBalance();
                shared.closeWindow(result);
            };
            function readCountdown() {
                let mins = 241;
                try {
                    let timeLeft = timerSpans.last().text().split(':');
                    if(timeLeft.length === 3) {
                        mins = parseInt(timeLeft[0]) * 60 + parseInt(timeLeft[1]);
                    }
                } catch (err) { }
                return mins;
            };
            function readBalance() {
                let balance = "";
                try {
                    balance = $('span.text-accent').first().text() + " BTC";
                } catch (err) { }
                return balance;
            };
            return {
                run: run,
                processRunDetails: processRunDetails
            };
        },
        createCFProcessor: function() {
            const NavigationProcess = {
                ROLLING: 1,
                PROCESSING_PROMOTION: 2
            };
            let navigationProcess;
            let countdown;
            let rollButton;
            let promotionTag;
            let timeWaiting= 0;

            function run() {
                navigationProcess = NavigationProcess.ROLLING;
                displayStatusUi();
                setTimeout(CFProcessor.findCountdownOrRollButton, helpers.randomMs(2000, 5000));
            };
            function runPromotion() {
                navigationProcess = NavigationProcess.PROCESSING_PROMOTION
                displayStatusUi();
                setTimeout(CFProcessor.findPromotionTag, helpers.randomMs(1000, 3000));
            };
            function findCountdownOrRollButton() {
                if( isCountdownVisible() && !isRollButtonVisible() ) {
                    timeWaiting = 0;
                    processRunDetails();
                } else if ( !isCountdownVisible() && isRollButtonVisible() ) {
                    timeWaiting = 0;
                    interact();
                } else {
                    if (timeWaiting/1000 > helpers.randomInt(15, 35)) {
                        window.location.reload();
                    }
                    timeWaiting += 3000;
                    setTimeout(CFProcessor.findCountdownOrRollButton, helpers.randomMs(2000, 5000));
                }
            };
            function interact() {
                let selectables = []
                selectables = selectables.concat($('td').toArray());
                selectables = selectables.concat($('p').toArray());
                selectables = selectables.concat($('th').toArray());

                interactions.start(selectables);
                setTimeout(CFProcessor.waitInteractions, helpers.randomMs(2000, 4000));
            }
            function waitInteractions() {
                if(interactions.completed()) {
                    roll();
                } else {
                    setTimeout(CFProcessor.waitInteractions, helpers.randomMs(2000, 4000));
                }
            }
            function isCountdownVisible() {
                countdown = $('.timeout-wrapper');
                return ($(countdown).length > 0 && $(countdown[0]).is(':visible'));
            };
            function isRollButtonVisible() {
                rollButton = $('.main-button-2.roll-button.bg-2');
                return ($(rollButton).length > 0 && $(rollButton[0]).is(':visible'));
            };
            function roll() {
                $(rollButton[0]).click();
                setTimeout(CFProcessor.findCountdownOrRollButton, helpers.randomMs(2000, 3000));
            }
            function isPromotionTagVisible() {
                let pTags = $('p');
                if (pTags.length > 0) {
                    promotionTag = $('p')[0];
                    return true;
                }
                return false;
            };
            function findPromotionTag() {
                if( isPromotionTagVisible() ) {
                    processRunDetails();
                } else {
                    setTimeout(CFProcessor.findPromotionTag, helpers.randomMs(2000, 5000));
                }
            };
            function processRunDetails() {
                let result = {};
                if(navigationProcess == NavigationProcess.ROLLING) {
                    result.nextRoll = readCountdown();
                    result.claimed = readClaimed();
                    result.balance = readBalance();
                    if(result.claimed != 0) {
                        result.rolledNumber = readRolledNumber();
                    }
                    result.balance = readBalance();
                } else if (navigationProcess == NavigationProcess.PROCESSING_PROMOTION) {
                    result.promoStatus = readPromoStatus();
                    result.promoCode = readPromoCode();
                    if (result.promoStatus == PromoStatus.ACCEPTED) {
                        result.nextRoll = helpers.addMinutes(new Date(), "-120");
                    }
                }
                shared.closeWindow(result);
            };
            function readCountdown() {
                let minsElement = $('.timeout-container .minutes .digits');
                let mins = "0";
                if ($(minsElement).length > 0) {
                    mins = $(minsElement)[0].innerHTML;
                }
                if (mins) {
                    return helpers.addMinutes(new Date(), mins.toString());
                } else {
                    return null;
                }
            };
            function readClaimed() {
                let claimed = 0;
                try {
                    claimed = $('.result')[0].innerHTML;
                    claimed = claimed.trim();
                    claimed = claimed.slice(claimed.lastIndexOf(" ") + 1);
                } catch(err) { }
                return claimed;
            };
            function readRolledNumber() {
                let number = 0;
                try {
                    number = $('.lucky-number').toArray().map(x => x.innerText).join('');
                    number = parseInt(number);
                } catch(err) { }
                return number;
            };
            function readBalance() {
                let balance = "";
                try {
                    balance = $('.navbar-coins.bg-1 a').first().text();
                } catch(err) { }
                return balance;
            };
            function readPromoStatus() {
                let promoStatus = PromoStatus.UNKNOWNERROR;
                try {
                    if(promotionTag.innerHTML.indexOf(localeConfig.stringSearches.promoCodeAccepted) > 0) {
                        return PromoStatus.ACCEPTED;
                    } else if(promotionTag.innerHTML.indexOf(localeConfig.stringSearches.promoCodeUsed) > 0) {
                        return PromoStatus.USEDBEFORE;
                    } else if(promotionTag.innerHTML.indexOf(localeConfig.stringSearches.promoCodeInvalid) > 0) {
                        return PromoStatus.INVALID;
                    } else if(promotionTag.innerHTML.indexOf(localeConfig.stringSearches.promoCodeInvalid) > 0) {
                        return PromoStatus.INVALID;
                    }
                } catch ( err ) { }
                return promoStatus;
            };
            function readPromoCode() {
                var urlSplit = window.location.href.split('/');
                return urlSplit[urlSplit.length - 1];
            };
            function displayStatusUi() {
                $( 'body' ).prepend( '<div class="withdraw-button bg-2" style="top:30%; z-index:1500;" href="#">⚙️ Processing</div>' );
            };
            return {
                run: run,
                runPromotion: runPromotion,
                findCountdownOrRollButton: findCountdownOrRollButton,
                findPromotionTag: findPromotionTag,
                waitInteractions: waitInteractions
            };
        },
        createCFHistory: function() {
            let rollsMeta = [
                { id: 0, range: '0000-9885', count: 0 },
                { id: 1, range: '9886-9985', count: 0 },
                { id: 2, range: '9986-9993', count: 0 },
                { id: 3, range: '9994-9997', count: 0 },
                { id: 4, range: '9998-9999', count: 0 },
                { id: 5, range: '10000', count: 0 }
            ];

            function initOrLoad() {
                let storedData = persistence.load('CFHistory', true);
                if(storedData) {
                    rollsMeta = storedData;
                }
            };

            function addRoll(number) {
                switch(true) {
                    case (number <= 9885):
                        rollsMeta[0].count++;
                        break;
                    case (number <= 9985):
                        rollsMeta[1].count++;
                        break;
                    case (number <= 9993):
                        rollsMeta[2].count++;
                        break;
                    case (number <= 9997):
                        rollsMeta[3].count++;
                        break;
                    case (number <= 9999):
                        rollsMeta[4].count++;
                        break;
                    case (number == 10000):
                        rollsMeta[5].count++;
                        break;
                    default:
                        break;
                }
                save();
            };

            function getRollsMeta() {
                return rollsMeta.map(x => x.count);
            };

            function save() {
                persistence.save('CFHistory', rollsMeta, true);
            };

            return {
                initOrLoad: initOrLoad,
                addRoll: addRoll,
                getRollsMeta: getRollsMeta
            }
        }
    };


    /**
    * Prevents alert popups to be able to reload the faucet if invisible captcha validation fails
    */
    function overrideSelectNativeJS_Functions () {
        window.alert = function alert (message) {
            console.log (message);
        }
    }
    function addJS_Node (text, s_URL, funcToRun) {
        var scriptNode= document.createElement ('script');
        scriptNode.type= "text/javascript";
        if (text)scriptNode.textContent= text;
        if (s_URL)scriptNode.src= s_URL;
        if (funcToRun)scriptNode.textContent = '(' + funcToRun.toString() + ')()';

        var element = document.getElementsByTagName ('head')[0] || document.body || document.documentElement;
        element.appendChild (scriptNode);
    }

    function detectWeb() {
        if(!shared.isOpenedByManager(window.location.href)) {
            return;
        }

        addJS_Node (null, null, overrideSelectNativeJS_Functions);
        if(window.location.href.indexOf('promotion') > 0) {
            CFProcessor = objectGenerator.createCFProcessor();
            interactions = objectGenerator.createInteractions();

            setTimeout(CFProcessor.runPromotion, helpers.randomMs(5000, 10000));
        } else if (shared.getCurrent().type == WebType.CRYPTOSFAUCETS) {
            CFProcessor = objectGenerator.createCFProcessor();
            interactions = objectGenerator.createInteractions();

            setTimeout(CFProcessor.run, helpers.randomMs(1000, 3000));
        } else if (shared.getCurrent().type == WebType.STORMGAIN) {
            SGProcessor = objectGenerator.createSGProcessor();

            setTimeout(SGProcessor.run, helpers.randomMs(10000, 20000));
        }
    }

    function init() {
        shared = objectGenerator.createShared();
        persistence = objectGenerator.createPersistence();
        if(window.location.host === 'satology.onrender.com') {
            manager = objectGenerator.createManager();
            CFPromotions = objectGenerator.createCFPromotions();
            ui = objectGenerator.createUi();
            CFHistory = objectGenerator.createCFHistory();

            manager.init();
            GM_openInTab('https://www.google.com', 'loadInBackground');
        } else {
            detectWeb();
        }
    }
    init();
})();