fogbanksy / Chaturbate Tipping Spree

// ==UserScript==
// @name         Chaturbate Tipping Spree
// @namespace    https://openuserjs.org/users/fogbanksy
// @version      1.1.2
// @author       fogbanksy
// @copyright    2022, fogbanksy (https://openuserjs.org/users/fogbanksy)
// @copyright    2021, pfuixxx (https://openuserjs.org/users/pfuixxx)
// @match        https://chaturbate.com/*
// @exclude      https://chaturbate.com/
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @icon         https://chaturbate.com/favicon.ico
// @require      https://cdn.jsdelivr.net/npm/jquery@3.6.1
// @require      https://cdn.jsdelivr.net/npm/jqueryui@1.11.1/jquery-ui.min.js
// @require      https://cdn.jsdelivr.net/npm/jquery.cookie@1.4.1/jquery.cookie.min.js
// @description  Adds a button for automatically tipping a bunch of times in a row
// @license      MIT
// ==/UserScript==

// ==OpenUserJS==
// @author       fogbanksy
// ==/OpenUserJS==

// HELPERS
const cr = (tag, obj) => Object.assign(document.createElement(tag), obj);
const q = (sel) => document.querySelector(sel);

let elements = {}, sibling, saveId;

const createButton = () => {
    const buttonStyle = `background-color: #090;
    color: #fff;
    border: 1px solid #288a09;
    border-radius: 4px;
    overflow: hidden;
    line-height: 1.4;
    height: 24px;
    font-size: 12px;
    font-family: UbuntuMedium, Helvetica, Arial, sans-serif;
    margin: 11px 4px 11px 0px;
    text-overflow: ellipsis;
    padding: 3px 10px;
    box-sizing: border-box;
    cursor: pointer;
    display: inline-block;
    border-width: 1px;
    border-style: solid;`;

    elements.popupBtn = cr('span', { className: 'tippingSpreeButton', innerText: 'TIP SPREE', style: buttonStyle });
    elements.popupBtn.addEventListener('click', openPopup);
    sibling.parentElement.appendChild(elements.popupBtn);
};

const createPopup = () => {
    elements.popup = cr('div', { className:'tippingSpreePopup' });
    elements.popup.style.position = 'absolute';
    elements.popup.appendChild(cr('div', { innerText:'Tipping Spree' }));

    const vars = GM_getValue('vars', {});

    const div = cr('div');
    div.appendChild(cr('label', { forHtml:'tippingSpreePerTip', innerText:'Amount per tip:' }));
    div.appendChild(elements.perTip = cr('input', { id:'tippingSpreePerTip', value:vars.perTip || 1 }));

    div.appendChild(cr('label', { forHtml:'tippingSpreeNumTimes', innerText:'Number of tips:' }));
    div.appendChild(elements.numTimes = cr('input', { id:'tippingSpreeNumTimes', value:vars.numTimes || 10 }));
    div.appendChild(cr('hr'));

    div.appendChild(cr('label', { forHtml:'tippingSpreeInterval', innerText:'Interval (seconds):' }));
    div.appendChild(elements.interval = cr('input', { id:'tippingSpreeInterval', value:vars.interval || 5 }));

    div.appendChild(cr('label', { forHtml:'tippingSpreeRanMin', innerText:'Lower variance (seconds):' }));
    div.appendChild(elements.ranMin = cr('input', { id:'tippingSpreeRanMin', placeholder:'(optional)', value:vars.ranMin || '' }));

    div.appendChild(cr('label', { forHtml:'tippingSpreeRanMax', innerText:'Upper variance (seconds):' }));
    div.appendChild(elements.ranMax = cr('input', { id:'tippingSpreeRanMax', placeholder:'(optional)', value:vars.ranMax || '' }));
    div.appendChild(cr('hr'));

    const totTip = cr('span', { innerText:'Total tip: ' });
    totTip.appendChild(elements.infoTotal = cr('span', { className:'tippingSpreeHighlight' }));
    const totDur = cr('span', { innerText:'Total duration: ' });
    totDur.appendChild(elements.infoDur = cr('span', { className:'tippingSpreeHighlight' }));
    div.appendChild(totTip);
    div.appendChild(totDur);
    div.appendChild(cr('hr'));

    div.appendChild(elements.start = cr('button', { innerText:'Start' }));
    elements.start.style.backgroundColor = '#33ca33';

    elements.popup.appendChild(div);

    const parent = q('#SplitModeTipCallout').parentElement;
    parent.appendChild(elements.popup);
    parent.appendChild(elements.close = cr('div', { className:'tippingSpreePopupClose', hidden:true }));
    closePopup();

    // Setup event listeners
    elements.perTip.addEventListener('input', onInput);
    elements.numTimes.addEventListener('input', onInput);
    elements.interval.addEventListener('input', onInput);
    elements.ranMin.addEventListener('input', onInput);
    elements.ranMax.addEventListener('input', onInput);
    elements.close.addEventListener('click', closePopup);
    elements.start.addEventListener('click', () => {
        if (0 < tipsLeft) {
            stopTipSpree();
            elements.start.innerText = 'Start';
            elements.start.style.backgroundColor = '#33ca33';
        } else if (startTipSpree()) {
            elements.start.innerText = 'Stop';
            elements.start.style.backgroundColor = 'red';
        }
    });

    onInput();
};

const onInput = () => {
    clearTimeout(saveId);
    const vars = getVars();
    if (isFinite(vars.numTimes)) {
        if (isFinite(vars.perTip)) {
            elements.infoTotal.innerText = vars.perTip * vars.numTimes;
        } else {
            elements.infoTotal.innerText = '-';
        }
        if (isFinite(vars.interval)) {
            let min=0, max=0;
            if (isFinite(vars.ranMin)) {
                min = vars.interval - vars.ranMin;
            }
            if (isFinite(vars.ranMax)) {
                max = vars.ranMax + vars.interval;
            }
            min *= (vars.numTimes - 1);
            max *= (vars.numTimes - 1);
            if (min === max) {
                elements.infoDur.innerText = `${min}s`;
            } else {
                elements.infoDur.innerText = `${min}s-${max}s`;
            }
        } else {
            elements.infoDur.innerText = '-';
        }
    }
    saveId = setTimeout(() => GM_setValue('vars', vars), 500);
};

const getVars = () => {
    return {
        perTip: +elements.perTip.value,
        numTimes: +elements.numTimes.value,
        interval: +elements.interval.value,
        ranMin: elements.ranMin.value.length ? +elements.ranMin.value : null,
        ranMax: +elements.ranMax.value.length ? +elements.ranMax.value : null
    };
};

const openPopup = () => {
    if (!elements.popup) {
        createPopup();
    }

    const btnRect = elements.popupBtn.getBoundingClientRect();

    elements.popup.style.display = 'block';
    elements.close.style.display = 'block';
    elements.popup.style.top = `${scrollY + btnRect.top - 10 - elements.popup.offsetHeight}px`;
    elements.popup.style.left = `${scrollX + btnRect.left - 50}px`;
};

const closePopup = () => {
    elements.popup.style.display = 'none';
    elements.close.style.display = 'none';
};

const sendTip = (username, tipAmount) => {
    console.log('Sending', tipAmount, 'tip to', username);
    $.post(`https://chaturbate.com/tipping/send_tip/${username}/`, {
        csrfmiddlewaretoken: $.cookie('csrftoken'),
        tip_amount: tipAmount
    });
};

let spreeId, tipsLeft = 0;
const startTipSpree = () => {
    if (spreeId) {
        return;
    }

    const vars = getVars();
    if (!validateVars(vars)) {
        return false;
    }

    tipsLeft = vars.numTimes;

    const username = /\/(.*?)\//.exec(location.pathname)[1];
    const tipAmount = vars.perTip;

    const nextTip = () => {
        if (0 < --tipsLeft) {
            let time = vars.interval + (Math.random() * (vars.ranMax + vars.ranMin) - vars.ranMin);
            spreeId = setTimeout(() => {
                sendTip(username, tipAmount);
                nextTip();
            }, time * 1000);
            elements.popupBtn.innerText = `TIP SPREE (${tipsLeft} left)`;
        } else {
            elements.start.innerText = 'Start';
            elements.start.style.backgroundColor = 'green';
            stopTipSpree();
        }
    };

    sendTip(username, tipAmount);
    nextTip();

    return true;
};

const stopTipSpree = () => {
    tipsLeft = 0;
    clearTimeout(spreeId);
    spreeId = null;
    elements.popupBtn.innerText = 'TIP SPREE';
};

const validateVars = (vars) => {
    if (!isFinite(vars.perTip)) {
        alert('Amount per tip is not a number');
        return false;
    } else if (Math.round(vars.perTip) !== vars.perTip) {
        alert('Amount per tip is not an even number');
        return false;
    } else if (vars.perTip < 1) {
        alert('Amount per tip is not a positive number');
        return false;
    }

    if (!isFinite(vars.numTimes)) {
        alert('Number of tips is not a number');
        return false;
    } else if (Math.round(vars.perTip) !== vars.perTip) {
        alert('Number of tips is not an even number');
        return false;
    } else if (vars.perTip < 1) {
        alert('Number of tips is not a positive number');
        return false;
    }

    if (!isFinite(vars.interval)) {
        alert('Interval is not a number');
        return false;
    } else if (vars.interval <= 0) {
        alert('Interval is not a positive number');
        return false;
    }

    if (vars.ranMin != null) {
        if (!isFinite(vars.ranMin)) {
            alert('Lower variance is not a number');
            return false;
        } else if (vars.ranMin <= 0) {
            alert('Lower variance is not a positive number');
            return false;
        }

        if (vars.interval <= vars.ranMin) {
            alert('Lower variance is too low');
            return false;
        }
    }

    if (vars.ranMax != null) {
        if (!isFinite(vars.ranMax)) {
            alert('Upper variance is not a number');
            return false;
        } else if (vars.ranMax <= 0) {
            alert('Upper variance is not a positive number');
            return false;
        }
    }

    return true;
};

// Init
const init = () => {
    GM_addStyle(`.tippingSpreePopup { position:absolute; background-color:white; z-index:1001; display:block; border:2px solid #0b5d81;
        border-radius:4px; font-family:UbuntuRegular,Helvetica,Arial,sans-serif; font-size:12px; color:#494949; }
.tippingSpreePopup > div { padding:6px; }
.tippingSpreePopup > div:first-child { font-size:15px; font-family:UbuntuRegular,Helvetica,Arial,sans-serif; color:#0b5d81; background-color:#e0e0e0; font-weight:bold; }
.tippingSpreePopup label { display:block; font-size:11px; }
.tippingSpreePopup input { margin-bottom:5px; }
.tippingSpreePopup > div > span { display:block; }
.tippingSpreePopup > div > span > span { color:#c35a00; font-weight:bold; }
.tippingSpreePopup button { padding:9px; border:1px solid #333; border-radius:4px; }
.tippingSpreePopupClose { position:fixed; left:0; top:0; right:0; bottom:0; }
.tippingSpreeButton { overflow:hidden; line-height:1.4; height:21px; font-size:12px; font-family:UbuntuMedium,Helvetica,Arial,sans-serif;
    text-shadow:rgb(88, 141, 61) 1px 1px 0px; margin:11px 5px; text-overflow:ellipsis; padding:3px 10px;
    border-radius:4px; box-sizing:border-box; cursor:pointer; display:inline-block; }`);

    createButton();
};

const wait = () => {
    'use strict'

    if (document.querySelector('.sendTipButton')) {
        if (sibling = document.querySelector('.currentBalance')) {
            init();
        }
    } else {
        setTimeout(wait, 500);
    }
};

wait();