- // ==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"]();
- /******/
- /******/ })()
- ;