Greasy Fork

Greasy Fork is available in English.

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

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

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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

            //TODO: refactor
            function resultReader() {
                if(isObsolete()) {
                    return;
                }

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

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

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

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

                if (promoCode == 'REMOVEALLPROMOS' ) {
                    CFPromotions.removeAll();
                    promoCodeElement.innerText = '';
                    promoDisplayStatus.innerHTML = 'Promo codes removed!';
                    ui.refresh(null, CFPromotions.getAll());
                } else if(promoCode != '') {
                    CFPromotions.addNew(promoCode);
                    promoCodeElement.innerText = '';
                    $('#promo-text-input').val('');
                    promoDisplayStatus.innerHTML = 'Code ' + promoCode + ' added!';
                    ui.log('Promo code ' + promoCode + ' added');
                    ui.refresh(null, CFPromotions.getAll());
                }
            };
            function updateWebListItem(result) {
                GM_log(result);
                if (result.claimed) {
                    result.claimed = parseFloat(result.claimed);
                    if(!isNaN(result.claimed)) {
                        webList[0].lastClaim = result.claimed;
                        webList[0].aggregate += result.claimed;
                    }
                }
                if(result.balance) {
                    webList[0].balance = result.balance;
                }
                if(result.nextRoll) {
                    webList[0].nextRoll = new Date(result.nextRoll);
                }
                if(result.promoStatus) {
                    CFPromotions.updateFaucetForCode(result.promoCode, webList[0].id, result.promoStatus);
                }
            };
            function getCFlist() {
                let items;
                items = webList.filter(f => f.type === WebType.CRYPTOSFAUCETS);
                items = items.map(x => {
                    return {
                        id: x.id,
                        name: x.name
                    };});
                items.sort((a, b) => (a.name > b.name) ? 1 : -1);

                return items;
            };
            return{
                init:start,
                process: process,
                resultReader: resultReader,
                getFaucetsForPromotion: getCFlist,
                readNewPromoCode: readNewPromoCode
            };
        },
        createUi: function() {
            let logLines = ['', '', '', '', ''];
            function init(cfFaucets) {
                appendCSS();
                appendJavaScript();
                appendHtml();
                createPromoTable(cfFaucets);
            };
            function appendCSS() {
                let css = '';
                css += '<style type="text/css">';
                css += '#promo-container { width: 80%; }';
                css += '#promo-code-new { display: none; }';
                css += 'td { text-align: center; }';
                css += '</style>';
                $('head').append(css);
            };
            function appendJavaScript() {
                let js = '';
                js += '<script language="text/javascript">';
                js += 'function savePromoCode() {';
                js += 'var promoText = document.getElementById("promo-text-input");';
                js += 'var promoCode = document.getElementById("promo-code-new");';
                js += 'var promoDisplayStatus = document.getElementById("promo-display-status");';
                js += 'promoCode.innerHTML = promoText.value.trim();';
                js += 'promoDisplayStatus.innerHTML = "Adding code&nbsp&quot;<b>" + promoCode.innerHTML + "</b>&quot;... This could take around a minute. Please wait..."';
                js += '}';
                js += 'function removeAllPromos() {';
                js += 'var promoCode = document.getElementById("promo-code-new");';
                js += 'var promoDisplayStatus = document.getElementById("promo-display-status");';
                js += 'promoCode.innerHTML = "REMOVEALLPROMOS";';
                js += 'promoDisplayStatus.innerHTML = "Removing all promotion codes... This could take around a minute. Please wait..."';
                js += '}';
                js += '</script>';

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            function getAll() {
                return codes;
            };

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

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

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

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

            function removeAll() {
                codes = [];
                save();
            };
            return {
                addNew: addNew,
                removeAll: removeAll,
                getAll: getAll,
                load: load,
                updateFaucetForCode: updateFaucetForCode,
                hasPromoAvailable: hasPromoAvailable
            }
        },
        createInteractions: function(){
            let randomInteractionLevel = RandomInteractionLevel.MEDIUM;
            let maxActions = 0;
            let performedActions = -1;
            let selectableElements;
            let actions = {
                available: [
                    function() {
                        GM_log('Scrolling...');
                        $('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() {
                        GM_log('Selecting text...');
                        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);
                            } else {
                                GM_log('Text Selection not working');
                            }
                        } 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()) {
                    GM_log('SG: is loading');
                    setTimeout(SGProcessor.run, helpers.randomMs(5000, 10000));
                    return;
                } else {
                    GM_log('SG: starting process');
                    if(isMinerActive()) {
                        processRunDetails();
                    } else {
                        activateMiner();
                    }
                }
            };
            function isLoading() {
                return $('#loader-logo').length;
            };
            function isMinerActive() {
                timerSpans = $('.mb-8 .wrapper .mb-1 span');
                GM_log(timerSpans);
                if(timerSpans.length > 0) {
                    GM_log('SG: is already running');
                    return true;
                } else {
                    GM_log('SG: is not running');
                    return false;
                }
                return (timerSpans.length === 0);
            };
            function activateMiner() {
                const activateButton = document.querySelector('.mb-8 .wrapper button');
                if (activateButton) {
                    GM_log('SG: Activating');
                    activateButton.click();
                    //TODO: wait for SG to activate before checking values
                    setTimeout(SGProcessor.processRunDetails, helpers.randomMs(10000, 20000));
                } else {
                    GM_log('SG: Activate Button not found');
                    if(!is404Error()) {
                        SGProcessor.processRunDetails()
                    }
                }
                GM_log('SG: Process ended');
            };

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

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

            function run() {
                navigationProcess = NavigationProcess.ROLLING;
                setTimeout(CFProcessor.findCountdownOrRollButton, helpers.randomMs(2000, 5000));
            };
            function runPromotion() {
                GM_log('WEB DE PROMOTION');
                navigationProcess = NavigationProcess.PROCESSING_PROMOTION
                setTimeout(CFProcessor.findPromotionTag, helpers.randomMs(1000, 3000));
            };
            function findCountdownOrRollButton() {
                GM_log('En findCountdownOrRollButton');
                if( isCountdownVisible() && !isRollButtonVisible() ) {
                    GM_log('Countdown visible');
                    timeWaiting = 0;
                    processRunDetails();
                } else if ( !isCountdownVisible() && isRollButtonVisible() ) {
                    GM_log('Roll Button Visible');
                    timeWaiting = 0;
                    interact();
                } else {
                    if (timeWaiting/1000 > helpers.randomInt(80, 120)) {
                        window.location.reload();
                    }
                    GM_log('Roll Button/Countdown combination not valid yet');
                    timeWaiting += 3000;
                    setTimeout(CFProcessor.findCountdownOrRollButton, helpers.randomMs(2000, 5000));
                    //                setTimeout(CFProcessor.findCountdownOrRollButton, timeWaiting += helpers.randomMs(100, 200));
                    GM_log('TIME WAITING: ' + timeWaiting);
                }
            };
            function interact() {
                let selectables = []
                selectables = selectables.concat($('td').toArray());
                selectables = selectables.concat($('p').toArray());
                selectables = selectables.concat($('th').toArray());

                interactions.start(selectables);
                setTimeout(CFProcessor.waitInteractions, helpers.randomMs(2000, 4000));
            }
            function waitInteractions() {
                GM_log('Waiting for random interactions...');
                if(interactions.completed()) {
                    roll();
                } else {
                    setTimeout(CFProcessor.waitInteractions, helpers.randomMs(2000, 4000));
                }
            }
            function isCountdownVisible() {
                countdown = $('.timeout-wrapper');
                return ($(countdown).length > 0 && $(countdown[0]).is(':visible'));
            };
            function isRollButtonVisible() {
                rollButton = $('.main-button-2.roll-button.bg-2');
                return ($(rollButton).length > 0 && $(rollButton[0]).is(':visible'));
            };
            function roll() {
                $(rollButton[0]).click();
                // wait for results and countdown
                setTimeout(CFProcessor.findCountdownOrRollButton, helpers.randomMs(2000, 3000));
            }
            function isPromotionTagVisible() {
                let pTags = $('p');
                if (pTags.length > 0) {
                    promotionTag = $('p')[0];
                    return true;
                }
                return false;
            };
            function findPromotionTag() {
                GM_log('En findPromotionTag');
                if( isPromotionTagVisible() ) {
                    GM_log('PromotionTag visible');
                    //TODO: process it
                    processRunDetails();
                } else {
                    GM_log('PromotionTag not found yet');
                    setTimeout(CFProcessor.findPromotionTag, helpers.randomMs(2000, 5000));
                }
            };
            function processRunDetails() {
                GM_log('En processRunDetails');
                let result = {};
                if(navigationProcess == NavigationProcess.ROLLING) {
                    result.nextRoll = readCountdown();
                    result.claimed = readClaimed();
                    result.balance = readBalance();
                } else if (navigationProcess == NavigationProcess.PROCESSING_PROMOTION) {
                    result.promoStatus = readPromoStatus();
                    result.promoCode = readPromoCode();
                    if (result.promoStatus == PromoStatus.ACCEPTED) {
                        result.nextRoll = helpers.addMinutes(new Date(), "-120");
                    }
                }
                GM_log('Almacenaria:');
                GM_log('nextRoll: [' + result.nextRoll + ']');
                GM_log('claimed: [' + result.claimed + ']');
                GM_log('balance: [' + result.balance + ']');
                GM_log('promoStatus: [' + result.promoStatus + ']');
                shared.closeWindow(result);
            };
            function readCountdown() {
                GM_log('En 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() {
                GM_log('En readClaimed')
                let claimed = 0;
                try {
                    claimed = $('.result')[0].innerHTML;
                    claimed = claimed.trim();
                    claimed = claimed.slice(claimed.lastIndexOf(" ") + 1);
                } catch(err) { }
                return claimed;
            };
            function readBalance() {
                GM_log('En readBalance')
                let balance = "";
                try {
                    balance = $('.navbar-coins.bg-1 a').first().text();
                } catch(err) { }
                return balance;
            };
            function readPromoStatus() {
                GM_log('En readPromoStatus')
                let promoStatus = PromoStatus.UNKNOWNERROR;
                try {
                    if(promotionTag.innerHTML.indexOf(localeConfig.stringSearches.promoCodeAccepted) > 0) {
                        return PromoStatus.ACCEPTED;
                    } else if(promotionTag.innerHTML.indexOf(localeConfig.stringSearches.promoCodeUsed) > 0) {
                        return PromoStatus.USEDBEFORE;
                    } else if(promotionTag.innerHTML.indexOf(localeConfig.stringSearches.promoCodeInvalid) > 0) {
                        return PromoStatus.INVALID;
                    } else if(promotionTag.innerHTML.indexOf(localeConfig.stringSearches.promoCodeInvalid) > 0) {
                        return PromoStatus.INVALID;
                    }
                } catch ( err ) { }
                return promoStatus;
            };
            function readPromoCode() {
                GM_log('En readPromoCode')
                var urlSplit = window.location.href.split('/');
                return urlSplit[urlSplit.length - 1];
            }
            return {
                run: run,
                runPromotion: runPromotion,
                findCountdownOrRollButton: findCountdownOrRollButton,
                findPromotionTag: findPromotionTag,
                waitInteractions: waitInteractions
            };
        }
    };


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

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

    function detectWeb() {
        if(!shared.isOpenedByManager(window.location.href)) {
            GM_log('Web was manually opened: ' + window.location.href);
            return;
        }

        addJS_Node (null, null, overrideSelectNativeJS_Functions);
        //TODO: Add a fixed div to let the user know the process is running
        GM_log(shared.getCurrent().type);
        if(window.location.href.indexOf('promotion') > 0) {
            CFProcessor = objectGenerator.createCFProcessor();
            interactions = objectGenerator.createInteractions();

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

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

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

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

            manager.init();
        } else {
            detectWeb();
        }
    }
    init();
})();