Chaturbate Easy Tipping Fixed Version

Adds a new tipping popup and modifies the existing one

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Chaturbate Easy Tipping Fixed Version
// @namespace    madTipper
// @version      0.13.1
// @author       omgmikey - fixed on 1/21/2018 by tblopgreg
// @match        https://es.chaturbate.com/*
// @require      https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js
// @require      https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/jquery-cookie/1.4.1/jquery.cookie.min.js
// @grant        GM_getValue
// @grant        GM_setValue
// @license      MIT
// @run-at       document-idle
// @description Adds a new tipping popup and modifies the existing one
// ==/UserScript==

var CSS_GREY  = {'color': 'rgb(88,141,61)'};
var CSS_WHITE = {'color': '#FFFFFF'};
var CSS_BLACK = {'color': '#000000'};

var ID_PREFIX    = '#madTipper'
var CLASS_PREFIX = '.madTipper'
var CLASS_INPUT  = CLASS_PREFIX + '_input';

var HTML_IDS = {
    'BUTTON': 'button',
    'POPUP': 'popup',
    'AMOUNT': 'amount',
    'COUNT': 'count',
    'INTERVAL': 'interval',
    'VARIANCE_LOWER': 'variance_lower',
    'VARIANCE_UPPER': 'variance_upper',

    'START': 'start',
    'STOP': 'stop',
    'TOTAL': 'total',
    'ETA': 'eta'
}

for (var key in HTML_IDS) {
    HTML_IDS[key] = ID_PREFIX + '_' + HTML_IDS[key];
}

var shell = $('.tip_shell');
var tipsLeft = 0;
var tipFunctionTimeout = null;
var juration = loadJuration();

(function initialize() {

    createTipperButton();
    createTipperPopup();
    injectCSS();
    improveDefaultTipPopup();

    loadPreviousSettings();
    initializeButtonCallbacks();
    updateTipperButton();
})();

function createTipperButton() {

    shell.append('<div id="madTipper_button_bg"><a href="#" id="madTipper_button"></a></div>');
}

function updateTipperButton() {

    if (tipsLeft == 0) {
        $(HTML_IDS['BUTTON']).html('MAD TIPPER').css({'width': '80px'});
    }
    else {
        $(HTML_IDS['BUTTON']).html('MAD TIPPER (' + tipsLeft + ')').css({'width': '120px'});
    }
}

function createTipperPopup() {

    shell.append(
        '<div class="overlay_popup" id="madTipper_popup">' +
            '<table width="100%" border="0" cellspacing="0" cellpadding="0">' +
                '<tbody>' +
                    '<tr>' +
                        '<td class="formborder">' +
                            '<div class="title">Mad Tipper</div>' +
                            '<div class="body">' +
                                '<form>' +
                                    '<label>Amount per tip:</label><br >' +
                                    '<input type="text" id="madTipper_amount" class="madTipper_input">' +
                                    '<br />' +

                                    '<label>Number of tips:</label><br >' +
                                    '<input type="text" id="madTipper_count" class="madTipper_input">' +
                                    '<br /><hr />' +

                                    '<label>Interval:</label><br >' +
                                    '<input type="text" id="madTipper_interval" class="madTipper_input">' +
                                    '<br />' +

                                    '<label>Interval variance lower (optional):</label><br >' +
                                    '<input type="text" id="madTipper_variance_lower" class="madTipper_input">' +
                                    '<br />' +

                                    '<label>Interval variance upper (optional):</label><br >' +
                                    '<input type="text" id="madTipper_variance_upper" class="madTipper_input">' +
                                    '<br /><hr />' +

                                    'Total tip:  ' + '<a id="madTipper_total"></a>' +
                                    '<br />' +
                                    'Estimated duration:  ' + '<a id="madTipper_eta"></a>' +
                                '</form>' +
                                '<hr />' +
                                '<button id="madTipper_start">Start</button>' +
                                '<button id="madTipper_stop" disabled="disabled">Stop</button>' +
                            '</div>' +
                        '</td>' +
                    '</tr>' +
                '</tbody>' +
            '</table>' +
        '</div>'
    );
}

function injectCSS() {

    var buttonBackgroundUrl =
        'url("https://ssl-ccstatic.highwebmedia.com/images/btn-sprites2.gif?ac5eba7d5cf3") no-repeat right';

    var buttonFontFamily =
        'UbuntuMedium,Arial,Helvetica,sans-serif';

    var genericButtonCSS = {
        'height':'21px',
        'width':'100px',
        'padding-left':'10px',
        'margin-right':'10px',
        'font-size':'12px',
        'text-shadow':'1px 1px 0 #588d3d',
        'color': '#FFFFFF'
    };

    genericButtonCSS['font-family'] = buttonFontFamily;
    genericButtonCSS['background'] = buttonBackgroundUrl + ' -84px';

    var mainButtonCSS = {
        'position': 'absolute',
        'z-index': 1000,
        'left': '500px',
        'top': '30px',
        'height': '18px',
        'padding': '3px 10px 0 0',
        'text-decoration': 'none',
        'text-align': 'center',
        'width': '80px'
    }

    for (var key in genericButtonCSS) {
        if (mainButtonCSS[key] === undefined) {
            mainButtonCSS[key] = genericButtonCSS[key];
        }
    }

    $(HTML_IDS['BUTTON']).css(mainButtonCSS);

    $(CLASS_INPUT).css({
        'width': 'auto',
        'margin-bottom': '10px'
    });

    $(HTML_IDS['POPUP']).css({
        'position': 'absolute',
        'z-index': 1000,
        'width': '280px',
        'top': '-456px',
        'left': '452px',
        'display': 'none'
    }).draggable();

    $(HTML_IDS['POPUP'] + ' .formborder').css({
        'border-bottom': '2px solid #0b5d81',
        'height': '420px'
    });

    $(HTML_IDS['START']).css(genericButtonCSS);
    genericButtonCSS['background'] = buttonBackgroundUrl + ' -42px';

    delete genericButtonCSS['color'];
    $(HTML_IDS['STOP']).css(genericButtonCSS);
}

function improveDefaultTipPopup() {

    $('.overlay_popup.tip_popup').css({
        'top': '-240px'
    }).draggable();

    $('#tip_message').css({
        'margin-bottom': '20px'
    })
    .append('<input type="checkbox" class="float_right" id="tip_keepopen"></input><br />')
    .append('<br /><label class="float_right" for="tip_keepopen">Keep this window open after tipping</label>');

    $('.float_right').css({
        'float': 'right'
    });

    setPopupHeight('250px');

    var tipPopup = $('.tip_popup');
    var keepOpenCheckbox = $('#tip_keepopen');
    var popupIsForcedOpen = false;

    /* use CB jquery to ensure correct callback execution order */
    var tipPopupForm = defchat_settings.domroot.find('.tip_popup form');
    tipPopupForm.submit(onFormSubmit);

    keepOpenCheckbox.css({
        'margin-top': '10px'
    });

    $('body').click(function(ev) {

        if ($('.tip_button').is(ev.target)) {
            popupIsForcedOpen = false;
            return;
        }

        if (!popupIsForcedOpen || tipPopup.has(ev.target).length) {
            return;
        }

        if (tipPopup.is(':visible')) {
            tipPopup.hide();
        }

        popupIsForcedOpen = false;
    });

    function onFormSubmit() {

        setPopupHeight('270px');

        if (!keepOpenCheckbox.is(':checked')) {
            return;
        }

        if (!tipPopup.is(':visible')) {
            tipPopup.show();
            popupIsForcedOpen = true;
        }
    }

    function setPopupHeight(value) {
        $('.overlay_popup.tip_popup .formborder').css({
            'height': value,
        });
    }
}

function startTipping() {

    var err = verifyTipperFields();

    if (err) {
        alert(err);
        stopTipping();
        return;
    }

    saveCurrentSettings();

    $(HTML_IDS['START']).prop('disabled', true).css(CSS_GREY);
    $(HTML_IDS['STOP']).prop('disabled', false).css(CSS_WHITE);
    $(CLASS_INPUT).prop('disabled', true).css(CSS_GREY);

    tipsLeft = getTipCount();

    /* we really want to send the first one immediately */
    sendTip();

    if (tipsLeft > 0) {
        chainQueueTips();
    }
}

function verifyTipperFields() {

    function isInt(value) {
        var regex = /^[0-9]+$/;
        return regex.test(String(value));
    }

    function isDuration(value) {
        try {
            juration.parse(value);
            return true;
        }
        catch(ex) {
            return false;
        }
    }

    function isDurationOrEmpty(value) {
        return value === '' || isDuration(value);
    }

    if (!isInt(getTipAmountRaw()) || getTipAmount() <= 0) {
        return 'Tip amount field should be a positive integer.';
    }

    if (!isInt(getTipCountRaw()) || getTipCount() <= 0) {
        return 'Tip count field should be a positive integer.';
    }

    if (!isDuration(getTipInterval())) {
        return 'Tip interval should contain a duration. E.g.: "2.5s", "1", "2min"';
    }

    if (!isDurationOrEmpty(getVarianceLowerRaw()) || !isDurationOrEmpty(getVarianceUpperRaw())) {
        return 'Variance fields should contain durations, or be left blank. E.g.: "", "2.5s"';
    }
}

function getSleepInterval() {

    var interval = juration.parse(getTipInterval());
    var lower_bound = interval - getVarianceLower();
    var upper_bound = interval + getVarianceUpper();

    return getRandomNumber(lower_bound, upper_bound) * 1000;
}

function getRandomNumber(min, max) {

    return Math.random() * (max - min) + min;
}

function chainQueueTips() {

    var sleepTime = getSleepInterval();

    tipFunctionTimeout = setTimeout(function() {
        sendTip(chainQueueTips);
    }, sleepTime);
}

function sendTip(queueNextTipFn) {

    var queryUrl = $('.tip_popup form').attr('action');

    var queryParams = $.param({
        'csrfmiddlewaretoken': $.cookie('csrftoken'),
        'tip_amount': getTipAmount(),
        'message': '',
        'tip_room_type': $('#id_tip_room_type').val(),
        'tip_v': defchat_settings.v_tip_vol,
    });

    $.ajax({
        url: queryUrl,
        data: queryParams,
        dataType: 'json',
        type: 'post',
        success: function(response) {
            if (response.error) {
                alert(response.error);
                stopTipping();
            }
        }
    });

    updateTipsLeft();

    if (tipsLeft === 0) {
        stopTipping();
    }
    else if (queueNextTipFn) {
        queueNextTipFn();
    }
}

function updateTipsLeft() {

    tipsLeft--;
    updateTipperButton();
}

function stopTipping() {

    clearTimeout(tipFunctionTimeout);
    tipFunctionTimeout = null;

    tipsLeft = 0;
    updateTipperButton();

    $(HTML_IDS['STOP']).prop('disabled', true).css(CSS_GREY);
    $(HTML_IDS['START']).prop('disabled', false).css(CSS_WHITE);
    $(CLASS_INPUT).prop('disabled', false).css(CSS_BLACK);
}

function initializeButtonCallbacks() {

    var popup = $(HTML_IDS['POPUP']);
    var button = $(HTML_IDS['BUTTON']);

    button.click(function(ev) {
        if (popup.is(':visible')) {
            popup.hide();
        }
        else {
            popup.show();
        }
    });

    popup.click(function(ev) {
        ev.stopPropagation();
    });

    $(HTML_IDS['START']).click(function() {
        startTipping();
        $(HTML_IDS['POPUP']).hide();
    });

    $(HTML_IDS['STOP']).click(function() {
        stopTipping();
    });

    $('body').click(function(ev) {
        if (ev.target.id != button.prop('id')) {
            $(HTML_IDS['POPUP']).hide();
        }
    });

    $(CLASS_INPUT).change(function() {
        calculateAndSetTotalTip();
        calculateAndSetETA();
    });
}

function calculateAndSetTotalTip() {

    var value = $(HTML_IDS['AMOUNT']).val() * $(HTML_IDS['COUNT']).val();
    $(HTML_IDS['TOTAL']).html(value + ' tokens');
}

function calculateAndSetETA() {

    var interval = juration.parse($(HTML_IDS['INTERVAL']).val());

    /* we're not counting the first tip */
    var count = getTipCount() - 1;

    var variance_lower = getVarianceLower();
    var variance_upper = getVarianceUpper();

    var eta = (interval + variance_upper - variance_lower) * count;
    $(HTML_IDS['ETA']).html(juration.stringify(eta, {'format': 'long', 'units': 2}));
}

function getTipAmount() {

    return parseInt(getTipAmountRaw());
}

function getTipAmountRaw() {

    return $(HTML_IDS['AMOUNT']).val();
}

function getTipInterval() {

    return $(HTML_IDS['INTERVAL']).val();
}

function getTipCount() {

    return parseInt(getTipCountRaw());
}

function getTipCountRaw() {

    return $(HTML_IDS['COUNT']).val();
}

function getVarianceLower() {

    return parseVariance(getVarianceLowerRaw());
}

function getVarianceLowerRaw() {

    return $(HTML_IDS['VARIANCE_LOWER']).val();
}


function getVarianceUpper() {

    return parseVariance(getVarianceUpperRaw());
}

function getVarianceUpperRaw() {

    return $(HTML_IDS['VARIANCE_UPPER']).val();
}

function parseVariance(variance) {

    if (variance == '0') {
        variance = 0;
    }

    variance = variance || 0;

    if (variance != 0) {
        variance = juration.parse(variance);
    }

    return variance;
}

function saveCurrentSettings() {

    GM_setValue('amount', getTipAmount());
    GM_setValue('interval', getTipInterval());
    GM_setValue('count', getTipCount());
    GM_setValue('variance_lower', getVarianceLower());
    GM_setValue('variance_upper', getVarianceUpper());
}

function loadPreviousSettings() {

    var amount = GM_getValue('amount', 1);
    $(HTML_IDS['AMOUNT']).val(amount);

    var count = GM_getValue('count', 10);
    $(HTML_IDS['COUNT']).val(count);

    var interval = GM_getValue('interval', '1s');
    $(HTML_IDS['INTERVAL']).val(interval);

    var variance_lower = GM_getValue('variance_lower', '');
    $(HTML_IDS['VARIANCE_LOWER']).val(variance_lower);

    var variance_upper = GM_getValue('variance_upper', '');
    $(HTML_IDS['VARIANCE_UPPER']).val(variance_upper);

    calculateAndSetTotalTip();
    calculateAndSetETA();
}


// Script ends here
// Libs included because they're not on a popular cdn 


/*
 * juration - a natural language duration parser
 * https://github.com/domchristie/juration
 *
 * Copyright 2011, Dom Christie
 * Licenced under the MIT licence
 *
 */

function loadJuration() {

  var UNITS = {
    seconds: {
      patterns: ['second', 'sec', 's'],
      value: 1,
      formats: {
        'chrono': '',
        'micro':  's',
        'short':  'sec',
        'long':   'second'
      }
    },
    minutes: {
      patterns: ['minute', 'min', 'm(?!s)'],
      value: 60,
      formats: {
        'chrono': ':',
        'micro':  'm',
        'short':  'min',
        'long':   'minute'
      }
    },
    hours: {
      patterns: ['hour', 'hr', 'h'],
      value: 3600,
      formats: {
        'chrono': ':',
        'micro':  'h',
        'short':  'hr',
        'long':   'hour'
      }
    },
    days: {
      patterns: ['day', 'dy', 'd'],
      value: 86400,
      formats: {
        'chrono': ':',
        'micro':  'd',
        'short':  'day',
        'long':   'day'
      }
    },
    weeks: {
      patterns: ['week', 'wk', 'w'],
      value: 604800,
      formats: {
        'chrono': ':',
        'micro':  'w',
        'short':  'wk',
        'long':   'week'
      }
    },
    months: {
      patterns: ['month', 'mon', 'mo', 'mth'],
      value: 2628000,
      formats: {
        'chrono': ':',
        'micro':  'm',
        'short':  'mth',
        'long':   'month'
      }
    },
    years: {
      patterns: ['year', 'yr', 'y'],
      value: 31536000,
      formats: {
        'chrono': ':',
        'micro':  'y',
        'short':  'yr',
        'long':   'year'
      }
    }
  };

  var stringify = function(seconds, options) {

    if(!_isNumeric(seconds)) {
      throw "juration.stringify(): Unable to stringify a non-numeric value";
    }

    if((typeof options === 'object' && options.format !== undefined) && (options.format !== 'micro' && options.format !== 'short' && options.format !== 'long' && options.format !== 'chrono')) {
      throw "juration.stringify(): format cannot be '" + options.format + "', and must be either 'micro', 'short', or 'long'";
    }

    var defaults = {
      format: 'short',
      units: undefined
    };

    var opts = _extend(defaults, options);

    var units = ['years', 'months', 'days', 'hours', 'minutes', 'seconds'], values = [];
    var remaining = seconds;
    var activeUnits = 0;
    for(var i = 0, len = units.length;
        i < len && (opts.units == undefined || activeUnits < opts.units);
        i++) {
      var unit = UNITS[units[i]];
      values[i] = Math.floor(remaining / unit.value);
      if (values[i] > 0 || activeUnits > 0)
        activeUnits++;

      if(opts.format === 'micro' || opts.format === 'chrono') {
        values[i] += unit.formats[opts.format];
      }
      else {
        values[i] += ' ' + _pluralize(values[i], unit.formats[opts.format]);
      }
      remaining = remaining % unit.value;
    }
    var output = '';
    for(i = 0, len = values.length; i < len; i++) {
      if(values[i].charAt(0) !== "0" && opts.format != 'chrono') {
        output += values[i] + ' ';
      }
      else if (opts.format == 'chrono') {
        output += _padLeft(values[i]+'', '0', i==values.length-1 ? 2 : 3);
      }
    }
    return output.replace(/\s+$/, '').replace(/^(00:)+/g, '').replace(/^0/, '');
  };

  var parse = function(string) {

    // returns calculated values separated by spaces
    for(var unit in UNITS) {
      for(var i = 0, mLen = UNITS[unit].patterns.length; i < mLen; i++) {
        var regex = new RegExp("((?:\\d+\\.\\d+)|\\d+)\\s?(" + UNITS[unit].patterns[i] + "s?(?=\\s|\\d|\\b))", 'gi');
        string = string.replace(regex, function(str, p1, p2) {
          return " " + (p1 * UNITS[unit].value).toString() + " ";
        });
      }
    }

    var sum = 0,
        numbers = string
                    .replace(/(?!\.)\W+/g, ' ')                       // replaces non-word chars (excluding '.') with whitespace
                    .replace(/^\s+|\s+$|(?:and|plus|with)\s?/g, '')   // trim L/R whitespace, replace known join words with ''
                    .split(' ');

    for(var j = 0, nLen = numbers.length; j < nLen; j++) {
      if(numbers[j] && isFinite(numbers[j])) {
         sum += parseFloat(numbers[j]);
      } else if(!numbers[j]) {
        throw "juration.parse(): Unable to parse: a falsey value";
      } else {
        // throw an exception if it's not a valid word/unit
        throw "juration.parse(): Unable to parse: " + numbers[j].replace(/^\d+/g, '');
      }
    }
    return sum;
  };

  // _padLeft('5', '0', 2); // 05
  var _padLeft = function(s, c, n) {
      if (! s || ! c || s.length >= n) {
        return s;
      }

      var max = (n - s.length)/c.length;
      for (var i = 0; i < max; i++) {
        s = c + s;
      }

      return s;
  };

  var _pluralize = function(count, singular) {
    return count == 1 ? singular : singular + "s";
  };

  var _isNumeric = function(n) {
    return !isNaN(parseFloat(n)) && isFinite(n);
  };

  var _extend = function(obj, extObj) {
    for (var i in extObj) {
      if(extObj[i] !== undefined) {
        obj[i] = extObj[i];
      }
    }
    return obj;
  };

  var juration = {
    parse: parse,
    stringify: stringify,
    humanize: stringify
  };

  return juration;
};