ChaturbateTokenStats

Get Chaturbate token stats for individual rooms.

// ==UserScript==
// @name         ChaturbateTokenStats
// @namespace    http://tampermonkey.net/
// @version      2025-04-22
// @description  Get Chaturbate token stats for individual rooms.
// @author       nyoob/seraphine24
// @match        https://chaturbate.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=chaturbate.com
// @grant        none
// @require      https://code.jquery.com/jquery-3.6.0.min.js
// ==/UserScript==

const div = `
<div style="background-color: red; height: auto; width: 100%; position: static; overflow: hidden; display: block; padding: 5px 0px; text-align: center; box-sizing: border-box; font-size: 14px; font-weight: 400; font-family: UbuntuMedium, Helvetica, Arial, sans-serif; color: rgb(73, 73, 73);" ts="p" class="siteNotice">
   <div class="wrapper seratkstats" style="background-color: crimson; padding: 15px; border-width: 1px; border-style: solid;">
      <h2>Stop spending money on porn! You can watch for free.</h2>
      <div>Imagine what you could do with that money! Go on vacation, get something for your mother, buy furniture, save up for a car.</div>
      <div>Chaturbate is even worse than gambling, more addictive with less returns. The only thing with comparable dopamine output is heroin.</div>
      <div>Remember, the love you think you're receiving is fake. These models learned a lot of psychological tricks to get you to feel special so you spend your money on them.</div>
      <button class="seraBtn" onclick="getTkStats()">Load token stats</button>
      <button class="seraBtn seraDlBtn" onclick="dlTkTx()" disabled>Download Tx JSON</button>
   </div>
</div>
`;

const styles = `
#site_notices .wrapper > div { margin-top: 8px; }
.loadStats { color: blue; }
.seratable { display: flex; justify-content: center; }
.seratable td, .seratable th { border-bottom: 1px solid white; }
.seraBtn { padding: 6px 12px; background-color: orange; border-radius: 8px; border: 1px solid pink; margin-top: 8px; }
.seraBtn:disabled { background-color: gray; }
`;

window.getTkStats = () => {
    var all = []
    function loadMore(last_tx_id) {
        fetch("/api/ts/tipping/token-stats/?max_transaction_id=" + last_tx_id + "&cashpage=0")
            .then((r) => r.json())
            .then((r) => {
            all = [...all, ...Object.values(r.transactions)];

            if(!r.txns_fully_loaded) {
                loadMore(r.transactions[r.transactions.length - 1].id)
            } else {
                localStorage.setItem("seraTkStats", JSON.stringify(all));
                alert("finished loading tk stats");
                window.location.reload();
            }
        })
    }
    loadMore();
}

window.dlTkTx = () => {
    const stats = localStorage.getItem("seraTkStats");
    var dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(stats);
    var downloadAnchorNode = document.createElement('a');
    downloadAnchorNode.setAttribute("href", dataStr);
    downloadAnchorNode.setAttribute("download", "transactions.json");
    document.body.appendChild(downloadAnchorNode); // required for firefox
    downloadAnchorNode.click();
    downloadAnchorNode.remove();
}

const calculateTotalStats = () => {
    const data = JSON.parse(localStorage.getItem("seraTkStats"));

    var totalSpent = 0;
    var spentByUser = {};
    data.forEach((e) => {
        if(e.description == "Tokens purchased") return;
        totalSpent += Math.abs(e.tokens);
        if(!spentByUser[e.username]) {
            spentByUser[e.username] = 0;
        }
        spentByUser[e.username] += Math.abs(e.tokens);
    });

    spentByUser = Object.entries(spentByUser).sort(([,a],[,b]) => b-a);

    return {totalSpent, spentByUser};
}

const tksToDollar = (tks) => {
    const minPrice = 0.079
    const maxPrice = 0.109
    return {min: (tks * minPrice).toFixed(2), max: (tks * maxPrice).toFixed(2)};
}


(function() {
    // div
    $("#site_notices").append(div);
    // style
    $('html > head').append(`<style>${styles}</style>`);
    // stats
    const totalStats = calculateTotalStats();
    const totalSpentInDollar = tksToDollar(totalStats.totalSpent);

    if(!totalStats) {
        $("#site_notices .wrapper").append(`
            <div class="seraAlert">Please load token stats. You have to be logged in to do that.<br/>Detailed stats will be shown after loading.</div>
        `)
    } else {
        $(".seraDlBtn").prop('disabled', false);
        $("#site_notices .wrapper").append(`
    <div>Total spent: ${totalStats.totalSpent}tks (in dollars: ${totalSpentInDollar.min}-${totalSpentInDollar.max})</div>
    <div>
    <details>
    <summary>Tks spent per user</summary>
    <div class="seratable">
    <table><thead>
  <tr>
    <td class="">Username</td>
    <td class="">Tokens spent</td>
    <td class="">Min Dollars</td>
    <td class="">Max Dollars</td>
  </tr></thead>
<tbody>
    ${totalStats.spentByUser.map(([user, spent]) => {
        const totalSpentInDollar = tksToDollar(spent);
        return `<tr>
        <td>${user}</td><td>${spent}tks</td> <td>${totalSpentInDollar.min}</td><td>${totalSpentInDollar.max}</td>
        </tr>`;
    }).join("")}
    </tbody>
    </table>
    </div>
    </details>
    </div>
    `)
    }
})();