[satology] Auto Claim Multiple Faucets with Monitor UI

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

当前为 2021-07-11 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         [satology] Auto Claim Multiple Faucets with Monitor UI
// @description  Freebitco.in, StormGain Miner, 15 Faucets with Promo Codes + Autologin, FaucetPay PTC, etc.
// @description  Automatic activation of StormGain Miner every 4 hours and hourly claim of free ADA, BNB, BCH, BTC, DASH, DGB, DOGE, ETH, LINK, LTC, NEO, STEAM, TRX, USDC, USDT, XEM, XRP, ZEC
// @version      1.5.5
// @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                                                                              AND
// @note         to roll Free-ethereum.io you need to add the following two lines to hCaptcha Solver script, right at the beginning of function getSynonyms(word) [line 232 on version 3.5]:

// @note         const WORDS_WITH_CYRILLIC = {'аirplane': AIRPLANE, 'bіcycle': BICYCLE, 'bοat': BOAT, 'сar': CAR, 'mοtorbus': MOTORBUS, 'mοtorcycle': MOTORCYCLE, 'trаin': TRAIN, 'truсk': TRUCK };
// @note         word = (encodeURIComponent(word).indexOf('%') > -1) ? WORDS_WITH_CYRILLIC[word] : word;
// @note         -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

// @note         - LAST UPDATES ----------------------------------------------------------------------------------------------------------------------------------------------------------------
// @note         [@1.5.5]  BUG Fixed: Timeout wasn't triggering if Keran/Bagi withdraw failed due to an Ad redirect.
// @note         [@1.5.1]  BUG Fixed: sometimes new faucets weren't claiming due to URL error.
// @note         [@1.5 BETA RELEASE]  Added Wallet to fill with your FaucetPay addresses [from the UI], promo codes suggestions [reusables] and Bagi & Keran faucets
// @note                              Please don't activate ALL of the bagi/keran faucets. Consider the time it takes to roll them. This is a beta release as some modifications
// @note                              will be added: needs refactor, might add priority to make sure certain faucets are rolled when available, etc.
// @note                              Withdraw options: never, after each claim or every X hours (default). Can be configured @config var
// @note                              Aggregate holds the sats you have at Keran/Bagi. Balance will accumulate the amount you withdraw to Faucet Pay.
// @note                              You will see badges (timeouts/errors). Please don't report them unless they are labeled as 'ERROR'.
// @note                              Faucets have referral links which are randomize. It has my ref links and of some users who have contributed with the
// @note                              coding (thanks nat & karl)
// @note         [@1.4]  Added Free-Litecoin.com & Free-Ethereum.io
// @note         [@1.3]  Added FaucetPay PTC. It runs on focus every 5 hs aprox (set by hoursBetweenRuns in config)
// @note         [@1.2]  UI Changes
// @note         [@1.1]  Added Freebitco.in and autologin for @cryptosfaucets
// @note         ----------------------------------------------------------------------------------------------------------------------------------------------------------------

// @note         - MAIN FEATURES ------------------------------------------------------------------------------------------------------------------------------------------------
// @note         > Automatic rolls and claims for faucets/PTCs/miners
// @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. CF 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         ----------------------------------------------------------------------------------------------------------------------------------------------------------------
// @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*
// @match        https://faucetpay.io/account/login
// @match        https://free-litecoin.com/*
// @match        https://www.free-ethereum.io/
// @match        https://www.free-ethereum.io/free/
// @match        https://bagi.co.in/*
// @match        https://keran.co/*
// ==/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,
        FREELITECOIN: 5,
        FREEETHEREUMIO: 6,
        BAGIKERAN: 7
    };
    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
    const HS_2_IN_MILLISECONDS = 7200000;   //and 2hs gap retry when code is flagged as USEDBEFORE
    const USE_DEFAULT = -1;
    const CFReusableCodesSuggestion = ['55khv20st4', '90nq6mcmz2', 'lytovoap04', 'vmuph8j0c6', 'ykxlvmg9ja', 'd8fmqxjlma', 'rjnmzjs673'];
    const WalletType = {
        FP_MAIL: 100,
        FP_BTC: 101,
        FP_BNB: 102,
        FP_BCH: 103,
        FP_DASH: 104,
        FP_DGB: 105,
        FP_DOGE: 106,
        FP_ETH: 107,
        FP_FEY: 108,
        FP_LTC: 109,
        FP_TRX: 110,
        FP_USDT: 111,
        FP_ZEC: 112
    };

    //TODO: add to UI
    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
            maxTimeInMinutes: 10, // max time it will stay working on each run
        },
        bagikeran: { // Bagi.co.in & keran.co
            withdrawMode: 1, // POSSIBLE VALUES:
                             // 0: Disables auto withdraw
                             // 1: Withdraws once every 'hoursBetweenWithdraws' hours
                             // 2: Withdraw after each successful claim
            hoursBetweenWithdraws: 6
        }
    }

//    let persistence, shared, manager, ui, CFPromotions, interactions, SGProcessor, CFProcessor, CFHistory, FBProcessor, FPProcessor, FreeLitecoinProcessor, FreeGenericFaucetProcessor;
    let persistence, shared, manager, ui, CFPromotions, interactions, CFHistory, SiteProcessor;

    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);
        },
        hsToMs: function(hours) {
            return hours * 60 * 60 * 1000;
        },
        minToMs: function(min) {
            return min * 60 * 1000;
        },
        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 devlog(value) {
                let log;
                if(value) {
                    log = persistence.load('devlog', true);
                    log = log ?? [];
                    log.push(value);
                } else {
                    log = [];
                }
                persistence.save('devlog', log, true);
            };
            function isOpenedByManager(currentHost) {
                loadFlowControl();
                if(!flowControl) {
                    return false;
                }
                let millisecondsDistance = new Date() - flowControl.requestedTime;

                if((flowControl.type != WebType.FAUCETPAY && flowControl.type != WebType.BAGIKERAN && flowControl.opened) || (flowControl.host != currentHost && !flowControl.expectedHosts.includes(currentHost)) || millisecondsDistance > 120000) {
                    return false;
                }
                return true;
            };
            function setFlowControl(id, url, webType, expectedHosts, params = null) {
                flowControl = {
                    id: id,
                    url: url,
                    host: url.host,
                    type: webType,
                    requestedTime: new Date(),
                    opened: false,
                    expectedHosts: expectedHosts,
                    error: false,
                    result: {}
                };
                if(params) {
                    flowControl.params = params;
                }
                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;
                if(flowControl.params) {
                    current.params = flowControl.params;
                }

                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;

                persistence.save('flowControl', flowControl, true);
                window.close();
            };
            function clearFlowControl() {
                flowControl = {};
                persistence.save('flowControl', flowControl, true);
            };
            return {
                devlog: devlog,
                setFlowControl: setFlowControl,
                wasVisited: wasVisited,
                isOpenedByManager: isOpenedByManager,
                getCurrent: getCurrent,
                getResult: getResult,
                closeWindow: saveAndclose,
                closeWithError: closeWithError,
                updateWithoutClosing: markAsVisited,
                hasErrors: hasErrors,
                clearFlowControl: clearFlowControl
            };
        },
        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 = [];
            let userWallet = [];

            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', 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', url: new URL('https://freebitco.in/'), rf: '?r=41092365', type: WebType.FREEBITCOIN },
                { id: '18', name: 'FaucetPay PTC', cmc: '1', url: new URL('https://faucetpay.io/ptc'), rf: '?r=41092365', type: WebType.FAUCETPAY },
                { id: '19', name: 'Free-Litecoin.com', cmc: '2', url: new URL('https://free-litecoin.com/'), rf: 'login?referer=1332950', type: WebType.FREELITECOIN },
                { id: '20', name: 'Free-Ethereum.io', cmc: '1027', url: new URL('https://www.free-ethereum.io/'), rf: '?referer=1064662', type: WebType.FREEETHEREUMIO },
                { id: '21', name: 'Bagi BTC', cmc: '1', wallet: WalletType.FP_BTC, url: new URL('https://bagi.co.in/bitcoin/'), rf: ['?ref=53706', '?ref=63428', '?ref=54350'], type: WebType.BAGIKERAN },
                { id: '22', name: 'Bagi BNB', cmc: '1839', wallet: WalletType.FP_BNB, url: new URL('https://bagi.co.in/binance/'), rf: ['?ref=12529', '?ref=23852', '?ref=13847'], type: WebType.BAGIKERAN },
                { id: '23', name: 'Bagi BCH', cmc: '1831', wallet: WalletType.FP_BCH, url: new URL('https://bagi.co.in/bitcoincash/'), rf: ['?ref=44242', '?ref=50185', '?ref=41957'], type: WebType.BAGIKERAN },
                { id: '24', name: 'Bagi DASH', cmc: '131', wallet: WalletType.FP_DASH, url: new URL('https://bagi.co.in/dash/'), rf: ['?ref=32724', '?ref=38540', '?ref=40441'], type: WebType.BAGIKERAN },
                { id: '25', name: 'Bagi DGB', cmc: '109', wallet: WalletType.FP_DGB, url: new URL('https://bagi.co.in/digibyte/'), rf: ['?ref=22664', '?ref=27872', '?ref=29669'], type: WebType.BAGIKERAN },
                { id: '26', name: 'Bagi DOGE', cmc: '74', wallet: WalletType.FP_DOGE, url: new URL('https://bagi.co.in/dogecoin/'), rf: ['?ref=45047', '?ref=54217', '?ref=45568'], type: WebType.BAGIKERAN },
                { id: '27', name: 'Bagi ETH', cmc: '1027', wallet: WalletType.FP_ETH, url: new URL('https://bagi.co.in/ethereum/'), rf: ['?ref=24486', '?ref=27799', '?ref=24847'], type: WebType.BAGIKERAN },
                { id: '28', name: 'Bagi FEY', cmc: '10361', wallet: WalletType.FP_FEY, url: new URL('https://bagi.co.in/feyorra/'), rf: ['?ref=5049', '?ref=7433', '?ref=5318'], type: WebType.BAGIKERAN },
                { id: '29', name: 'Bagi LTC', cmc: '2', wallet: WalletType.FP_LTC, url: new URL('https://bagi.co.in/litecoin/'), rf: ['?ref=48335', '?ref=57196', '?ref=48878'], type: WebType.BAGIKERAN },
                { id: '30', name: 'Bagi TRX', cmc: '1958', wallet: WalletType.FP_TRX, url: new URL('https://bagi.co.in/tron/'), rf: ['?ref=22622', '?ref=31272', '?ref=23075'], type: WebType.BAGIKERAN },
                { id: '31', name: 'Bagi USDT', cmc: '825', wallet: WalletType.FP_USDT, url: new URL('https://bagi.co.in/tether/'), rf: ['?ref=25462', '?ref=32491', '?ref=25981'], type: WebType.BAGIKERAN },
                { id: '32', name: 'Bagi ZEC', cmc: '1437', wallet: WalletType.FP_ZEC, url: new URL('https://bagi.co.in/zcash/'), rf: ['?ref=9181', '?ref=15120', '?ref=9878'], type: WebType.BAGIKERAN },
                { id: '33', name: 'Keran BTC', cmc: '1', wallet: WalletType.FP_BTC, url: new URL('https://keran.co/BTC/'), rf: ['?ref=73729', '?ref=92353', '?ref=79321'], type: WebType.BAGIKERAN },
                { id: '34', name: 'Keran BNB', cmc: '1839', wallet: WalletType.FP_BNB, url: new URL('https://keran.co/BNB/'), rf: ['?ref=19287', '?ref=31242', '?ref=20659'], type: WebType.BAGIKERAN },
                { id: '35', name: 'Keran BCH', cmc: '1831', wallet: WalletType.FP_BCH, url: new URL('https://keran.co/BCH/'), rf: ['?ref=58232', '?ref=67326', '?ref=70759'], type: WebType.BAGIKERAN },
                { id: '36', name: 'Keran DASH', cmc: '131', wallet: WalletType.FP_DASH, url: new URL('https://keran.co/DASH/'), rf: ['?ref=45229', '?ref=53041', '?ref=55716'], type: WebType.BAGIKERAN },
                { id: '37', name: 'Keran DGB', cmc: '109', wallet: WalletType.FP_DGB, url: new URL('https://keran.co/DGB/'), rf: ['?ref=32788', '?ref=39527', '?ref=42014'], type: WebType.BAGIKERAN },
                { id: '38', name: 'Keran DOGE', cmc: '74', wallet: WalletType.FP_DOGE, url: new URL('https://keran.co/DOGE/'), rf: ['?ref=73512', '?ref=85779', '?ref=89613'], type: WebType.BAGIKERAN },
                { id: '39', name: 'Keran ETH', cmc: '1027', wallet: WalletType.FP_ETH, url: new URL('https://keran.co/ETH/'), rf: ['?ref=32226', '?ref=36427', '?ref=32676'], type: WebType.BAGIKERAN },
                { id: '40', name: 'Keran FEY', cmc: '10361', wallet: WalletType.FP_FEY, url: new URL('https://keran.co/FEY/'), rf: ['?ref=6269', '?ref=9019', '?ref=6569'], type: WebType.BAGIKERAN },
                { id: '41', name: 'Keran LTC', cmc: '2', wallet: WalletType.FP_LTC, url: new URL('https://keran.co/LTC/'), rf: ['?ref=69102', '?ref=80726', '?ref=84722'], type: WebType.BAGIKERAN },
                { id: '42', name: 'Keran TRX', cmc: '1958', wallet: WalletType.FP_TRX, url: new URL('https://keran.co/TRX/'), rf: ['?ref=49686', '?ref=46544', '?ref=34485'], type: WebType.BAGIKERAN },
                { id: '43', name: 'Keran USDT', cmc: '825', wallet: WalletType.FP_USDT, url: new URL('https://keran.co/USDT/'), rf: ['?ref=40582', '?ref=48907', '?ref=41009'], type: WebType.BAGIKERAN },
                { id: '44', name: 'Keran ZEC', cmc: '1437', wallet: WalletType.FP_ZEC, url: new URL('https://keran.co/ZEC/'), rf: ['?ref=11748', '?ref=18976', '?ref=12487'], type: WebType.BAGIKERAN }
            ];

            const wallet = [
                { id: '101', name: 'FaucetPay BTC (Bitcoin)', type: WalletType.FP_BTC },
                { id: '102', name: 'FaucetPay BNB (Binance Coin)', type: WalletType.FP_BNB },
                { id: '103', name: 'FaucetPay BCH (Bitcoin Cash)', type: WalletType.FP_BCH },
                { id: '104', name: 'FaucetPay DASH (Dash)', type: WalletType.FP_DASH },
                { id: '105', name: 'FaucetPay DGB (DigiByte)', type: WalletType.FP_DGB },
                { id: '106', name: 'FaucetPay DOGE (Dogecoin)', type: WalletType.FP_DOGE },
                { id: '107', name: 'FaucetPay ETH (Ethereum)', type: WalletType.FP_ETH },
                { id: '108', name: 'FaucetPay FEY (Feyorra)', type: WalletType.FP_FEY },
                { id: '109', name: 'FaucetPay LTC (Litecoin)', type: WalletType.FP_LTC },
                { id: '110', name: 'FaucetPay TRX (Tron)', type: WalletType.FP_TRX },
                { id: '111', name: 'FaucetPay USDT (Tether TRC20)', type: WalletType.FP_USDT },
                { id: '112', name: 'FaucetPay ZEC (Zcash)', type: WalletType.FP_ZEC }
            ];

            function start() {
                loader.initialize();
                ui.init(getCFlist());
                update();
                ui.refresh(null, null, userWallet);
                uiUpdatesInterval = setInterval(readUpdateValues, 10000);
                processTimer = setTimeout(manager.process, 2000);
            };
            let loader = function() {
                function initialize() {
                    setTimestamp();
                    initializeWebList();
                    initializeUserWallet();
                    initializePromotions();
                    initializeHistory();
                };
                function initializeWebList() {
                    addSites();
                    addStoredSitesData();
                };
                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);
                        if(arr[idx].type == WebType.BAGIKERAN) {
                            arr[idx].lastWithdraw = new Date();
                        }
                    });
                };
                function addStoredSitesData() {
                    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();
                                }
                                if(element.lastWithdraw) {
                                    webList[idx].lastWithdraw = new Date(element.lastWithdraw);
                                }
                                //webList[idx].focusTab = element.focusTab ?? webList[idx].focusTab;
                            }
                        });
                    } else {
//                        helpers.shuffle(webList);
                    }
                };
                function initializeUserWallet() {
                    addWallets();
                    addStoredWalletData();
                };
                function addWallets() {
                    wallet.forEach(x => userWallet.push(x));
                    userWallet.forEach(function (element, idx, arr) {
                        arr[idx].address = '';
                    });
                };
                function addStoredWalletData() {
                    let storedData = persistence.load('userWallet', true);
                    if(storedData) {
                        storedData.forEach( function (element) {
                            let idx = userWallet.findIndex(x => x.id == element.id);
                            if(idx != -1) {
                                userWallet[idx].address = element.address ?? webList[idx].address;
                            }
                        });
                    }
                };
                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.wallet.changed) {
                            updateObj.wallet.items.forEach(function (element, idx, arr) {
                                try {
                                    let itemIndex = userWallet.findIndex(x => x.id == element.id);
                                    userWallet[itemIndex].address = element.address;

                                    ui.log(`Wallet Address updated [${userWallet[itemIndex].name}]: ${userWallet[itemIndex].address}`);
                                } catch (err) {
                                    ui.log(`Error updating wallet/address: ${err}`);
                                }
                            });
                            $('#faucets-display-status')[0].innerHTML = '';
                            ui.refresh(null, null, userWallet);
                            saveUserWallet();
                        }

                        if(updateObj.runAsap.changed || updateObj.editSingle.changed) {
                            $('#faucets-display-status')[0].innerHTML = '';
                            update(true);
                            process();
                            return;
                        }
                    }
                }
                if(forceCheck) {
                    process();
                }
            };
            function update(sortIt = true) {
                let updateRollStats = webList[0].type == WebType.CRYPTOSFAUCETS;
                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());
                if(updateRollStats) {
                    updateRollStatsSpan();
                }
            };
            function saveWebList() {
                const data = webList.map(function(x) {
                    let ret = {
                        id: x.id,
                        name: x.name,
                        lastClaim: x.lastClaim,
                        aggregate: x.aggregate,
                        balance: x.balance,
                        stats: x.stats,
                        nextRoll: x.nextRoll,
                        enabled: x.enabled
                    };

                    if (x.lastWithdraw) {
                        ret.lastWithdraw = x.lastWithdraw;
                    }

                    return ret;
                });

                persistence.save('webList', data, true);
            }
            function saveUserWallet() {
                const data = userWallet.map(x => {
                    return {
                        id: x.id,
                        address: x.address
                    };});

                persistence.save('userWallet', 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;
				try {
                    if(promoCode) {
                        navUrl = new URL('promotion/' + promoCode, webList[0].url.origin);
                        ui.log(`Opening ${webList[0].name} with Promo Code [${promoCode}]`);
                    }

                    if(Array.isArray(webList[0].rf)) {
                        navUrl = new URL(navUrl.href + webList[0].rf[helpers.randomInt(0, webList[0].rf.length - 1)]);
                    }

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

                    let params = {};
                    if(webList[0].type == WebType.BAGIKERAN) {
                        //TODO: VALIDATE THAT ADDRESS EXISTS AND IS VALID!!!
                        try {
                            params.address = userWallet.find(x => x.type == webList[0].wallet).address;
                        } catch {
                            params.address = '123'; //not available @ wallet
                        }
                        params.doWithdraw = getDoWithdraw(webList[0].lastWithdraw);
                    }

                    shared.setFlowControl(webList[0].id, navUrl, webList[0].type, expectedHosts, params);
                    setTimeout(manager.resultReader, 15000);

                    // Try to close old workingTab if still opened
                    if (workingTab && !workingTab.closed) {
                        try {
                            workingTab.close();
                        } catch { }
                    }

                    workingTab = GM_openInTab(navUrl.href, { active: ((!webList[0].focusTab || webList[0].focusTab == USE_DEFAULT) ? config.defaults.focusTab : webList[0].focusTab) });
                } catch(err) {
                    ui.log(`Error opening tab: ${err}`)
                }
            };

            function getDoWithdraw(lastWithdraw) {
                switch (config.bagikeran.withdrawMode) {
                    case 0:
                        return false;
                        break;
                    case 2:
                        return true;
                        break;
                    case 1:
                        if(lastWithdraw == null) {
                            return true;
                        }
                        return (lastWithdraw && ( (Date.now() - lastWithdraw.getTime()) > helpers.hsToMs(config.bagikeran.hoursBetweenWithdraws)))
                        break;
                    default:
                        return false;
                }
                return false;
            }

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

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

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

                    let result = shared.getResult();

                    if (result) {
                        updateWebListItem(result);

                        if ( (webList[0].type == WebType.CRYPTOSFAUCETS) &&
                            ( (result.claimed) || (result.promoStatus && result.promoStatus != PromoStatus.ACCEPTED) )) {
                                let promoCode = CFPromotions.hasPromoAvailable(webList[0].id);
                                if (promoCode) {
                                    timeWaiting = 0;
                                    update(false);
                                    open(promoCode);
                                    return;
                                }
                        }

                        if ( webList[0].type == WebType.BAGIKERAN && shared.getCurrent().params.doWithdraw && !result.withdrawnAmount) {
                            if(!result.withdrawing) {
                                shared.updateWithoutClosing({ withdrawing: true });
                                update(false);
                                timeWaiting = 0;
                            }

                            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`);
                                moveNext();
                                return;
                            }

                            timeWaiting += 15;
                            ui.log(`Waiting for ${webList[0].name} withdraw...`, timeWaiting);
                            setTimeout(manager.resultReader, 15000);
                            return;
                        }
                    } else {
                        ui.log(`Unable to read last run result, for ID: ${webList[0].id} > ${webList[0].name}`);
                    }

                    timeWaiting = 0;
                    update(true);
                    readUpdateValues(true);
                    return;
                } else {
                    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`);
                    }

                    moveNext();
                    return;
                }
            };

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

                timeWaiting = 0;
                readUpdateValues(true);
            }

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

            function updateWebListItem(result) {
                if (result.withdrawing) {
                    return;
                }

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

                if (result.withdrawnAmount && result.withdrawnAmount > 0) {
                    webList[0].lastWithdraw = new Date();
                    webList[0].balance += result.withdrawnAmount;
                    webList[0].lastClaim = 0;
                    webList[0].aggregate = 0;
                    return;
                }

                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: [] }, wallet: { 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 editWalletCancel() {';
                js += '$.each($("#wallet-table-body .em-input input"), function(x, y) {';
                js += 'y.value = y.dataset.original;});';
                js += '}';

                js += 'function editWalletSave() {';
                js += '  let updateObject = getUpdateObject();';
                js += '  $.each($("#wallet-table-body tr"), function(x, row) {';
                js += '    let textInput = row.querySelector(".em-input input");';
                js += '    if(textInput.dataset.original != textInput.value) {';
                js += '      let single = { id: row.dataset.id, address: textInput.value.trim() };';
                js += '      updateObject.wallet.items.push(single);';
                js += '      updateObject.wallet.changed = true;';
                js += '    }';
                js += '  });';
                js += '  if(updateObject.wallet.changed) {';
                js += '    document.getElementById("update-data").innerHTML = JSON.stringify(updateObject);';
                js += '    document.getElementById("faucets-display-status").innerHTML = "Wallet will be updated as soon as possible...";';
                js += '  }';
                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 += '<div class="modal fade" id="modal-wallet" tabindex="-1" role="dialog" data-backdrop="static" aria-hidden="true">';
                html += '<div class="modal-dialog modal-lg modal-dialog-centered modal-dialog-scrollable" role="document">';
                html += '<div class="modal-content bg-beige" style="background-color: #f6fdd0!important;">';
                html += '<div class="modal-header"><h5 class="modal-title">Your Addresses</h5></div>';
                html += '<div class="modal-body"><div><table class="table table-striped" id="wallet-table">';
                html += '<thead><tr><th class="">Name</th><th class="">Address</th></tr></thead>';
                html += '<tbody class="overflow-auto" id="wallet-table-body"></tbody></table></div></div>';
                html += '<div class="modal-footer"><a class="btn m-2 anchor btn-outline-danger align-middle" onclick="editWalletCancel()" data-dismiss="modal"><i class="fa fa-times-circle"></i> Cancel</a>';
                html += '<a class="btn m-2 anchor btn-outline-success align-middle" onclick="editWalletSave()" data-dismiss="modal"><i class="fa fa-check-circle"></i> Save</a></div>';
                html += '</div></div>';
                html += '</div>';

                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" data-toggle="modal" data-target="#modal-wallet"><i class="fa fa-wallet"></i> Wallet</a><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-4 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" list="promoCode_list" placeholder="Type a Promo code...">';
                html += '<datalist id="promoCode_list">';
                CFReusableCodesSuggestion.forEach( function(x) { html += '<option>' + x + '</option>' });
                html += '</datalist>';
                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-3 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="';
                    if (data[i].balance) {
                        if(typeof data[i].balance == 'string') {
                            tableBody += data[i].balance.split(' ')[0];
                        } else {
                            tableBody += data[i].balance.toFixed(8);
                        }
                    }
                    tableBody += '">';
                    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 text-left" data-field="displayName">' + data[i].name + '</td>';
                    tableBody +='<td class="align-middle text-left" style="width: 20%">';
                    if (Array.isArray(data[i].rf)) {
                        tableBody +='<a class="edit-site" title="Visit site" target="_blank" href="' + data[i].url.href + data[i].rf[helpers.randomInt(0, data[i].rf.length -1)] + '"><i class="fa fa-external-link-alt"></i></a> ';
                    } else {
                        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].type == WebType.BAGIKERAN ? data[i].url.host + data[i].url.pathname : 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">';
                    if (data[i].balance) {
                        if(typeof data[i].balance == 'string') {
                            tableBody += data[i].balance.split(' ')[0];
                        } else {
                            tableBody += data[i].balance.toFixed(8);
                        }
                    }
                    tableBody + '</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 loadWalletTable(data) {
                let tableBody = '';

                for(let i=0, all = data.length; i < all; i++) {
                    tableBody += '<tr class="align-middle" data-id="'+ data[i].id + '">';
                    tableBody += '<td class="align-middle">' + data[i].name + '</td>';
                    tableBody += '<td class="align-middle em-input"><input type="text" class="w-100" data-field="address" data-original="' + data[i].address + '" value="' + data[i].address + '"></td>';
                    tableBody += '</tr>';
                }

                $('#wallet-table-body').html(tableBody);
            };
            function refresh(scheduleData, promotionData, walletData) {
                if (scheduleData) {
                    loadScheduleTable(scheduleData);
                }
                if (promotionData) {
                    loadPromotionTable(promotionData);
                }
                if (walletData) {
                    loadWalletTable(walletData);
                }
            };
            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(run, helpers.randomMs(5000, 10000));
                    return;
                } else if (hasPopup()) {
                        closePopup();
                        setTimeout(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(processRunDetails, helpers.randomMs(10000, 20000));
                } else {
                    if(!is404Error()) {
                        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(isFullyLoaded, helpers.randomMs(45000, 55000));
                }
            };
            function runPromotion() {
                navigationProcess = NavigationProcess.PROCESSING_PROMOTION
                displayStatusUi();
                setTimeout(findPromotionTag, helpers.randomMs(1000, 3000));
            };
            function findCountdownOrRollButton() {
                if( isCountdownVisible() && !isRollButtonVisible() ) {
                    timeWaiting = 0;
                    processRunDetails();
                } else if ( !isCountdownVisible() && isRollButtonVisible() ) {
                    timeWaiting = 0;
                    setTimeout(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(waitInteractions, helpers.randomMs(2000, 4000));
            }
            function waitInteractions() {
                if(interactions.completed()) {
                    roll();
                } else {
                    setTimeout(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 countdownMinutes;
            let timeWaiting= 0;
            function timedOut(addMs) {
                if (timeWaiting/1000 > config.defaults.timeout * 60) {
                    shared.closeWithError('TIMEOUT', '');
                    return true;
                }

                timeWaiting += addMs;
                return false;
            }

            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();
                    }
                } else {
                    setTimeout(findCountdownOrRollButton, helpers.randomMs(12000, 15000));
                }
            };
            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 (timedOut(10000)) {
                        return;
                    }
                    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 (timedOut(5000)) {
                    return;
                }
                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').click();
                        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').click();
                    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 { }
                    }
                }

                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(null, 'TIMEOUT', 'Timed out after clicking a CLAIM button.'), helpers.minToMs(config.defaults.timeout));
            }

            return {
                ptcList: ptcList,
                ptcSingle: ptcSingle
            };
        },
        // TODO: refactor to add more faucets to this processor:
        createGenericFaucetProcessor: function(wt) {
            let webType = wt;
            let countdownMinutes;
            let timeWaiting= 0;
            let selectElement = {
                rollButton: function() {
                    switch (webType) {
                        case WebType.FREELITECOIN:
                            return document.getElementById('roll');
                            break;
                        case WebType.FREEETHEREUMIO:
                            return document.querySelector('#rollform button');
                            break;
                        default:
                            return;
                            break;
                    }
                },
                balance: function() {
                    switch (webType) {
                        case WebType.FREELITECOIN:
                            return document.getElementById('money');
                            break;
                        case WebType.FREEETHEREUMIO:
                            return document.getElementById('cryptovalue')
                            break;
                        default:
                            return;
                            break;
                    }
                }
            };

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

                timeWaiting += addMs;
                return false;
            }

            function run() {
                setTimeout(findCountdownOrRollButton, helpers.randomMs(12000, 15000));
            };
            function findCountdownOrRollButton() {
                if ( isCountdownVisible() ) {
                    timeWaiting = 0;
                    let countdownMinutes = document.getElementById('cislo1');
                    let result = {};
                    result.balance = readBalance();
                    result.nextRoll = helpers.addMinutes(new Date(), countdownMinutes.innerHTML.toString());

                    shared.closeWindow(result);
                    return;
                }

                if ( isRollButtonVisible() ) {
                    if (isHCaptchaVisible()) {
                        waitForCaptcha();
                    } else {
                        clickRoll();
                    }
                } else {
                    setTimeout(findCountdownOrRollButton, helpers.randomMs(10000, 12000));
                }
            };
            function isCountdownVisible() {
                return $(document.getElementById('cislo1')).is(':visible') || $(document.getElementById('cislo2')).is(':visible');
            };
            function isHCaptchaVisible() {
                let hCaptchaFrame = document.querySelector('.h-captcha > iframe');
                if (hCaptchaFrame && $(hCaptchaFrame).is(':visible')) {
                    return true;
                }
                return false;
            };
            function isRollButtonVisible() {
                let rollButton = selectElement.rollButton();
                return rollButton && $(rollButton).is(':visible');
            };
            function waitForCaptcha() {
                if ( document.querySelector('.h-captcha > iframe').getAttribute('data-hcaptcha-response').length > 0) {
                    clickRoll();
                } else {
                    if (timedOut(10000)) {
                        return;
                    }

                    setTimeout(waitForCaptcha, helpers.randomMs(10000, 12000));
                }
            };
            function clickRoll() {
                try {
                    selectElement.rollButton().click();
                    setTimeout(processRunDetails, helpers.randomMs(10000, 12000));
                } catch (err) {
                    shared.closeWithError('CLICK_ROLL_ERROR', err);
                }
            };
            function processRunDetails() {
                let info = document.getElementById('info');

                if (info && $(info).is(':visible')) {
                    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 (timedOut(5000)) {
                    return;
                }
                setTimeout(processRunDetails, helpers.randomMs(5000, 6000));
            };
            function readRolledNumber() {
                let rolled = 0;
                try {
                    rolled = parseInt(document.getElementById('numberroll').innerHTML);
                } catch { }
                return rolled;
            };
            function readBalance() {
                let balance = 0;
                try {
                    balance = selectElement.balance().innerHTML;
                } catch { }
                return balance;
            };
            function readClaimed() {
                let claimed = 0;
                try {
                    let info = document.getElementById('info').innerHTML;
                    let idx = info.search(/0\./);
                    claimed = parseFloat(info.slice(idx, idx + 10));
                } catch { }
                return claimed;
            };

            return {
                run: run
            };
        },
        createBagiKeranProcessor: function() {
            let timeWaiting= 0;
            let elements = {
                errorDivs: function() {
                    return document.querySelectorAll('.alert.alert-danger');
                },
                warningDivs: function() {
                    return document.querySelectorAll('.alert.alert-warning');
                },
                successDivs: function() {
                    return document.querySelectorAll('.alert.alert-success');
                },
                errorCloudflare: function() {
                    return document.querySelector('#cf-error-details p');
                },
                openLoginModalButton: function() {
                    return document.getElementById('submit');
                },
                modal: function() {
                    return document.querySelector('#myModal.show');
                },
                addressInput: function() {
                    return document.querySelector('input[name="address"]');
                },
                submitButton: function() {
                    return document.querySelector('#myModal button[type="submit"]');
                },
                openClaimModalButton: function() {
                    return document.querySelector('form button[type="submit"]');
                },
                openWithdrawModal: function() {
                    return document.getElementById('submit');
                },
                linkWithdrawMinNotReached: function() {
                    return document.querySelector('a.btn.btn-primary.btn-block');
                }
            };

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

                timeWaiting += addMs;
                return false;
            }

            function run() {
                readAlerts();
                processIndex();
//                setTimeout(processIndex, helpers.randomMs(1000, 3000));
            };

            function runCaptchaPage() {
                readAlerts();
                processCaptchaPage();
//                setTimeout(processCaptchaPage, helpers.randomMs(1000, 3000));
            }

            function runWithdraw() {
                readAlerts();
                processWithdraw();
//                setTimeout(processWithdraw, helpers.randomMs(1000, 3000));
            }

            function readAlerts() {
                let elm;

                elements.warningDivs().forEach(function (elem) {
                    if (elem && elem.innerText.includes('already claimed')) { // "You have already claimed in the last 60 minutes.<br>You can claim again in 59 minutes.<br>"
                        let result = {};
                        try {
                            let mins = elem.innerText.split('\n')[1].replace(/\D/g, '');
                            result.nextRoll = helpers.addMinutes(new Date(), mins);
                        } catch {
                            result.nextRoll = helpers.addMinutes(new Date(), "60");
                        }

                        if (shared.getCurrent().params.doWithdraw) {
                            shared.updateWithoutClosing(result);
                            window.location.href = (new URL('withdraw.php', window.location.href)).href;;
                        } else {
                            shared.closeWindow(result);
                        }
                        return;
                    }
                });

                elements.successDivs().forEach(function (elem) {
                    if (elem) {
                        if (elem.innerText.includes('claimed successfully')) { // "You've claimed successfully 2 Satoshi BTC." ...
                            let result = {};
                            result.nextRoll = helpers.addMinutes(new Date(), "60");
                            result.claimed = 0;

                            try {
                                let val = elem.innerText.split('\n')[0].replace(/\D/g, '');
                                if (typeof val == 'string') {
                                    val = +val;
                                }
                                if (Number.isInteger(val)) {
                                    val = val / 100000000;
                                }

                                result.claimed = val;
                            } catch { }

                            try {
                                let mins = elem.innerText.split('\n')[1].replace(/\D/g, '');
                                result.nextRoll = helpers.addMinutes(new Date(), mins);
                            } catch { }

                            if (shared.getCurrent().params.doWithdraw) {
                                shared.updateWithoutClosing(result);
                                let link = elem.querySelector('a');
                                if (link && link.innerText.includes('withdraw')) {
                                    link.click();
                                    return;
                                } else {
                                    window.location.href = (new URL('withdraw.php', window.location.href)).href;;
                                }
                            }
                            shared.closeWindow(result);
                            return;
                        } else if (elem.innerText.includes('was sent to')) { //2 satoshi was sent to <a target="_blank" href="https://faucetpay.io/page/user-admin">your account at FaucetPay.io</a>
                            let result = {};
                            result.withdrawnAmount = 0;
                            let val = elem.innerHTML.split(' ')[0];
                            if (typeof val == 'string') {
                                val = +val;
                            }
                            if (Number.isInteger(val)) {
                                val = val / 100000000;
                            }

                            result.withdrawnAmount = val;
                            shared.closeWindow(result);
                            return;
                        }
                    }
                });

                elm = elements.errorCloudflare();
                if (elm) {
                    // "Access denied | bagi.co.in used Cloudflare to restrict access" @document.title
                    shared.closeWithError('IP RESTRICTED', document.title + ' | ' + elm.innerText);
                    return;
                }

                elements.errorDivs().forEach(function (elem) {
                    if (elem) {
                        if (elem.innerText.toLowerCase().includes('vpn/proxy/tor')) { // "VPN/Proxy/Tor is not allowed on this faucet." ...
                            shared.closeWithError('IP ERROR', elem.innerText);
                            return;
                        } else if (elem.innerText.toLowerCase().includes('look valid')) { // The Bitcoin Address doesn't look valid
                            //invalid address
                            shared.closeWithError('INVALID ADDRESS', elem.innerText);
                            return;
                        } else if (elem.innerText.toLowerCase().includes('login not valid')) { // Login Not Valid, Please reLogin
                            // might be a login timeout (hCaptcha taking too long)
                            shared.closeWithError('LOGIN TIMEOUT', elem.innerText);
                            return;
                        } else if (elem.innerText.toLowerCase().includes('claim not valid')) { // Claim not Valid, Please reClaim. Try again
                            // might be a claim timeout (hCaptcha taking too long)
                            shared.closeWithError('CLAIM TIMEOUT', elem.innerText);
                            return;
                        } else {
                            // Unknown issue
                            shared.closeWithError('ERROR', elem.innerText);
                            return;
                        }
                    }
                });
            }

            function processIndex() {
                if (elements.modal() && elements.addressInput() && elements.submitButton()) { // 2. Fill address & click Login
                    elements.addressInput().value = shared.getCurrent().params.address;
                    elements.submitButton().click(); // shoud redirect but check for timeout
                    return;
                }

                if (elements.openLoginModalButton()) { // 1. Click the Get Started Button
                    elements.openLoginModalButton().click();
                    timeWaiting = 0;
                    setTimeout(processIndex, helpers.randomMs(1000, 3000));
                    return;
                }

                if (elements.openClaimModalButton()) { // Claim Bitcoin Button
                    elements.openClaimModalButton().click();
                    timeWaiting = 0;
                    setTimeout(processIndex, helpers.randomMs(2000, 4000));
                    return;
                }

                setTimeout(run, helpers.randomMs(2000, 4000));
            };

            function processCaptchaPage() {
                if(elements.modal()) {
                    setTimeout(waitForCaptcha, helpers.randomMs(2000, 4000));
                    return;
                }

                if (elements.openLoginModalButton()) { // 1. Click the Claim Button to open the modal w/the hCaptcha
                    elements.openLoginModalButton().click();
                    timeWaiting = 0;
                    setTimeout(processCaptchaPage, helpers.randomMs(1000, 3000));
                    return;
                }
                setTimeout(runCaptchaPage, helpers.randomMs(2000, 4000));
            }

            function processWithdraw() {
                if(elements.modal()) {
                    setTimeout(waitForCaptcha, helpers.randomMs(2000, 4000));
                    return;
                }

                if (elements.openWithdrawModal()) { // 1. Click the Withdraw to FaucetPay submit button to open the modal w/the hCaptcha
                    elements.openWithdrawModal().click();
                    timeWaiting = 0;
                    setTimeout(processWithdraw, helpers.randomMs(2000, 4000));
                    return;
                }
                setTimeout(runWithdraw, helpers.randomMs(2000, 4000));
            }

            function waitForCaptcha() {
                //weird
                let iframe = document.querySelector('.h-captcha > iframe');
                if (!iframe) {
                    iframe = document.querySelector('.g-recaptcha > iframe');
                }

                if(iframe && iframe.getAttribute('data-hcaptcha-response').length > 0) {
                    //claim after hCaptcha
                    if(elements.submitButton()) {
                        elements.submitButton().click(); // should redirect
                        return;
                    }
                }

                if (timedOut(5000)) {
                    return;
                }

                setTimeout(waitForCaptcha, helpers.randomMs(5000, 6000));
            };

            return {
                run: run,
                runCaptchaPage: runCaptchaPage,
                runWithdraw: runWithdraw
            };
        },
    };

    /**
    * 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() {
        // TODO: Review for bagi/keran as it should consider the path too
        if(!shared.isOpenedByManager(window.location.host)) {
            return;
        }

        let currentFromManager = shared.getCurrent();

        if (currentFromManager.type == WebType.STORMGAIN) {
            SiteProcessor = objectGenerator.createSGProcessor();
            setTimeout(SiteProcessor.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);
                            SiteProcessor = objectGenerator.createCFProcessor();
                            interactions = objectGenerator.createInteractions();
                            setTimeout(SiteProcessor.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);
                                SiteProcessor = objectGenerator.createCFProcessor();
                                setTimeout(SiteProcessor.doLogin, helpers.randomMs(1000, 3000));
                            } else {
                                shared.closeWithError('NEED_TO_LOGIN', '');
                            }
                            break;
                        default:
                            break;
                    }
                    break;
                case CFUrlType.PROMOTION:
                    SiteProcessor = objectGenerator.createCFProcessor();
                    interactions = objectGenerator.createInteractions();
                    setTimeout(SiteProcessor.runPromotion, helpers.randomMs(5000, 10000));
                    break;
            }

            return;
        }

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

        if (currentFromManager.type == WebType.FREELITECOIN) {
            SiteProcessor = objectGenerator.createGenericFaucetProcessor(currentFromManager.type);
            let url = new URL(window.location.href);

            if (url.pathname == '/') {
                setTimeout(SiteProcessor.run, helpers.randomMs(2000, 5000));
            } else if (url.pathname.includes('/login')) {
                shared.closeWithError('NEED_TO_LOGIN', '');
            }
            return;
        }

        if (currentFromManager.type == WebType.FREEETHEREUMIO) {
            SiteProcessor = objectGenerator.createGenericFaucetProcessor(currentFromManager.type);
            let url = new URL(window.location.href);

            if (url.pathname == '/free/') {
                setTimeout(SiteProcessor.run(), helpers.randomMs(2000, 5000));
            } else if (url.pathname == '/') {
                shared.closeWithError('NEED_TO_LOGIN', '');
            }
            return;
        }

        if (currentFromManager.type == WebType.BAGIKERAN) {
            SiteProcessor = objectGenerator.createBagiKeranProcessor();
            let url = new URL(window.location.href);

            if (url.href.includes('captha.php')) {
                setTimeout(SiteProcessor.runCaptchaPage, helpers.randomMs(8000, 12000));
                return;
            } else if (url.href.includes('withdraw.php')) {
                setTimeout(SiteProcessor.runWithdraw, helpers.randomMs(8000, 12000));
                return;
            } else {
                setTimeout(SiteProcessor.run, helpers.randomMs(8000, 12000));
                return;
            }
            return;
        }

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

            if(url.pathname.includes('ptc/view')) {
                setTimeout(SiteProcessor.ptcSingle, helpers.randomMs(2000, 5000));
            } else if (url.pathname.includes('ptc')) {
                setTimeout(SiteProcessor.ptcList, helpers.randomMs(2000, 5000));
//            } else if (url.pathname.includes('page/user-admin')) {
//                // Try to claim Reward Points
            } else if (url.href.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();
})();