Greasy Fork

[satology] Auto Claim Multiple Faucets with Monitor UI

Freebitco.in, StormGain Miner, 15 Faucets with Promo Codes + Autologin and FaucetPay PTC

目前为 2021-07-07 提交的版本。查看 最新版本

// ==UserScript==
// @name         [satology] Auto Claim Multiple Faucets with Monitor UI
// @description  Freebitco.in, StormGain Miner, 15 Faucets with Promo Codes + Autologin and FaucetPay PTC
// @description  Automatic activation of StormGain Miner every 4 hours and hourly claim of free ADA, BNB, BTC, DASH, DOGE, ETH, LINK, LTC, NEO, STEAM, TRX, USDC, USDT, XEM, XRP
// @version      1.3.1
// @author       satology
// @namespace    satology.onrender.com
// @homepage     https://satology.onrender.com/faucets/referrals

// @note         - IMPORTANT ----------------------------------------------------------------------------------------------------------------------------------------------------
// @note                   YOU MUST HAVE A hCaptcha solver INSTALLED to claim from the faucets
// @note                   I recommend this script: https://greasyfork.org/en/scripts/425854-hcaptcha-solver-automatically-solves-hcaptcha-in-browser
// @note         ----------------------------------------------------------------------------------------------------------------------------------------------------------------

// @note         - LAST UPDATES -------------------------------------------------------------------------------------------------------------------------------------------------
// @note         [@1.3.0]  Added FaucetPay PTC. It runs on focus every 5 hs aprox (set by hoursBetweenRuns in config)
// @note         [@1.2.1]  Fixed BUG that didn't let new users enable the faucets, and changed the reusable promo codes interval
// @note         [@1.2.0]  What's new?
// @note                   > Activate/Deactivate faucets (from UI)
// @note                   > Run ASAP a specific faucet (from UI)
// @note                   > Change faucets 'display' name (from the UI)
// @note                   > Reusable promo codes option (by selecting 'Daily' from UI when adding a code)
// @note                   > Remove promo codes individually
// @note                   > FIAT conversion column, filled by the website using CMC Tickers
// @note                   > Some refactor on how the data is stored
// @note                   + Run ASAP and Faucet changes are not always immediate. If the script is claiming, it will wait until it's finished to avoid data errors.
// @note                   + FIAT conversion is done from the website as I was having some issues with the event listeners. Also, the ticker doesn't seem to be
// @note                     updating itself unless you change the fiat, so I'm open to suggestions if you know of other plug-n-play tickers
// @note                   + 'Reusable' promo codes are those codes that you can run every 24hs on every CF faucet. You might know that, at least for now,
// @note                      shortlink promo codes are always the same. I recommend doing the shortlinks daily anyway just to be safe.
// @note                      Reusable codes will be reprocessed automatically after 1 day for each faucet if their status is ACCEPTED or USEDBEFORE.
// @note                      You can verify the last run datetime by placing your mouse over the emojis.
// @note         [@1.1.0] Added Freebitco.in and autologin for @cryptosfaucets
// @note                   > You can enable autologin/credentials in the config object
// @note                   > You can enable activation of daily RPBonus for FreeBitco.in in the config object
// @note                   > Added boolean setToEnglish (DEFAULT TRUE) at localeConfig object, to change the language of the faucets to English.
// @note         ----------------------------------------------------------------------------------------------------------------------------------------------------------------

// @note         - MAIN FEATURES ------------------------------------------------------------------------------------------------------------------------------------------------
// @note         > Automatic hourly rolls for 16 faucets: Freebitco.in and @CryptosFaucets (ADA, BNB, BTC, DASH, DOGE, ETH, LINK, LTC, NEO, STEAM, TRX, USDC, USDT, XEM, XRP)
// @note         > Automatic activation of StormGain Miner (free BTC every 4 hours)
// @note         > FaucetPay PTC autoclaim
// @note         > Accepts promotion codes (http://twitter.com/cryptosfaucets, free roll shortlinks) for CF 15 faucets
// @note         > Simple Monitor UI on top of a website to track progress (claims, next rolls, promo codes)

// @note         - IMPORTANT CONSIDERATIONS -------------------------------------------------------------------------------------------------------------------------------------
// @note         0. You need to enable popups on the Manager UI website to be able to open the faucets
// @note         1. FAUCETS WEBSITES MUST OPEN IN ENGLISH TO BE ABLE TO RECOGNIZE IF THE PROMO CODE WAS ACCEPTED
// @note            In case you don't want to have them in English, you need to change the 3 strings the code uses for validation and change setToEnglish to false
// @note            (Search for localeStrings in the code)
// @note         2. Autorolls will trigger ONLY when the faucet was opened by the Manager UI.
// @note            This is to allow users to navigate the websites to get the ShortLinks extra rolls, for example,
// @note            without having to stop the script.
// @note         3. You can enable/disable faucets from the UI. By default they are all set to false.
// @note            It would be great if you could use my referral links listed below if you need an account.
// @note            To disable them, just set enabled: false in the webList array, save the script & refresh the manager
// @note         4. You can change the config object to enable autologin, RP free roll bonus, etc.
// @note         5. All data stored for tracking and to be displayed is stored locally in your environment. Nothing is uploaded.

// @note         Always open 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
// @note         ----------------------------------------------------------------------------------------------------------------------------------------------------------------
// @note         DISCLAIMER: This script is shared to help. Use at your own discretion. I've being using it for months and works fine but I cannot
// @note         guarantee that the faucets won't ban your IP or account
// @note         ----------------------------------------------------------------------------------------------------------------------------------------------------------------
// @note         Requested for upcoming updates:
// @note         - Add bagi.co.in
// @note         ----------------------------------------------------------------------------------------------------------------------------------------------------------------

// @note         ----------------------------------------------------------------------------------------------------------------------------------------------------------------
// @note         If you wanna team up or just share some ideas, you can contact me at [email protected]
// @note         ----------------------------------------------------------------------------------------------------------------------------------------------------------------

// @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/*
// @match        https://freebinancecoin.com/*
// @match        https://freebitcoin.io/*
// @match        https://freedash.io/*
// @match        https://free-doge.com/*
// @match        https://freeethereum.com/*
// @match        https://freechainlink.io/*
// @match        https://free-ltc.com/*
// @match        https://freeneo.io/*
// @match        https://freesteam.io/*
// @match        https://free-tron.com/*
// @match        https://freeusdcoin.com/*
// @match        https://freetether.com/*
// @match        https://freenem.com/*
// @match        https://coinfaucet.io/*
// @match        https://freebitco.in/
// @match        https://faucetpay.io/ptc
// @match        https://faucetpay.io/ptc*
// ==/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 want to view the faucets in another language, you will need to change the following values
      */
    const localeConfig = {
        setToEnglish: true, // will set the faucets to English
        stringSearches: {
            promoCodeAccepted: 'roll',
            promoCodeUsed: 'already used',
            promoCodeInvalid: ['not found', 'only alphanumeric'],
            promoCodeExpired: ['ended']
        }
    };

    const WebType = {
        CRYPTOSFAUCETS: 1,
        STORMGAIN: 2,
        FREEBITCOIN: 3,
        FAUCETPAY: 4
    };
    const CFUrlType = {
        HOME: 0,
        FREE: 1,
        CONTACTTWITTER: 2,
        PROMOTION: 3,
        STATS: 4,
        SETTINGS: 5,
        FREEROLLS: 6,
        IGNORE: 99
    };
    const PromoStatus = {
        NOCODE: 0,
        PENDING: 1,
        ACCEPTED: 2,
        USEDBEFORE: 3,
        INVALID: 4,
        UNKNOWNERROR: 5,
        EXPIRED: 6
    };
    const RandomInteractionLevel = {
        NONE: 0,
        LOW: 1,
        MEDIUM: 2,
        HIGH: 3
    };
    const HS_26_IN_MILLISECONDS = 93600000; //Using 26 hs instead of 24hs, and 2hs gap retry when code is flagged as USEDBEFORE
    const HS_2_IN_MILLISECONDS = 7200000; //Using 26 hs instead of 24hs, and 2hs gap retry when code is flagged as USEDBEFORE
    const USE_DEFAULT = -1;

    let config = {
        defaults: {
            timeout: 4,        // Use -1 to disable the timeout functionality. In minutes. Max time the monitor will wait for a result/roll.
                               // After timeout is reached, the Manager/Monitor will assume something is wrong
                               // (usually faucet tab being accidentally closed or failed to connect)
            postponeMinutes: 65,   // Minutes to wait before retrying a faucet that timed out
            focusTab: false    // If false the faucets will be opened in the background
        },
        cf: {
            autologin: false,
            credentials: {
                mode: 1, // POSSIBLE VALUES:
                         // 1: Use email/password saved through the script
                         // 2: Waits for the credentials to be added manually or by a third party software/extension.
                         //    Useful if you use different email/password combinations on each faucet
                email: '[email protected]',
                password: 'YOURPASSWORD'
            }
        },
        fb: { // FreeBitco.in
            activateRPBonus: false // will try to activate the highest RP Roll Bonus available for the account (100, 50 25, ... points per roll),
        },
        fp: { // FaucetPay.io
            hoursBetweenRuns: 5 // how many hours to wait between each FaucetPay PTC run
        }
    }

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

    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()) {
            if (date == null) {
                return '';
            }
            return ('0' + date.getHours()).slice(-2) + ':' + ('0' + date.getMinutes()).slice(-2) + ':' + ('0' + date.getSeconds()).slice(-2);
        },
        getPrintableDateTime: function (date) {
            if (date != null) {
                return ('0' + date.getDate()).slice(-2) + '/' + ('0' + (date.getMonth() + 1)).slice(-2) + ' ' + ('0' + date.getHours()).slice(-2) + ':' + ('0' + date.getMinutes()).slice(-2);
            } else {
                return '';
            }
        },
        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);
        },
        getRandomMillisecondsFromMinutesRange(minute, rangeDiffInPercentage) {
            const msCenter = minute * 60 * 1000;
            const msRangeDiff = Math.round(msCenter * rangeDiffInPercentage / 100);
            return helpers.randomMs(msCenter - msRangeDiff, msCenter + msRangeDiff);
        },
        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.EXPIRED:
                    return '📅';
                    break;
                case PromoStatus.UNKNOWNERROR:
                    return '❗';
                    break;
            }
        },
        getHost: function(url, withHttps = false) {
            if (url.includes('//')) {
                url = url.split('//')[1];
            }
            url = url.split('/')[0];
            return withHttps ? ('https://' + url) : url;
        },
        cf: {
            getUrlType: function(url) {
                if (url.endsWith('/free')) {
                    return CFUrlType.FREE;
                }
                if (url.includes('/promotion/')) {
                    return CFUrlType.PROMOTION;
                }
                if (url.endsWith('/contact-twitter')) {
                    return CFUrlType.CONTACTTWITTER;
                }
                if (url.endsWith('/free-rolls')) {
                    return CFUrlType.FREEROLLS;
                }
                if (url.endsWith('/settings')) {
                    return CFUrlType.SETTINGS;
                }
                if (url.endsWith('/stats')) {
                    return CFUrlType.STATS;
                }
                if (url.endsWith('/')) {
                    url = url.slice(0, -1);
                    if (url == helpers.getHost(url, true)) {
                        return CFUrlType.HOME;
                    }
                }

                return CFUrlType.IGNORE;
            }
        }
    }


    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(currentHost) {
                loadFlowControl();
                if(!flowControl) {
                    return false;
                }
                let millisecondsDistance = new Date() - flowControl.requestedTime;

                if((flowControl.type != WebType.FAUCETPAY && flowControl.opened) || (flowControl.host != currentHost && !flowControl.expectedHosts.includes(currentHost)) || millisecondsDistance > 120000) {
                    return false;
                }
                return true;
            };
            function setFlowControl(id, url, webType, expectedHosts) {
                flowControl = {
                    id: id,
                    url: url,
                    host: url.host,
                    type: webType,
                    requestedTime: new Date(),
                    opened: false,
                    expectedHosts: expectedHosts,
                    error: false,
                    result: {}
                };
                persistence.save('flowControl', flowControl, true);
            };
            function wasVisited(expectedId) {
                loadFlowControl();
                return flowControl.id == expectedId && flowControl.opened;
            };
            function hasErrors(expectedId) {
                return flowControl.id == expectedId && flowControl.error;
            };
            function getResult() {
                return flowControl.result;
            };
            function getCurrent() {
                let current = {};
                current.url = flowControl.url;
                current.host = flowControl.host;
                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);
            };
            function closeWithError(errorType, errorMessage) {
                flowControl.error = true;

                flowControl.result.errorType = errorType;
                flowControl.result.errorMessage = errorMessage;

                // flowControl.result = {
                //     errorType: errorType,
                //     errorMessage: errorMessage
                // };
                persistence.save('flowControl', flowControl, true);
                window.close();
            };
            function updateWithoutClosing(runDetails) {
                markAsVisited(runDetails);
            };
            return {
                setFlowControl: setFlowControl,
                wasVisited: wasVisited,
                isOpenedByManager: isOpenedByManager,
                getCurrent: getCurrent,
                getResult: getResult,
                closeWindow: saveAndclose,
                closeWithError: closeWithError,
                updateWithoutClosing: updateWithoutClosing,
                hasErrors: hasErrors
            };
        },
        createManager: function() {
            const STATUS = {
                INITIALIZING: 0,
                IDLE: 1,
                CLAIMING: 2
            };

            let timestamp = null;
            let timeWaiting = 0;
            let uiUpdatesInterval;
            let status = STATUS.INITIALIZING;
            let processTimer;
            let workingTab;

            let webList = [];

            const sites = [
                { id: '1', name: 'CF ADA', cmc: '2010', coinRef: 'ADA', url: new URL('https://freecardano.com/free'), rf: '?ref=335463', type: WebType.CRYPTOSFAUCETS },
                { id: '2', name: 'CF BNB', cmc: '1839', coinRef: 'BNB', url: new URL('https://freebinancecoin.com/free'), rf: '?ref=161127', type: WebType.CRYPTOSFAUCETS },
                { id: '3', name: 'CF BTC', cmc: '1', coinRef: 'BTC', url: new URL('https://freebitcoin.io/free'), rf: '?ref=490252', type: WebType.CRYPTOSFAUCETS },
                { id: '4', name: 'CF DASH', cmc: '131', coinRef: 'DASH', url: new URL('https://freedash.io/free'), rf: '?ref=124083', type: WebType.CRYPTOSFAUCETS },
                { id: '5', name: 'CF ETH', cmc: '1027', coinRef: 'ETH', url: new URL('https://freeethereum.com/free'), rf: '?ref=204076', type: WebType.CRYPTOSFAUCETS },
                { id: '6', name: 'CF LINK', cmc: '1975', coinRef: 'LINK', url: new URL('https://freechainlink.io/free'), rf: '?ref=78652', type: WebType.CRYPTOSFAUCETS },
                { id: '7', name: 'CF LTC', cmc: '2', coinRef: 'LTC', url: new URL('https://free-ltc.com/free'), rf: '?ref=117042', type: WebType.CRYPTOSFAUCETS },
                { id: '8', name: 'CF NEO', cmc: '1376', coinRef: 'NEO', url: new URL('https://freeneo.io/free'), rf: '?ref=100529', type: WebType.CRYPTOSFAUCETS },
                { id: '9', name: 'CF STEAM', cmc: '1230', coinRef: 'STEEM', url: new URL('https://freesteam.io/free'), rf: '?ref=117686', type: WebType.CRYPTOSFAUCETS },
                { id: '10', name: 'CF TRX', cmc: '1958', coinRef: 'TRX', url: new URL('https://free-tron.com/free'), rf: '?ref=145047', type: WebType.CRYPTOSFAUCETS },
                { id: '11', name: 'CF USDC', cmc: '3408', coinRef: 'USDC', url: new URL('https://freeusdcoin.com/free'), rf: '?ref=100434', type: WebType.CRYPTOSFAUCETS },
                { id: '12', name: 'CF USDT', cmc: '825', coinRef: 'USDT', url: new URL('https://freetether.com/free'), rf: '?ref=181230', type: WebType.CRYPTOSFAUCETS },
                { id: '13', name: 'CF XEM', cmc: '873', coinRef: 'XEM', url: new URL('https://freenem.com/free'), rf: '?ref=295274', type: WebType.CRYPTOSFAUCETS },
                { id: '14', name: 'CF XRP', cmc: '52', coinRef: 'XRP', url: new URL('https://coinfaucet.io/free'), rf: '?ref=808298', type: WebType.CRYPTOSFAUCETS },
                { id: '15', name: 'StormGain', cmc: '1', coinRef: 'BTC', url: new URL('https://app.stormgain.com/crypto-miner/'), rf: 'friend/BNS27140552', type: WebType.STORMGAIN },
                { id: '16', name: 'CF DOGE', cmc: '74', coinRef: 'DOGE', url: new URL('https://free-doge.com/free'), rf: '?ref=97166', type: WebType.CRYPTOSFAUCETS },
                { id: '17', name: 'FreeBitco.in', cmc: '1', coinRef: 'BTC', url: new URL('https://freebitco.in/'), rf: '?r=41092365', type: WebType.FREEBITCOIN },
                { id: '18', name: 'FaucetPay PTC', cmc: '1', coinRef: 'BTC', url: new URL('https://faucetpay.io/ptc'), rf: '?r=41092365', type: WebType.FAUCETPAY }
            ];

            function start() {
                loader.initialize();
                ui.init(getCFlist());
                update();
                uiUpdatesInterval = setInterval(readUpdateValues, 10000);
                processTimer = setTimeout(manager.process, 2000);
            };
            let loader = function() {
                function initialize() {
                    setTimestamp();
                    initializeWebList();
                    initializePromotions();
                    initializeHistory();
                };
                function initializeWebList() {
                    addSites();
                    addStoredData();
                };
                function addSites() {
                    sites.forEach(x => webList.push(x));
                    webList.forEach(function (element, idx, arr) {
                        arr[idx].enabled = false;
                        arr[idx].lastClaim = 0;
                        arr[idx].aggregate = 0;
                        arr[idx].balance = 0;
                        arr[idx].stats = {};
                        arr[idx].nextRoll = null;
                        arr[idx].focusTab = (element.id == '18' ? true : USE_DEFAULT);
                    });
                };
                function addStoredData() {
                    let storedData = persistence.load('webList', true);
                    if(storedData) {
                        storedData.forEach( function (element) {
                            let idx = webList.findIndex(x => x.id == element.id);
                            if(idx != -1) {
                                webList[idx].name = element.name ?? webList[idx].name;
                                webList[idx].lastClaim = element.lastClaim ?? webList[idx].lastClaim;
                                webList[idx].aggregate = element.aggregate ?? webList[idx].aggregate;
                                webList[idx].balance = element.balance ?? webList[idx].balance;
                                webList[idx].stats = element.stats ?? webList[idx].stats;
                                webList[idx].enabled = element.enabled ?? webList[idx].enabled;
                                if(!webList[idx].enabled) {
                                    webList[idx].nextRoll = null;
                                } else {
                                    webList[idx].nextRoll = element.nextRoll ? new Date(element.nextRoll) : new Date();
                                }
                                //webList[idx].focusTab = element.focusTab ?? webList[idx].focusTab;
                            }
                        });
                    } else {
//                        helpers.shuffle(webList);
                    }
                };
                function initializePromotions() {
                    let storedData = persistence.load('CFPromotions', true);
                    if (storedData) {
                        storedData.forEach( function (element, idx, arr) {
                            arr[idx].added = new Date(element.added);
                            arr[idx].statusPerFaucet.forEach( function (el, i, a) {
                                a[i].execTimeStamp = (el.execTimeStamp != null) ? new Date(el.execTimeStamp) : null;
                            });
                        });
                        CFPromotions.load(storedData);
                    }
                };
                function initializeHistory() {
                    CFHistory.initOrLoad();
                };
                function setTimestamp() {
                    timestamp = new Date().getTime();
                    persistence.save('timestamp', timestamp);
                };
                function removeDisabledFaucets() {
                    webList = webList.filter(x => x.enabled);
                };
                return {
                    initialize: initialize
                };
            }();
            function readUpdateValues(forceCheck = false) {
                readPromoCodeValues();

                if(status == STATUS.IDLE || forceCheck) {
                    let updateDataElement = $('#update-data')[0];
                    let updateValues = helpers.cleanString(updateDataElement.innerText);

                    if (updateValues != '') {
                        updateDataElement.innerText = '';
                        let updateObj = JSON.parse(updateValues);
                        if(updateObj.runAsap.changed) {
                            updateObj.runAsap.ids.forEach(function (element, idx, arr) {
                                try {
                                    let itemIndex = webList.findIndex(x => x.id == element)
                                    webList[itemIndex].enabled = true;
                                    webList[itemIndex].nextRoll = new Date(754000 + idx);
                                    ui.log(`${webList[itemIndex].name} updated to run ASAP`);
                                } catch (err) {
                                    ui.log(`Error setting faucet to run ASAP: ${err}`);
                                }
                            });
                        }
                        if(updateObj.editSingle.changed) {
                            updateObj.editSingle.items.forEach(function (element, idx, arr) {
                                try {
                                    let itemIndex = webList.findIndex(x => x.id == element.id);
                                    webList[itemIndex].name = element.displayName;

                                    if (webList[itemIndex].enabled != element.enabled) {
                                        webList[itemIndex].enabled = element.enabled;
                                        if(webList[itemIndex].enabled) {
                                            webList[itemIndex].nextRoll = new Date(idx);
                                        } else {
                                            webList[itemIndex].nextRoll = null;
                                        }
                                    }
                                    ui.log(`Faucet updated. New name: ${element.displayName}. Active: ${element.enabled}`);
                                } catch (err) {
                                    ui.log(`Error updating faucet data: ${err}`);
                                }
                            });
                        }

                        if(updateObj.runAsap.changed || updateObj.editSingle.changed) {
                            $('#faucets-display-status')[0].innerHTML = '';
                            update(true);
                            process();
                            return;
                        }
                    }
                }
                if(forceCheck) {
                    process();
                }
            };
            function update(sortIt = true) {
                if(sortIt) {
                    webList.sort( function(a,b) {
                        if (a === b) {
                            return 0;
                        } else if (a.nextRoll === null) {
                            return 1;
                        } else if (b.nextRoll === null) {
                            return -1;
                        } else {
                            return a.nextRoll.getTime() < b.nextRoll.getTime() ? -1 : 1;
                        }
                    });
                }

                saveWebList();
                ui.refresh(webList, CFPromotions.getAll());
                updateRollStatsSpan();
            };
            function saveWebList() {
                const data = webList.map(x => {
                    return {
                        id: x.id,
                        name: x.name,
                        lastClaim: x.lastClaim,
                        aggregate: x.aggregate,
                        balance: x.balance,
                        stats: x.stats,
                        nextRoll: x.nextRoll,
                        enabled: x.enabled
                    };});

                persistence.save('webList', data, true);
            }
            function process() {
                if(isObsolete()) {
                    return;
                }
                if(webList[0].nextRoll == null) {
                    ui.log(`All faucets are disabled. Click edit and select those you want to run...`);
                    clearTimeout(processTimer);
                    status = STATUS.IDLE;
                    return;
                }

                if(webList[0].nextRoll.getTime() < (new Date()).getTime()) {
                    ui.log(`Opening ${webList[0].name}`);
                    clearTimeout(processTimer);
                    status = STATUS.CLAIMING;
                    open();
                } else {
                    let timeUntilNext = webList[0].nextRoll.getTime() - (new Date()).getTime() + helpers.randomMs(1000, 2000);
                    ui.log(`Waiting ${(timeUntilNext/1000/60).toFixed(2)} minutes...`);
                    clearTimeout(processTimer);
                    processTimer = setTimeout(manager.process, timeUntilNext);
                    status = STATUS.IDLE;
                }
            };
            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(uiUpdatesInterval);
                    return true;
                }
                return false;
            };
            function open(promoCode) {
                let navUrl = webList[0].url;
                if(promoCode) {
                    navUrl = new URL('promotion/' + promoCode, webList[0].url.origin);
                    ui.log(`Opening ${webList[0].name} with Promo Code [${promoCode}]`);
                }

                let expectedHosts = [];
                expectedHosts.push(navUrl.host);
                if(webList[0].type == webList[0].type == WebType.STORMGAIN) {
                    expectedHosts.push((new URL('https://www.google.com').host));
                    expectedHosts.push((new URL('https://www.recaptcha.net').host));
                }

                shared.setFlowControl(webList[0].id, navUrl, webList[0].type, expectedHosts);
                setTimeout(manager.resultReader, 15000);
                workingTab = GM_openInTab(navUrl.href, { active: ((!webList[0].focusTab || webList[0].focusTab == USE_DEFAULT) ? config.defaults.focusTab : webList[0].focusTab) });
            };

            function getPromoUrl(promoCode) {
                return new URL('promotion/' + promoCode, webList[0].origin);
            }

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

                if (webList[0].type == WebType.FAUCETPAY && workingTab && !workingTab.closed) {
                    timeWaiting += 15;
                    ui.log(`Waiting for ${webList[0].name} results...`, timeWaiting);
                    setTimeout(manager.resultReader, 15000);
                    return;
                }

                if(shared.wasVisited(webList[0].id)) {

                    timeWaiting = 0;
                    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);
                    readUpdateValues(true);
                    return;
                } else {
//                     if (shared.hasNewClaimingStatus()) {
//                         timeWaiting = 0;
//                         let values = shared.getResult();
//                         if (values.status == 'CLAIMING' || values.status == 'CLAIMED') {
//                             ui.log(`${values.msg.pop()}`);
//                         }

//                         return;
//                     } else {
//                         timeWaiting += 15;
//                     }
                    timeWaiting += 15;
                    if (!shared.hasErrors(webList[0].id) && !hasTimedOut()) {
                        ui.log(`Waiting for ${webList[0].name} results...`, timeWaiting);
                        setTimeout(manager.resultReader, 15000);
                        return;
                    }

                    if (shared.hasErrors(webList[0].id)) {
                        webList[0].stats.errors = shared.getResult();
                        ui.log(`${webList[0].name} closed with error: ${webList[0].stats.errors.errorType} ${webList[0].stats.errors.errorMessage}`);
                    }

                    if (hasTimedOut()) {
                        if(webList[0].stats.countTimeouts) {
                            webList[0].stats.countTimeouts += 1;
                        } else {
                            webList[0].stats.countTimeouts = 1;
                        }

                        ui.log(`Waited too much time for ${webList[0].name} results: triggering timeout`);
                    }

                    let millisecondsDelay = helpers.getRandomMillisecondsFromMinutesRange(config.defaults.postponeMinutes, 5);
                    webList[0].nextRoll = new Date(helpers.addMilliseconds(new Date(), millisecondsDelay));
                    update(true);

                    timeWaiting = 0;
                    readUpdateValues(true);
                    return;
                }
            };

            function hasTimedOut() {
                return config.defaults.timeout != -1 && (timeWaiting > (config.defaults.timeout * 60));
            };

            function updateWebListItem(result) {
                ui.log(`Updating data: ${JSON.stringify(result)}`);
                webList[0].stats.countTimeouts = 0;
                webList[0].stats.errors = null;

                if (result.claimed) {
                    try {
                        result.claimed = parseFloat(result.claimed);
                    } catch { }
                    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 readPromoCodeValues() {
                let promoCodeElement = $('#promo-code-new')[0];
                let promoDataStr = helpers.cleanString(promoCodeElement.innerText);
                let promoDisplayStatus = $('#promo-display-status')[0];
                if (promoDataStr == '') {
                    promoDisplayStatus.innerHTML = '';
                    return;
                }

                let promoData = JSON.parse(promoDataStr);

                if(promoData.action) {
                    switch (promoData.action) {
                        case 'ADD':
                            CFPromotions.addNew(promoData.code, promoData.repeatDaily);
                            promoCodeElement.innerText = '';
                            $('#promo-text-input').val('');
                            promoDisplayStatus.innerHTML = 'Code ' + promoData.code + ' added!';
                            ui.log(`Promo code ${promoData.code} added`);
                            ui.refresh(null, CFPromotions.getAll());
                            break;
                        case 'REMOVEALLPROMOS':
                            CFPromotions.removeAll();
                            promoCodeElement.innerText = '';
                            promoDisplayStatus.innerHTML = 'Promo codes removed!';
                            ui.log(`Promo codes removed`);
                            ui.refresh(null, CFPromotions.getAll());
                            break;
                        case 'REMOVE':
                            if(CFPromotions.remove(promoData.id, promoData.code) != -1) {
                                ui.log(`Promo code ${promoData.code} removed`);
                            } else {
                                ui.log(`Unable to remove code ${promoData.code}`);
                            }
                            promoCodeElement.innerText = '';
                            ui.refresh(null, CFPromotions.getAll());
                            break;
                    }
                }
            };

            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.coinRef
                    };});
                items.sort((a, b) => (a.name > b.name) ? 1 : -1);

                return items;
            };
            return{
                init:start,
                process: process,
                resultReader: resultReader,
                getFaucetsForPromotion: getCFlist,
                readPromoCodeValues: readPromoCodeValues
            };
        },
        createUi: function() {
            let logLines = ['', '', '', '', ''];
            function init(cfFaucets) {
                appendCSS();
                appendJavaScript();
                appendHtml();
                createPromoTable(cfFaucets);
                document.querySelector('.page-title h1').innerHTML = 'Auto Claim';
            };
            function appendCSS() {
                let css = '<style>';
                css += 'td.em-input {';
                css += 'padding-top: 0;';
                css += 'padding-bottom: 0;';
                css += '}';
                css += 'td.em-input input {';
                css += '}';
                css += '</style>';
                $('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 += 'var promoDaily = document.getElementById("promo-daily");';
                js += 'var promoObject = { action: "ADD", code: promoText.value.trim(), repeatDaily: promoDaily.checked };'
                js += 'promoCode.innerHTML =JSON.stringify(promoObject);';
                js += 'promoDisplayStatus.innerHTML = "Adding &nbsp&quot;<b>" + promoObject.code + "</b>&quot;...<br>This could take around a minute. Please wait..."';
                js += '}';
                js += 'function removePromoCode(id, code) {';
                js += 'var promoCode = document.getElementById("promo-code-new");';
                js += 'var promoDisplayStatus = document.getElementById("promo-display-status");';
                js += 'var promoObject = { action: "REMOVE", id: id, code: code };'
                js += 'promoCode.innerHTML =JSON.stringify(promoObject);';
                js += 'promoDisplayStatus.innerHTML = "Removing code &nbsp&quot;<b>" + code + "</b>&quot;...<br>This could take around a minute. Please wait..."';
                js += '}';


                js += 'function getUpdateObject() {';
                js += 'let updateObject;';
                js += 'var updateData = document.getElementById("update-data");';
                js += 'if (updateData.innerHTML != "") {';
                js += 'updateObject = JSON.parse(updateData.innerHTML);';
                js += '} else {';
                js += 'updateObject = { runAsap: { ids: [], changed: false}, editSingle: { changed: false, items: [] } };';
                js += '}';
                js += 'return updateObject;';
                js += '}';
                js += 'function editList() {';
                js += '$.each($("#schedule-table-body td.em-input"), function(x, y) {';
                js += 'let val = y.innerHTML;';
                js += 'y.innerHTML = "<input type=\'text\' data-original=\'" + val + "\' value=\'" + val + "\' />";});';
                js += '$.each($("#schedule-table-body td.edit-status"), function(x, y) {';
                js += 'let activeSwitch = y.querySelector("input");';
                js += 'y.classList.remove("d-none");';
                js += '});';
                js += '$.each($(".em-only"), (x, y) => y.classList.remove("d-none"));';
                js += '$.each($(".em-hide"), (x, y) => y.classList.add("d-none"));';
                js += '}';
                js += 'function editListSave() {';
                js += '  let updateObject = getUpdateObject();';
                js += '  $.each($("#schedule-table-body tr"), function(x, row) {';
                js += '    let textInputCell = row.querySelector(".em-input");';
                js += '    let textInput = textInputCell.querySelector("input");';
                js += '    let activeSwitch = row.querySelector("td.edit-status input");';
                js += '    let single = { id: row.dataset.id, displayName: textInput.dataset.original, enabled: activeSwitch.dataset.original };';
                js += '    textInputCell.innerHTML = textInput.value;';
                js += '    if(textInput.dataset.original != textInput.value) {';
                js += '      single.displayName = textInput.value;';
                js += '    }';
                js += '    if(activeSwitch.dataset.original != Boolean(activeSwitch.checked)) {';
                js += '      single.enabled = Boolean(activeSwitch.checked);';
                js += '    }';
                js += '    if(textInput.dataset.original != textInput.value || activeSwitch.dataset.original != Boolean(activeSwitch.checked)) {';
                js += '      updateObject.editSingle.items.push(single);';
                js += '      updateObject.editSingle.changed = true;';
                js += '    }';
                js += '  });';
                js += '  if(updateObject.editSingle.changed) {';
                js += '    document.getElementById("update-data").innerHTML = JSON.stringify(updateObject);';
                js += '    document.getElementById("faucets-display-status").innerHTML = "Data will be updated as soon as possible...";';
                js += '  }';
                js += '$.each($(".em-only"), (x, y) => y.classList.add("d-none"));';
                js += '$.each($(".em-hide"), (x, y) => y.classList.remove("d-none"));';
                js += '}';
                js += 'function editListCancel() {';
                js += '$.each($("#schedule-table-body td.em-input input"), function(x, y) {';
                js += 'let val = y.dataset.original;';
                js += 'y.parentNode.innerHTML = val;});';
                js += '$.each($(".em-only"), (x, y) => y.classList.add("d-none"));';
                js += '$.each($(".em-hide"), (x, y) => y.classList.remove("d-none"));';
                js += '}';


                js += 'function updateValues(type, values) {';
                js += 'let updateObject = getUpdateObject();';
                js += 'if (type == "runAsap") {';
                js += 'updateObject.runAsap.ids.push(values.id);';
                js += 'updateObject.runAsap.changed = true;';
                js += 'document.getElementById("update-data").innerHTML = JSON.stringify(updateObject);';
                js += 'document.getElementById("faucets-display-status").innerHTML = "Faucet will be updated to run as soon as possible...";';
                js += '}';
                js += '}';


                js += 'function removeAllPromos() {';
                js += 'var promoCode = document.getElementById("promo-code-new");';
                js += 'var promoDisplayStatus = document.getElementById("promo-display-status");';
                js += 'var promoObject = {  action: "REMOVEALL" };'
                js += 'promoCode.innerHTML =JSON.stringify(promoObject);';
                js += 'promoDisplayStatus.innerHTML = "Removing all promotion codes...<br>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="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"><span id="faucets-display-status" class="text-white"></span></div><div class="title col-3 text-right">';
                html += '<div class="em-only d-none"><a class="btn m-2 anchor btn-outline-success align-middle" onclick="editListSave()"><i class="fa fa-check-circle"></i> Save</a>';
                html += '<a class="btn m-2 anchor btn-outline-danger align-middle" onclick="editListCancel()"><i class="fa fa-times-circle"></i> Cancel</a></div>';
                html += '<div class="em-hide"><a class="btn m-2 anchor btn-outline-primary align-middle" onclick="editList()"><i class="fa fa-edit"></i> Edit</a></div>';
                html += '</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>';
                html += '<span id="update-data" style="display:none;"></span></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 mb-1">';
                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"><div class="mx-2"><label class="switch" title="Check if the code can be reused every 24hs"><input id="promo-daily" type="checkbox"><span class="slider round"></span>Daily</label></div><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>';
                html +='<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" style="display:none;"></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>';
                html += '<section class="fragment"><div class="container-fluid bg-dark ">';
                html += '<div class="row justify-content-center"><a class="btn  m-2 anchor btn-outline-primary" id="stats-button" onclick="openStatsChart()">CF Lucky Number Stats</a></div>';
                html +='<div class="container py-1" id="stats-fragment" style="display:none;"><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>';

                $('#referral-table').before(html);
                $('#schedule-container').append( createScheduleTable() );
            };
            function createPromoTable(faucets) {
                let tableStructure = '';
                tableStructure += '<table class="table table-striped table-dark" 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-striped table-dark" id="schedule-table"><thead><tr>';
                tableStructure += '<th scope="col" class="edit-status d-none em-only" style="">Active</th><th class="">#</th><th class="">Name</th><th class="">Site</th><th class="">Last Claim</th>';
                tableStructure += '<th class="">Aggregate</th><th class="">Balance</th><th class="" id="converted-balance-col">FIAT</th><th class="">Next Roll</th>';
                tableStructure += '<th scope="col" class="">Msgs</th><th scope="col" class="em-hide" style="">Run ASAP</th></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 + '" data-cmc="' + data.find(x => x.id == data[i].id).cmc + '" data-balance="' + (data[i].balance ? data[i].balance.split(' ')[0] : "") + '">';
                    tableBody +='<td class="align-middle edit-status d-none em-only"><label class="switch"><input type="checkbox" data-original="' + (data[i].enabled ? '1' : '0') + '" ' + (data[i].enabled ? 'checked' : ' ') + '><span class="slider round"></span></label></td>';
                    tableBody +='<td class="align-middle">' + (data[i].enabled ? (i + 1).toString() : '-') + '</td>';
                    tableBody +='<td class="align-middle em-input" data-field="displayName">' + data[i].name + '</td>';
                    tableBody +='<td class="align-middle text-left" style="width: 20%">';
                    tableBody +='<a class="edit-site" title="Visit site" target="_blank" href="' + (new URL(data[i].rf, data[i].url.origin)).href + '"><i class="fa fa-external-link-alt"></i></a> ';
                    tableBody +=data[i].url.host + '</td>';
                    tableBody +='<td class="align-middle">' + data[i].lastClaim.toFixed(8) + '</td>';
                    tableBody +='<td class="align-middle">' + data[i].aggregate.toFixed(8) + '</td>';
                    tableBody +='<td class="align-middle">' + (data[i].balance ? data[i].balance.split(' ')[0] : "") + '</td>';
                    tableBody +='<td class="align-middle fiat-conversion"></td>';
                    tableBody +='<td class="align-middle">' + helpers.getPrintableTime(data[i].nextRoll) + '</td>';
                    tableBody +='<td class="align-middle">' + addBadges(data[i].stats) + '</td>';
                    tableBody +='<td class="align-middle em-hide"><a class="edit-site" data-toggle="tooltip" data-placement="left" title="Run ASAP" href="javascript:updateValues(\'runAsap\', { id: ' + data[i].id + ' })" onclick=""><i class="fa fa-redo"></i></a></td>';
                    tableBody +='</tr>';
                }

                $('#schedule-table-body').html(tableBody);

                location.href = 'javascript:convertToFiat();';
            };

            function addBadges(stats) {
                let consecutiveTimeout = stats.countTimeouts;
                let otherErrors = stats.errors;
                let html = ' ';

                if (consecutiveTimeout) {
                    html += `<span class="badge badge-pill badge-warning" title="${consecutiveTimeout} consecutive timeouts">${consecutiveTimeout}</span>`;
                }

                if (otherErrors) {
                    html += `<span class="badge badge-pill badge-warning" title="${otherErrors.errorMessage}">${otherErrors.errorType}</span>`;
                }
                return html;
            }
            function loadPromotionTable(codes) {
                let tableBody = '';

                for(let c=0; c < codes.length; c++) {
                    let data = codes[c];
                    tableBody += '<tr data-promotion-id="' + data.id + '">';
                    tableBody += '<td class="align-middle text-left ' + (data.repeatDaily ? 'text-warning' : '') + '">';
                    tableBody += '<a data-toggle="tooltip" data-placement="left" title="Remove" href="javascript:removePromoCode(' + data.id + ', \'' + data.code + '\')" onclick=""><i class="fa fa-times-circle"></i></a> ';
                    tableBody += '<span  title="' + (data.repeatDaily ? 'Reusable Code' : 'One-time-only Code') + '">' + data.code + '</span></td>';
                    tableBody +='<td class="align-middle" title="' + (data.repeatDaily ? 'Reusable Code' : 'One-time-only Code') + '">' + helpers.getPrintableDateTime(data.added) + '</td>';

                    for(let i=0, all = data.statusPerFaucet.length; i < all; i++) {
                        tableBody +='<td class="align-middle" title="Runned @' + helpers.getPrintableDateTime(data.statusPerFaucet[i].execTimeStamp) + '">' + 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, elapsed = false) {
                if(msg) {
                    let previous = logLines[0].split('&nbsp')[1];
                    if(elapsed && (previous == msg)) {
                        logLines[0] = helpers.getPrintableTime() + '&nbsp' + msg + '&nbsp[Elapsed time:&nbsp' + elapsed + '&nbspseconds]';
                    } else {
                        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, repeatDaily = false) {
                this.id = id;
                this.code = code;
                this.added = new Date();
                this.statusPerFaucet = [];
                this.repeatDaily = repeatDaily;
                this.lastExecTimeStamp = null;
            };

            function getFaucetStatusInPromo(promo, faucetId) {
                let faucet = promo.statusPerFaucet.find(x => x.id == faucetId);
                if (faucet.status && promo.repeatDaily) {
                    //Using 26 hs instead of 24hs, and 2hs gap retry when code is flagged as USEDBEFORE
                    if((faucet.status == PromoStatus.ACCEPTED && (Date.now() - faucet.execTimeStamp.getTime()) > HS_26_IN_MILLISECONDS)
                      || (faucet.status == PromoStatus.USEDBEFORE && (Date.now() - faucet.execTimeStamp.getTime()) > HS_2_IN_MILLISECONDS)) {
                        faucet.status = PromoStatus.PENDING;
                    }
                }
                return faucet.status ?? PromoStatus.NOCODE;
            };

            function addNew(code, repeatDaily = false) {
                let newPromo = new PromotionCode(codes.length, code, repeatDaily);
                newPromo.statusPerFaucet = manager.getFaucetsForPromotion().map(x => {
                    return {
                        id: x.id,
                    };});
                newPromo.statusPerFaucet.forEach(function (element, idx, arr) {
                    arr[idx].status = PromoStatus.PENDING;
                    arr[idx].execTimeStamp = null;
                });

                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;
                    faucet.execTimeStamp = new Date();
                    promo.lastExecTimeStamp = faucet.execTimeStamp;
                }
                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();
            };

            function remove(id, code) {
                let idx = codes.findIndex(x => x.id == id && x.code == code);
                if(idx != -1) {
                    codes.splice(idx, 1);
                    save();
                }

                return idx;
            };

            return {
                addNew: addNew,
                removeAll: removeAll,
                remove: remove,
                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 (hasPopup()) {
                        closePopup();
                        setTimeout(SGProcessor.run, helpers.randomMs(5000, 10000));
                } else {
                    if(isMinerActive()) {
                        processRunDetails();
                    } else {
                        activateMiner();
                    }
                }
            };
            function hasPopup() {
                return $('.wrapper.grid.min-h-0.md-min-h-1-2.md-relative.md-rounded-lg.md-bg-dark-4 svg circle').length > 0;
            };
            function closePopup() {
                try {
                    $('svg.flex.w-8.h-8.fill-current')[0].parentElement.click();
                } catch { }
            };
            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 = 15;
                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,
                LOGIN: 3
            };
            let navigationProcess;
            let countdown;
            let rollButton;
            let promotionTag;
            let timeWaiting= 0;
            let loopingForErrors = false;

            function run() {
                navigationProcess = NavigationProcess.ROLLING;
                displayStatusUi();

                setTimeout(findCountdownOrRollButton, helpers.randomMs(2000, 5000));
            };

            function doLogin() {
                navigationProcess = NavigationProcess.LOGIN;
                displayStatusUi();

                setTimeout(findLoginForm, helpers.randomMs(2000, 5000));
            };

            function isFullyLoaded() { //Waits 55 seconds max
                if(document.readyState == 'complete' || timeWaiting == -1) {
                    $('#process-status')[0].innerHTML = 'Interacting';
                    timeWaiting = 0;
                    interact();
                } else {
                    timeWaiting = -1;
                    $('#process-status')[0].innerHTML = 'Waiting for document fully loaded';
                    setTimeout(CFProcessor.isFullyLoaded, helpers.randomMs(45000, 55000));
                }
            };
            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;
                    setTimeout(CFProcessor.isFullyLoaded, helpers.randomMs(1000, 5000));
                } else {
                    if (timeWaiting/1000 > config.defaults.timeout * 60) {
                        shared.closeWithError('TIMEOUT', '');
                        return;
                    }

                    timeWaiting += 3000;
                    setTimeout(findCountdownOrRollButton, helpers.randomMs(2000, 5000));
                }
            };
            function findLoginForm() {
                if ( $('div.login-wrapper').is(':visible') ) {
                    //Other possible error is if recaptcha did not load yet... so maybe wait til the web is fully loaded for low connection issues
                    if( $('.login-wrapper .error').length > 0 && $('.login-wrapper .error')[0].innerHTML != '') {
                        let errorMessage = $('.login-wrapper .error').text();
                        shared.closeWithError('LOGIN_ERROR', errorMessage);
                        return;
                    }
                    if(!loopingForErrors) {
                        if(config.cf.credentials.mode == 1) {
                            timeWaiting = 0;
                            $('.login-wrapper input[name="email"]').val(config.cf.credentials.email);
                            $('.login-wrapper input[name="password"]').val(config.cf.credentials.password);
                            $('.login-wrapper button.login').click();
                            loopingForErrors = true;
                        } else {
                            if($('.login-wrapper input[name="email"]').val() != '' && $('.login-wrapper input[name="password"]').val() != '') {
                                $('.login-wrapper button.login').click();
                                $('#process-status')[0].innerHTML = 'Processing';
                                loopingForErrors = true;
                            } else {
                                $('#process-status')[0].innerHTML = 'Waiting for credentials...';
                                if (timeWaiting/1000 > (config.defaults.timeout / 1.5) * 60) {
                                    shared.closeWithError('LOGIN_ERROR', 'No credentials were provided');
                                    return;
                                }
                            }
                        }
                    }
                }

                if (timeWaiting/1000 > config.defaults.timeout * 60) {
                    shared.closeWithError('TIMEOUT', '');
                    return;
                }

                timeWaiting += 3000;
                setTimeout(findLoginForm, 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() {
                $('#process-status')[0].innerHTML = 'Roll triggered';
                $(rollButton[0]).click();
                setTimeout(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(-20), "0");
                    }
                }
                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.promoCodeExpired) > 0) {
                        return PromoStatus.EXPIRED;
                    } else if(localeConfig.stringSearches.promoCodeInvalid.findIndex(x => promotionTag.innerHTML.indexOf(x) > -1) == -1) {
                        return PromoStatus.INVALID;
                    }
                } catch ( err ) { }
                return promoStatus;
            };
            function validatePromoString() {

            };
            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="#">⚙️ <span id="process-status">Processing</span></div>' );
            };
            return {
                run: run,
                runPromotion: runPromotion,
                findPromotionTag: findPromotionTag,
                waitInteractions: waitInteractions,
                isFullyLoaded: isFullyLoaded,
                doLogin: doLogin
            };
        },
        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
            }
        },
        createFBProcessor: function() {
            let timeWaiting= 0;
            let countdownMinutes;
            function run() {
                setTimeout(findCountdownOrRollButton, helpers.randomMs(12000, 15000));
            };
            function findCountdownOrRollButton() {
                if ( isCountdownVisible() ) {
                    timeWaiting = 0;
                    countdownMinutes = +document.querySelectorAll('.free_play_time_remaining.hasCountdown .countdown_amount')[0].innerHTML + 1;
                    let result = {};
                    result.balance = readBalance();
                    result.nextRoll = helpers.addMinutes(new Date(), countdownMinutes.toString());

                    shared.closeWindow(result);
                    return;
                }

                if ( isRollButtonVisible() ) {
                    if (config.fb.activateRPBonus) {
                        if (!document.getElementById('bonus_container_free_points')) {
                            document.querySelector('a.rewards_link').click();
                            activateBonus(0);
                        }
                    }
                    if (isHCaptchaVisible()) {
                        waitForCaptcha();
                    } else {
                        clickRoll();
                    }
                }
            };
            function isCountdownVisible() {
                return document.querySelectorAll('.free_play_time_remaining.hasCountdown .countdown_amount').length > 0;
            };
            function isHCaptchaVisible() {
                let hCaptchaFrame = document.querySelector('.h-captcha > iframe');
                if (hCaptchaFrame && $(hCaptchaFrame).is(':visible')) {
                    return true;
                }
                return false;
            };
            function isRollButtonVisible() {
                return $(document.getElementById('free_play_form_button')).is(':visible');
            };
            function waitForCaptcha() {
                if ( document.querySelector('.h-captcha > iframe').getAttribute('data-hcaptcha-response').length > 0) {
                    clickRoll();
                } else {
                    if (timeWaiting/1000 > config.defaults.timeout * 60) {
                        shared.closeWithError('TIMEOUT', '');
                        return;
                    }

                    timeWaiting += 10000;
                    setTimeout(waitForCaptcha, helpers.randomMs(10000, 12000));
                }
            };
            function clickRoll() {
                try {
                    document.getElementById('free_play_form_button').click();
                    setTimeout(processRunDetails, helpers.randomMs(3000, 10000));
                } catch (err) {
                    shared.closeWithError('CLICK_ROLL_ERROR', err);
                }
            };
            function processRunDetails() {
                if ($(document.getElementById('winnings')).is(':visible')) {
                    closePopup();

                    let result = {};
                    result.claimed = readClaimed();
                    result.balance = readBalance();
                    if(result.claimed != 0) {
                        result.rolledNumber = readRolledNumber();
                        result.nextRoll = helpers.addMinutes(new Date(), "60");
                    }
                    shared.closeWindow(result);
                    return;
                }

                if ($('.free_play_result_error').is(':visible')) {
                    shared.closeWithError('ROLL_ERROR', $('.free_play_result_error')[0].innerHTML);
                    return;
                }

                if($('#free_play_error').is(':visible')) {
                    shared.closeWithError('ROLL_ERROR', $('.free_play_error')[0].innerHTML);
                    return;
                }

                if ($(document.getElementById('same_ip_error')).is(':visible')) {
                    shared.closeWithError('ROLL_ERROR', document.getElementById('same_ip_error').innerHTML);
                    return;
                }

                if (timeWaiting/1000 > config.defaults.timeout * 60) {
                    shared.closeWithError('TIMEOUT', '');
                    return;
                }

                timeWaiting += 5000;
                setTimeout(processRunDetails, helpers.randomMs(5000, 6000));
            };
            function closePopup() {
                let closePopupBtn = document.querySelector('.reveal-modal.open .close-reveal-modal');
                if (closePopupBtn) {
                    closePopupBtn.click();
                }
            };
            function readRolledNumber() {
                let rolled = 0;
                try {
                    rolled = parseInt([... document.querySelectorAll('#free_play_digits span')].map( x => x.innerHTML).join(''));
                } catch { }
                return rolled;
            };
            function readBalance() {
                let balance = 0;
                try {
                    balance = document.getElementById('balance').innerHTML;
                } catch { }
                return balance;
            };
            function readClaimed() {
                let claimed = 0;
                try {
                    claimed = document.getElementById('winnings').innerHTML;
                } catch { }
                return claimed;
            };

            function activateBonus(i) {
                if($(document.querySelector('#reward_point_redeem_result_container_div .reward_point_redeem_result_error')).is(':visible')) {
                    let closeBtn = document.querySelector('#reward_point_redeem_result_container_div .reward_point_redeem_result_box_close')
                    if ($(closeBtn).is(':visible')) {
                        closeBtn.click();
                    }
                } else if ($(document.querySelector('#reward_point_redeem_result_container_div .reward_point_redeem_result_success')).is(':visible')) {
                    let closeBtn = document.querySelector('#reward_point_redeem_result_container_div .reward_point_redeem_result_box_close')
                    if ($(closeBtn).is(':visible')) {
                        closeBtn.click();
                        document.querySelector('#free_play_link_li a');
                        setTimeout(findCountdownOrRollButton, helpers.randomMs(10000, 12000));
                        return;
                    }
                }

                try {
                    let redeemButtons = document.querySelectorAll('#free_points_rewards button');
                    redeemButtons[i].click();
                    i = i + 1;
                } catch (err) {
                }

                if(i > 4) {
                    document.querySelector('#free_play_link_li a');
                    setTimeout(findCountdownOrRollButton, helpers.randomMs(10000, 12000));
                    return;
                }
                setTimeout(activateBonus.bind(null, i), 5000);
            };
            return {
                run: run
            };
        },
        createFPProcessor: function() {
            let timeWaiting= 0;
            function timedOut(addMs) {
                if (timeWaiting/1000 > config.defaults.timeout * 60) {
                    shared.closeWithError('TIMEOUT', '');
                    return true;
                }

                timeWaiting += addMs;
                return false;
            }

            function ptcList() {
                let result;
                let runMsgDiv = document.querySelector('.alert.alert-info');
                if (runMsgDiv) {
                    let runMsg = runMsgDiv.innerHTML;
                    if (runMsg.includes('invalid captcha')) {
                        // Warn? Usually a error if ptcList is refreshed
                    } else if (runMsg.includes('Good job')) {
                        // "Good job! You have been credited with 0.00000001 BTC."
                        try {
                            let idx = runMsg.search(/\d/);
                            let claimed = parseFloat(runMsg.slice(idx, idx + 10));
                            result = shared.getResult();
                            result.claimed = (result.claimed ?? 0) + claimed;
                            result.nextRoll = helpers.addMilliseconds(new Date(), helpers.getRandomMillisecondsFromMinutesRange(config.fp.hoursBetweenRuns * 60, 2)); // Wait hoursBetweenRuns +/- 1%
                            shared.updateWithoutClosing(result);
                        } catch (err) {
                            // GM_log(`Error reading claimed amount: ${err}`);
                        }
                    }
                }

                if ($('b:contains("Whoops!")').length) {
                    result = shared.getResult();
                    result.nextRoll = helpers.addMilliseconds(new Date(), helpers.getRandomMillisecondsFromMinutesRange(config.fp.hoursBetweenRuns * 60, 2)); // Wait hoursBetweenRuns +/- 2%
                    shared.closeWindow(result);
                    return;
                }

                let adButtons = $('button').filter(function(idx) {
                    return this.innerHTML.includes('VISIT AD FOR') > 0;
                });

                if (adButtons.length > 0) {
                    adButtons[0].click();
                    return;
                }

                if (timedOut(10000)) {
                    return;
                }
                setTimeout(ptcList, helpers.randomMs(10000, 12000));
            }

            function ptcSingle() {
                if($('input[name="complete"]').is(':visible')) {
                    setTimeout(waitForCaptcha, 15000);
                } else {
                    if (timedOut(5000)) {
                        return;
                    }
                    setTimeout(ptcSingle, helpers.randomMs(5000, 6000));
                }
            }

            function waitForCaptcha() {
                if ( document.querySelector('.h-captcha > iframe').getAttribute('data-hcaptcha-response').length > 0 ) {
                    clickClaim();
                } else {
                    if (timedOut(9000)) {
                        return;
                    }
                    setTimeout(waitForCaptcha, helpers.randomMs(9000, 11000));
                }
            }

            function clickClaim() {
                $('input[name="complete"]').focus();
                $($('input[name="complete"]')[0]).attr("onclick", "");
                $('input[name="complete"]').click();
                //force close with timeout in case it's still opened
                setTimeout(shared.closeWithError.bind('TIMEOUT', 'Timed out after clicking a CLAIM button.'), config.defaults.timeout * 60 * 1000);
            }

            return {
                ptcList: ptcList,
                ptcSingle: ptcSingle
            };
        }
    };

    /**
    * 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.host)) {
            return;
        }

        let currentFromManager = shared.getCurrent();

        if (currentFromManager.type == WebType.STORMGAIN) {
            SGProcessor = objectGenerator.createSGProcessor();
            setTimeout(SGProcessor.run, helpers.randomMs(10000, 20000));
            return;
        }

        if (currentFromManager.type == WebType.CRYPTOSFAUCETS) {
            let expectedCfUrlType = helpers.cf.getUrlType(currentFromManager.url);
            let realCfUrlType = helpers.cf.getUrlType(window.location.href);

            switch(expectedCfUrlType) {
                case CFUrlType.FREE:
                    switch(realCfUrlType) {
                        case CFUrlType.FREE:
                            if(localeConfig.setToEnglish) {
                                let refValue = $('.nav-item a')[4].innerHTML;
                                if (refValue != 'Settings') {
                                    window.location.href = '/set-language/en';
                                }
                            }
                            addJS_Node (null, null, overrideSelectNativeJS_Functions);
                            CFProcessor = objectGenerator.createCFProcessor();
                            interactions = objectGenerator.createInteractions();
                            setTimeout(CFProcessor.run, helpers.randomMs(1000, 3000));
                            break;
                        case CFUrlType.CONTACTTWITTER:
                            //TODO: mark as possibly banned
                            break;
                        case CFUrlType.HOME:
                            if (config.cf.autologin) {
                                addJS_Node (null, null, overrideSelectNativeJS_Functions);
                                CFProcessor = objectGenerator.createCFProcessor();
                                setTimeout(CFProcessor.doLogin, helpers.randomMs(1000, 3000));
                            } else {
                                shared.closeWithError('NEED_TO_LOGIN', '');
                            }
                            break;
                        default:
                            break;
                    }
                    break;
                case CFUrlType.PROMOTION:
                    CFProcessor = objectGenerator.createCFProcessor();
                    interactions = objectGenerator.createInteractions();
                    setTimeout(CFProcessor.runPromotion, helpers.randomMs(5000, 10000));
                    break;
            }

            return;
        }

        if (currentFromManager.type == WebType.FREEBITCOIN) {
            FBProcessor = objectGenerator.createFBProcessor();
            setTimeout(FBProcessor.run, helpers.randomMs(2000, 5000));
            return;
        }

        if (currentFromManager.type == WebType.FAUCETPAY) {
            FPProcessor = objectGenerator.createFPProcessor();
            let url = new URL(window.location.href);

            if(url.pathname.includes('ptc/view')) {
                setTimeout(FPProcessor.ptcSingle, helpers.randomMs(2000, 5000));
//                setTimeout(isClaimVisible, 5000);
//                setTimeout(ptcHasFocus, 15000);
            } else if (url.pathname.includes('ptc')) {
                setTimeout(FPProcessor.ptcList, helpers.randomMs(2000, 5000));
//            } else if (url.pathname.includes('page/user-admin')) {
//                // Try to claim Reward Points
            } else if (url.pathname.includes('account/login')) {
                shared.closeWithError('NEED_TO_LOGIN', '');
            }
            return;
        }
    }

    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();
        } else {
            detectWeb();
        }
    }
    init();
})();