Greasy Fork is available in English.

twitter rate limit viewer

display current rate limit status

// ==UserScript==
// @name         twitter rate limit viewer
// @namespace    https://www.sapphire.sh/
// @description  display current rate limit status
// @match        https://twitter.com/*
// @match        https://mobile.twitter.com/*
// @grant        none
// @author       sapphire
// @version      1688745353703
// ==/UserScript==
/******/ (() => { // webpackBootstrap
/******/ 	"use strict";
/******/ 	var __webpack_modules__ = ({

/***/ "./src/scripts/twitter-rate-limit-viewer.ts":
/*!**************************************************!*\
  !*** ./src/scripts/twitter-rate-limit-viewer.ts ***!
  \**************************************************/
/***/ (function() {


var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
const DISPLAY_ID = 'rate-limit-viewer';
const DISPLAY_POSITION_KEY = `${DISPLAY_ID}-position`;
const statusTable = {};
const handleStatus = (status) => {
    // console.log('status', status);
    statusTable[status.url] = status;
};
const FONT_FAMILY = '"TwitterChirp",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif';
let dragging = false;
let offsetX = 0;
let offsetY = 0;
const attachDisplay = () => __awaiter(void 0, void 0, void 0, function* () {
    var _a;
    const el = document.createElement('div');
    el.id = DISPLAY_ID;
    const prevPosition = (_a = window.localStorage
        .getItem(DISPLAY_POSITION_KEY)) === null || _a === void 0 ? void 0 : _a.split(',');
    Object.assign(el.style, {
        position: 'fixed',
        padding: '0 8px',
        top: prevPosition ? `${prevPosition[1]}px` : '60vh',
        left: prevPosition ? `${prevPosition[0]}px` : '70vw',
        fontFamily: FONT_FAMILY,
        fontSize: 'small',
        backgroundColor: '#ffffff',
        whiteSpace: 'nowrap',
        border: '1px solid #000000',
        borderRadius: '8px',
        boxShadow: 'rgba(0, 0, 0, 0.05) 0px 6px 24px 0px, rgba(0, 0, 0, 0.08) 0px 0px 0px 1px',
        visibility: 'hidden',
    });
    el.addEventListener('mousedown', (event) => {
        const rect = el.getBoundingClientRect();
        offsetX = rect.left - event.clientX;
        offsetY = rect.top - event.clientY;
        dragging = true;
    });
    window.addEventListener('mousemove', (event) => {
        if (!dragging) {
            return;
        }
        if (!(event.target instanceof HTMLElement)) {
            return;
        }
        const x = event.clientX + offsetX;
        const y = event.clientY + offsetY;
        Object.assign(el.style, {
            top: `${y}px`,
            left: `${x}px`,
        });
    });
    window.addEventListener('mouseup', (event) => {
        if (!dragging) {
            return;
        }
        dragging = false;
        if (!(event.target instanceof HTMLElement)) {
            return;
        }
        const rect = el.getBoundingClientRect();
        const x = Math.max(0, Math.min(event.clientX + offsetX, window.innerWidth - rect.width));
        const y = Math.max(0, Math.min(event.clientY + offsetY, window.innerHeight - rect.height));
        Object.assign(el.style, {
            top: `${y}px`,
            left: `${x}px`,
        });
        window.localStorage.setItem(DISPLAY_POSITION_KEY, `${x},${y}`);
    });
    document.body.appendChild(el);
    return el;
});
const TIME_UNITS = [
    { amount: 60, name: 'seconds' },
    { amount: 60, name: 'minutes' },
    { amount: 24, name: 'hours' },
    { amount: 7, name: 'days' },
    { amount: 30 / 7, name: 'weeks' },
    { amount: 12, name: 'months' },
    { amount: Infinity, name: 'years' },
];
const formatTime = (a, b) => {
    const formatter = new Intl.RelativeTimeFormat('en-US', { numeric: 'auto' });
    let diff = (a - b) / 1000;
    for (const unit of TIME_UNITS) {
        if (Math.abs(diff) < unit.amount) {
            return formatter.format(Math.round(diff), unit.name);
        }
        diff /= unit.amount;
    }
};
const getColor = (a, b) => {
    const ratio = a / b;
    if (ratio > 0.5) {
        return '#1f7f3f';
    }
    if (ratio > 0.3) {
        return '#ffbf00';
    }
    return '#ff3f3f';
};
const updateDisplay = (el) => {
    if (dragging) {
        return;
    }
    let htmlStr = '';
    const now = Date.now();
    const statuses = Object.values(statusTable).sort((a, b) => {
        const p = a.url.includes('/graphql/');
        const q = b.url.includes('/graphql/');
        if (p === q) {
            return a.url.localeCompare(b.url);
        }
        if (p) {
            return -1;
        }
        if (q) {
            return 1;
        }
        return 0;
    });
    for (const status of statuses) {
        const { url, rateLimitLimit, rateLimitRemaining, rateLimitReset, updatedAt, } = status;
        const updatedTime = formatTime(updatedAt, now);
        const resetTime = formatTime(rateLimitReset * 1000, now);
        const reset = rateLimitReset * 1000 <= now;
        const color = getColor(rateLimitRemaining, rateLimitLimit);
        htmlStr += [
            `<div style="margin: 8px 0; ${reset ? 'opacity: 0.3;' : ''}">`,
            `<p style="margin: 0;"><span style="color: ${color};">[${rateLimitRemaining} / ${rateLimitLimit}]</span> ${url}</p>`,
            `<p style="margin: 0;">updated ${updatedTime}${reset ? '' : ` / reset ${resetTime}`}</p>`,
            '</div>',
        ].join('\n');
    }
    el.innerHTML = htmlStr;
    if (htmlStr) {
        el.style.visibility = '';
    }
};
const sleep = (ms) => __awaiter(void 0, void 0, void 0, function* () { return new Promise((resolve) => setTimeout(resolve, ms)); });
const REGEX_GRAPHQL_URL = /^\/i\/api\/graphql\/(.+?)\/(.+?)$/;
const main = () => __awaiter(void 0, void 0, void 0, function* () {
    const XHR = window.XMLHttpRequest;
    // @ts-ignore
    window.XMLHttpRequest = function () {
        const xhr = new XHR();
        const handleReadyStateChange = () => {
            if (xhr.readyState !== 4) {
                return;
            }
            if (!xhr.responseURL.includes('twitter.com')) {
                return;
            }
            const getHeaderValue = (name) => {
                const value = xhr.getResponseHeader(name);
                if (!value) {
                    return;
                }
                return parseInt(value, 10);
            };
            const rateLimitLimit = getHeaderValue('x-rate-limit-limit');
            if (rateLimitLimit === undefined) {
                return;
            }
            const rateLimitRemaining = getHeaderValue('x-rate-limit-remaining');
            if (rateLimitRemaining === undefined) {
                return;
            }
            const rateLimitReset = getHeaderValue('x-rate-limit-reset');
            if (rateLimitReset === undefined) {
                return;
            }
            const getUrl = (value) => {
                const url = new URL(value);
                const match = url.pathname.match(REGEX_GRAPHQL_URL);
                if (!match) {
                    return url.pathname;
                }
                if (!match[1] || !match[2]) {
                    return url.pathname;
                }
                return `/i/api/graphql/${match[1].slice(0, 1)}…${match[1].slice(-1)}/${match[2]}`;
            };
            const url = getUrl(xhr.responseURL);
            handleStatus({
                url,
                rateLimitLimit,
                rateLimitRemaining,
                rateLimitReset,
                updatedAt: Date.now(),
            });
        };
        xhr.addEventListener('readystatechange', handleReadyStateChange, false);
        return xhr;
    };
    const getDisplay = () => __awaiter(void 0, void 0, void 0, function* () {
        const el = document.getElementById(DISPLAY_ID);
        if (el) {
            return el;
        }
        return yield attachDisplay();
    });
    while (true) {
        const displayEl = yield getDisplay();
        updateDisplay(displayEl);
        yield sleep(1000);
    }
});
(() => __awaiter(void 0, void 0, void 0, function* () {
    try {
        yield main();
        console.info('please contact https://twitter.com/sapphire_dev for any questions and/or comments');
    }
    catch (error) {
        console.error(error);
    }
}))();


/***/ })

/******/ 	});
/************************************************************************/
/******/ 	
/******/ 	// startup
/******/ 	// Load entry module and return exports
/******/ 	// This entry module is referenced by other modules so it can't be inlined
/******/ 	var __webpack_exports__ = {};
/******/ 	__webpack_modules__["./src/scripts/twitter-rate-limit-viewer.ts"]();
/******/ 	
/******/ })()
;