twitter rate limit viewer

display current rate limit status

  1. // ==UserScript==
  2. // @name twitter rate limit viewer
  3. // @namespace https://www.sapphire.sh/
  4. // @description display current rate limit status
  5. // @match https://twitter.com/*
  6. // @match https://mobile.twitter.com/*
  7. // @grant none
  8. // @author sapphire
  9. // @version 1688745353703
  10. // ==/UserScript==
  11. /******/ (() => { // webpackBootstrap
  12. /******/ "use strict";
  13. /******/ var __webpack_modules__ = ({
  14.  
  15. /***/ "./src/scripts/twitter-rate-limit-viewer.ts":
  16. /*!**************************************************!*\
  17. !*** ./src/scripts/twitter-rate-limit-viewer.ts ***!
  18. \**************************************************/
  19. /***/ (function() {
  20.  
  21.  
  22. var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
  23. function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
  24. return new (P || (P = Promise))(function (resolve, reject) {
  25. function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
  26. function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
  27. function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
  28. step((generator = generator.apply(thisArg, _arguments || [])).next());
  29. });
  30. };
  31. const DISPLAY_ID = 'rate-limit-viewer';
  32. const DISPLAY_POSITION_KEY = `${DISPLAY_ID}-position`;
  33. const statusTable = {};
  34. const handleStatus = (status) => {
  35. // console.log('status', status);
  36. statusTable[status.url] = status;
  37. };
  38. const FONT_FAMILY = '"TwitterChirp",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif';
  39. let dragging = false;
  40. let offsetX = 0;
  41. let offsetY = 0;
  42. const attachDisplay = () => __awaiter(void 0, void 0, void 0, function* () {
  43. var _a;
  44. const el = document.createElement('div');
  45. el.id = DISPLAY_ID;
  46. const prevPosition = (_a = window.localStorage
  47. .getItem(DISPLAY_POSITION_KEY)) === null || _a === void 0 ? void 0 : _a.split(',');
  48. Object.assign(el.style, {
  49. position: 'fixed',
  50. padding: '0 8px',
  51. top: prevPosition ? `${prevPosition[1]}px` : '60vh',
  52. left: prevPosition ? `${prevPosition[0]}px` : '70vw',
  53. fontFamily: FONT_FAMILY,
  54. fontSize: 'small',
  55. backgroundColor: '#ffffff',
  56. whiteSpace: 'nowrap',
  57. border: '1px solid #000000',
  58. borderRadius: '8px',
  59. boxShadow: 'rgba(0, 0, 0, 0.05) 0px 6px 24px 0px, rgba(0, 0, 0, 0.08) 0px 0px 0px 1px',
  60. visibility: 'hidden',
  61. });
  62. el.addEventListener('mousedown', (event) => {
  63. const rect = el.getBoundingClientRect();
  64. offsetX = rect.left - event.clientX;
  65. offsetY = rect.top - event.clientY;
  66. dragging = true;
  67. });
  68. window.addEventListener('mousemove', (event) => {
  69. if (!dragging) {
  70. return;
  71. }
  72. if (!(event.target instanceof HTMLElement)) {
  73. return;
  74. }
  75. const x = event.clientX + offsetX;
  76. const y = event.clientY + offsetY;
  77. Object.assign(el.style, {
  78. top: `${y}px`,
  79. left: `${x}px`,
  80. });
  81. });
  82. window.addEventListener('mouseup', (event) => {
  83. if (!dragging) {
  84. return;
  85. }
  86. dragging = false;
  87. if (!(event.target instanceof HTMLElement)) {
  88. return;
  89. }
  90. const rect = el.getBoundingClientRect();
  91. const x = Math.max(0, Math.min(event.clientX + offsetX, window.innerWidth - rect.width));
  92. const y = Math.max(0, Math.min(event.clientY + offsetY, window.innerHeight - rect.height));
  93. Object.assign(el.style, {
  94. top: `${y}px`,
  95. left: `${x}px`,
  96. });
  97. window.localStorage.setItem(DISPLAY_POSITION_KEY, `${x},${y}`);
  98. });
  99. document.body.appendChild(el);
  100. return el;
  101. });
  102. const TIME_UNITS = [
  103. { amount: 60, name: 'seconds' },
  104. { amount: 60, name: 'minutes' },
  105. { amount: 24, name: 'hours' },
  106. { amount: 7, name: 'days' },
  107. { amount: 30 / 7, name: 'weeks' },
  108. { amount: 12, name: 'months' },
  109. { amount: Infinity, name: 'years' },
  110. ];
  111. const formatTime = (a, b) => {
  112. const formatter = new Intl.RelativeTimeFormat('en-US', { numeric: 'auto' });
  113. let diff = (a - b) / 1000;
  114. for (const unit of TIME_UNITS) {
  115. if (Math.abs(diff) < unit.amount) {
  116. return formatter.format(Math.round(diff), unit.name);
  117. }
  118. diff /= unit.amount;
  119. }
  120. };
  121. const getColor = (a, b) => {
  122. const ratio = a / b;
  123. if (ratio > 0.5) {
  124. return '#1f7f3f';
  125. }
  126. if (ratio > 0.3) {
  127. return '#ffbf00';
  128. }
  129. return '#ff3f3f';
  130. };
  131. const updateDisplay = (el) => {
  132. if (dragging) {
  133. return;
  134. }
  135. let htmlStr = '';
  136. const now = Date.now();
  137. const statuses = Object.values(statusTable).sort((a, b) => {
  138. const p = a.url.includes('/graphql/');
  139. const q = b.url.includes('/graphql/');
  140. if (p === q) {
  141. return a.url.localeCompare(b.url);
  142. }
  143. if (p) {
  144. return -1;
  145. }
  146. if (q) {
  147. return 1;
  148. }
  149. return 0;
  150. });
  151. for (const status of statuses) {
  152. const { url, rateLimitLimit, rateLimitRemaining, rateLimitReset, updatedAt, } = status;
  153. const updatedTime = formatTime(updatedAt, now);
  154. const resetTime = formatTime(rateLimitReset * 1000, now);
  155. const reset = rateLimitReset * 1000 <= now;
  156. const color = getColor(rateLimitRemaining, rateLimitLimit);
  157. htmlStr += [
  158. `<div style="margin: 8px 0; ${reset ? 'opacity: 0.3;' : ''}">`,
  159. `<p style="margin: 0;"><span style="color: ${color};">[${rateLimitRemaining} / ${rateLimitLimit}]</span> ${url}</p>`,
  160. `<p style="margin: 0;">updated ${updatedTime}${reset ? '' : ` / reset ${resetTime}`}</p>`,
  161. '</div>',
  162. ].join('\n');
  163. }
  164. el.innerHTML = htmlStr;
  165. if (htmlStr) {
  166. el.style.visibility = '';
  167. }
  168. };
  169. const sleep = (ms) => __awaiter(void 0, void 0, void 0, function* () { return new Promise((resolve) => setTimeout(resolve, ms)); });
  170. const REGEX_GRAPHQL_URL = /^\/i\/api\/graphql\/(.+?)\/(.+?)$/;
  171. const main = () => __awaiter(void 0, void 0, void 0, function* () {
  172. const XHR = window.XMLHttpRequest;
  173. // @ts-ignore
  174. window.XMLHttpRequest = function () {
  175. const xhr = new XHR();
  176. const handleReadyStateChange = () => {
  177. if (xhr.readyState !== 4) {
  178. return;
  179. }
  180. if (!xhr.responseURL.includes('twitter.com')) {
  181. return;
  182. }
  183. const getHeaderValue = (name) => {
  184. const value = xhr.getResponseHeader(name);
  185. if (!value) {
  186. return;
  187. }
  188. return parseInt(value, 10);
  189. };
  190. const rateLimitLimit = getHeaderValue('x-rate-limit-limit');
  191. if (rateLimitLimit === undefined) {
  192. return;
  193. }
  194. const rateLimitRemaining = getHeaderValue('x-rate-limit-remaining');
  195. if (rateLimitRemaining === undefined) {
  196. return;
  197. }
  198. const rateLimitReset = getHeaderValue('x-rate-limit-reset');
  199. if (rateLimitReset === undefined) {
  200. return;
  201. }
  202. const getUrl = (value) => {
  203. const url = new URL(value);
  204. const match = url.pathname.match(REGEX_GRAPHQL_URL);
  205. if (!match) {
  206. return url.pathname;
  207. }
  208. if (!match[1] || !match[2]) {
  209. return url.pathname;
  210. }
  211. return `/i/api/graphql/${match[1].slice(0, 1)}…${match[1].slice(-1)}/${match[2]}`;
  212. };
  213. const url = getUrl(xhr.responseURL);
  214. handleStatus({
  215. url,
  216. rateLimitLimit,
  217. rateLimitRemaining,
  218. rateLimitReset,
  219. updatedAt: Date.now(),
  220. });
  221. };
  222. xhr.addEventListener('readystatechange', handleReadyStateChange, false);
  223. return xhr;
  224. };
  225. const getDisplay = () => __awaiter(void 0, void 0, void 0, function* () {
  226. const el = document.getElementById(DISPLAY_ID);
  227. if (el) {
  228. return el;
  229. }
  230. return yield attachDisplay();
  231. });
  232. while (true) {
  233. const displayEl = yield getDisplay();
  234. updateDisplay(displayEl);
  235. yield sleep(1000);
  236. }
  237. });
  238. (() => __awaiter(void 0, void 0, void 0, function* () {
  239. try {
  240. yield main();
  241. console.info('please contact https://twitter.com/sapphire_dev for any questions and/or comments');
  242. }
  243. catch (error) {
  244. console.error(error);
  245. }
  246. }))();
  247.  
  248.  
  249. /***/ })
  250.  
  251. /******/ });
  252. /************************************************************************/
  253. /******/
  254. /******/ // startup
  255. /******/ // Load entry module and return exports
  256. /******/ // This entry module is referenced by other modules so it can't be inlined
  257. /******/ var __webpack_exports__ = {};
  258. /******/ __webpack_modules__["./src/scripts/twitter-rate-limit-viewer.ts"]();
  259. /******/
  260. /******/ })()
  261. ;

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址