您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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"](); /******/ /******/ })() ;
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址