ChatGPT 服务降级监控

监控 ChatGPT 服务状态、IP 质量和 PoW 难度

  1. // ==UserScript==
  2. // @name ChatGPT Degraded
  3. // @name:zh-CN ChatGPT 服务降级监控
  4. // @name:zh-TW ChatGPT 服務降級監控
  5. // @namespace https://github.com/lroolle/chatgpt-degraded
  6. // @version 0.2.8
  7. // @description Monitor ChatGPT service level, IP quality and PoW difficulty
  8. // @description:zh-CN 监控 ChatGPT 服务状态、IP 质量和 PoW 难度
  9. // @description:zh-TW 監控 ChatGPT 服務狀態、IP 質量和 PoW 難度
  10. // @author lroolle
  11. // @license AGPL-3.0
  12. // @match *://chat.openai.com/*
  13. // @match *://chatgpt.com/*
  14. // @connect status.openai.com
  15. // @connect scamalytics.com
  16. // @grant GM_xmlhttpRequest
  17. // @grant unsafeWindow
  18. // @run-at document-start
  19. // @icon data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI2NCIgaGVpZ2h0PSI2NCIgdmlld0JveD0iMCAwIDY0IDY0Ij4KICA8ZGVmcz4KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iZ3JhZGllbnQiIHgxPSIwJSIgeTE9IjAlIiB4Mj0iMTAwJSIgeTI9IjEwMCUiPgogICAgICA8c3RvcCBvZmZzZXQ9IjAlIiBzdHlsZT0ic3RvcC1jb2xvcjojMmE5ZDhmO3N0b3Atb3BhY2l0eToxIi8+CiAgICAgIDxzdG9wIG9mZnNldD0iMTAwJSIgc3R5bGU9InN0b3AtY29sb3I6IzJhOWQ4ZjtzdG9wLW9wYWNpdHk6MC44Ii8+CiAgICA8L2xpbmVhckdyYWRpZW50PgogIDwvZGVmcz4KICA8Zz4KICAgIDxjaXJjbGUgY3g9IjMyIiBjeT0iMzIiIHI9IjI4IiBmaWxsPSJ1cmwoI2dyYWRpZW50KSIgc3Ryb2tlPSIjZmZmIiBzdHJva2Utd2lkdGg9IjEiLz4KPCEtLU91dGVyIGNpcmNsZSBtb2RpZmllZCB0byBsb29rIGxpa2UgIkMiLS0+CiAgICA8Y2lyY2xlIGN4PSIzMiIgY3k9IjMyIiByPSIyMCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZmZmIiBzdHJva2Utd2lkdGg9IjEiIHN0cm9rZS1kYXNoYXJyYXk9IjEyNSA1NSIgc3Ryb2tlLWRhc2hvZmZzZXQ9IjIwIi8+CiAgICA8Y2lyY2xlIGN4PSIzMiIgY3k9IjMyIiByPSIxMiIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZmZmIiBzdHJva2Utd2lkdGg9IjEiLz4KICAgIDxjaXJjbGUgY3g9IjMyIiBjeT0iMzIiIHI9IjQiIGZpbGw9IiNmZmYiLz4KICA8L2c+Cjwvc3ZnPg==
  20. // @homepageURL https://github.com/lroolle/chatgpt-degraded
  21. // @supportURL https://github.com/lroolle/chatgpt-degraded/issues
  22. // ==/UserScript==
  23.  
  24. (function () {
  25. "use strict";
  26.  
  27. let displayBox, collapsedIndicator;
  28.  
  29. const i18n = {
  30. en: {
  31. service: "Service",
  32. ip: "IP",
  33. pow: "PoW",
  34. status: "Status",
  35. unknown: "Unknown",
  36. copyHistory: "Click to copy history",
  37. historyCopied: "History copied!",
  38. copyFailed: "Copy failed",
  39. riskLevels: {
  40. veryEasy: "Very Easy",
  41. easy: "Easy",
  42. medium: "Medium",
  43. hard: "Hard",
  44. critical: "Critical",
  45. },
  46. tooltips: {
  47. powDifficulty: "PoW Difficulty: Lower (green) means faster responses.",
  48. ipHistory: "IP History (recent 10):",
  49. warpPlus: "Protected by Cloudflare WARP+",
  50. warp: "Protected by Cloudflare WARP",
  51. clickToCopy: "Click to copy full history",
  52. },
  53. },
  54. "zh-CN": {
  55. service: "服务",
  56. ip: "IP",
  57. pow: "算力",
  58. status: "状态",
  59. unknown: "未知",
  60. copyHistory: "点击复制历史",
  61. historyCopied: "已复制历史!",
  62. copyFailed: "复制失败",
  63. riskLevels: {
  64. veryEasy: "非常容易",
  65. easy: "容易",
  66. medium: "中等",
  67. hard: "困难",
  68. critical: "严重",
  69. },
  70. tooltips: {
  71. powDifficulty: "PoW 难度:越低(绿色)响应越快",
  72. ipHistory: "IP 历史(最近10条):",
  73. warpPlus: "已启用 Cloudflare WARP+",
  74. warp: "已启用 Cloudflare WARP",
  75. clickToCopy: "点击复制完整历史",
  76. },
  77. },
  78. "zh-TW": {
  79. service: "服務",
  80. ip: "IP",
  81. pow: "算力",
  82. status: "狀態",
  83. unknown: "未知",
  84. copyHistory: "點擊複製歷史",
  85. historyCopied: "已複製歷史!",
  86. copyFailed: "複製失敗",
  87. riskLevels: {
  88. veryEasy: "非常容易",
  89. easy: "容易",
  90. medium: "中等",
  91. hard: "困難",
  92. critical: "嚴重",
  93. },
  94. tooltips: {
  95. powDifficulty: "PoW 難度:越低(綠色)回應越快",
  96. ipHistory: "IP 歷史(最近10筆):",
  97. warpPlus: "已啟用 Cloudflare WARP+",
  98. warp: "已啟用 Cloudflare WARP",
  99. clickToCopy: "點擊複製完整歷史",
  100. },
  101. },
  102. };
  103.  
  104. // Get user language
  105. const userLang = (navigator.language || "en").toLowerCase();
  106. const lang = i18n[userLang]
  107. ? userLang
  108. : userLang.startsWith("zh-tw")
  109. ? "zh-TW"
  110. : userLang.startsWith("zh")
  111. ? "zh-CN"
  112. : "en";
  113. const t = (key) => {
  114. const keys = key.split(".");
  115. return (
  116. keys.reduce((obj, k) => obj?.[k], i18n[lang]) ||
  117. i18n.en[keys[keys.length - 1]]
  118. );
  119. };
  120.  
  121. function updateUserType(type) {
  122. const userTypeElement = document.getElementById("user-type");
  123. if (!userTypeElement) return;
  124. const isPaid =
  125. type &&
  126. (type === "plus" ||
  127. type === "chatgpt-paid" ||
  128. type.includes("paid") ||
  129. type.includes("premium") ||
  130. type.includes("pro"));
  131. userTypeElement.textContent = isPaid ? "Paid" : "Free";
  132. userTypeElement.dataset.tooltip = `ChatGPT Account Type: ${isPaid ? "Paid" : "Free"}`;
  133. userTypeElement.style.color = isPaid
  134. ? "var(--success-color, #10a37f)"
  135. : "var(--text-primary, #374151)";
  136. }
  137.  
  138. function getRiskColorAndLevel(difficulty) {
  139. if (!difficulty || difficulty === "N/A") {
  140. return { color: "#e63946", level: "Unknown", percentage: 0 };
  141. }
  142. const cleanDifficulty = difficulty.replace(/^0x/, "").replace(/^0+/, "");
  143. const hexLength = cleanDifficulty.length;
  144. if (hexLength <= 2) {
  145. return { color: "#e63946", level: "Critical", percentage: 100 };
  146. } else if (hexLength <= 3) {
  147. return { color: "#FAB12F", level: "Hard", percentage: 75 };
  148. } else if (hexLength <= 4) {
  149. return { color: "#859F3D", level: "Medium", percentage: 50 };
  150. } else if (hexLength <= 5) {
  151. return { color: "#2a9d8f", level: "Easy", percentage: 25 };
  152. } else {
  153. return { color: "#4CAF50", level: "Very Easy", percentage: 0 };
  154. }
  155. }
  156.  
  157. function setProgressBar(bar, label, percentage, text, gradient, title) {
  158. bar.style.width = "100%";
  159. bar.style.background = gradient;
  160. bar.dataset.tooltip = title;
  161. label.innerText = text;
  162. }
  163.  
  164. function updateProgressBars(difficulty) {
  165. const powBar = document.getElementById("pow-bar");
  166. const powLevel = document.getElementById("pow-level");
  167. const difficultyElement = document.getElementById("difficulty");
  168. if (!powBar || !powLevel || !difficultyElement) return;
  169. const { color, level, percentage } = getRiskColorAndLevel(difficulty);
  170. const gradient = `linear-gradient(90deg, ${color} ${percentage}%, rgba(255, 255, 255, 0.1) ${percentage}%)`;
  171. setProgressBar(
  172. powBar,
  173. powLevel,
  174. percentage,
  175. level,
  176. gradient,
  177. "PoW Difficulty: Lower (green) means faster responses.",
  178. );
  179. difficultyElement.style.color = color;
  180. powLevel.style.color = color;
  181.  
  182. // Update icon animation based on difficulty level
  183. if (collapsedIndicator) {
  184. const outerRingAnim =
  185. collapsedIndicator.querySelector("#outer-ring-anim");
  186. const middleRingAnim =
  187. collapsedIndicator.querySelector("#middle-ring-anim");
  188. const centerDotAnim =
  189. collapsedIndicator.querySelector("#center-dot-anim");
  190. const gradientStops = collapsedIndicator.querySelector("#gradient");
  191.  
  192. // Adjust animation speed based on difficulty level
  193. const animationSpeed = percentage < 25 ? 0.5 : percentage / 25; // Make it more still when easy
  194. if (outerRingAnim)
  195. outerRingAnim.setAttribute("dur", `${8 / animationSpeed}s`);
  196. if (middleRingAnim)
  197. middleRingAnim.setAttribute("dur", `${4 / animationSpeed}s`);
  198. if (centerDotAnim) {
  199. centerDotAnim.setAttribute("dur", `${2 / animationSpeed}s`);
  200. // Smaller pulse for easy difficulty
  201. centerDotAnim.setAttribute(
  202. "values",
  203. percentage < 25 ? "4;4.5;4" : "4;5;4",
  204. );
  205. }
  206.  
  207. // Update color
  208. if (gradientStops) {
  209. gradientStops.innerHTML = `
  210. <stop offset="0%" style="stop-color:${color};stop-opacity:1" />
  211. <stop offset="100%" style="stop-color:${color};stop-opacity:0.8" />
  212. `;
  213. }
  214. }
  215. }
  216.  
  217. const originalFetch = unsafeWindow.fetch;
  218. unsafeWindow.fetch = async function (resource, options) {
  219. const response = await originalFetch(resource, options);
  220. const url = typeof resource === "string" ? resource : resource?.url;
  221. // console.log("Fetch URL:", url);
  222. // console.log("Fetch options:", options);
  223.  
  224. const isChatRequirements =
  225. url &&
  226. (url.includes("/backend-api/sentinel/chat-requirements") ||
  227. url.includes("/backend-anon/sentinel/chat-requirements") ||
  228. url.includes("/api/sentinel/chat-requirements"));
  229.  
  230. // const method = options?.method?.toUpperCase() || "GET";
  231. // console.log("Method:", method, "isChatRequirements URL match:", isChatRequirements);
  232.  
  233. // Check if this is a chat requirements request (regardless of method for now)
  234. if (isChatRequirements) {
  235. // console.log("Processing chat requirements request with method:", method);
  236. try {
  237. const clonedResponse = response.clone();
  238. const data = await clonedResponse.json();
  239. // console.log("Chat requirements response data:", data);
  240. const difficulty = data?.proofofwork?.difficulty;
  241. const userType = data?.persona || data?.user_type || data?.account_type;
  242. const difficultyElement = document.getElementById("difficulty");
  243. if (difficultyElement) {
  244. if (difficulty) {
  245. difficultyElement.innerText = difficulty;
  246. difficultyElement.dataset.tooltip = `Raw Difficulty Value: ${difficulty}`;
  247. // Update IP log with new PoW difficulty
  248. const ipElement = document.getElementById("ip-address");
  249. if (ipElement) {
  250. const fullIP = ipElement.dataset.fullIp;
  251. const ipQualityElement = document.getElementById("ip-quality");
  252. const score = ipQualityElement
  253. ? parseInt(ipQualityElement.dataset.score)
  254. : null;
  255. if (fullIP) {
  256. const logs = addIPLog(fullIP, score, difficulty);
  257. const formattedLogs = formatIPLogs(logs);
  258. const ipContainerTooltip = [
  259. "IP History (recent 10):",
  260. formattedLogs,
  261. "\n---",
  262. "Click to copy history",
  263. ].join("\n");
  264. ipElement.dataset.tooltip = ipContainerTooltip;
  265. }
  266. }
  267. } else {
  268. difficultyElement.innerText = "N/A";
  269. difficultyElement.dataset.tooltip = "No difficulty value found";
  270. }
  271. }
  272. updateUserType(userType || "free");
  273. updateProgressBars(difficulty || "N/A");
  274. } catch (error) {
  275. console.error("Error processing chat requirements:", error);
  276. const difficultyElement = document.getElementById("difficulty");
  277. if (difficultyElement) {
  278. difficultyElement.innerText = "N/A";
  279. difficultyElement.dataset.tooltip = `Error: ${error.message}`;
  280. }
  281. updateUserType("free");
  282. updateProgressBars("N/A");
  283. }
  284. }
  285. return response;
  286. };
  287.  
  288. function initUI() {
  289. displayBox = document.createElement("div");
  290. displayBox.style.position = "fixed";
  291. displayBox.style.bottom = "10px";
  292. displayBox.style.right = "55px";
  293. displayBox.style.width = "360px";
  294. displayBox.style.padding = "24px";
  295. displayBox.style.backgroundColor =
  296. "var(--surface-primary, rgb(255, 255, 255))";
  297. displayBox.style.color = "var(--text-primary, #374151)";
  298. displayBox.style.fontSize = "14px";
  299. displayBox.style.borderRadius = "16px";
  300. displayBox.style.boxShadow = "0 4px 24px rgba(0, 0, 0, 0.08)";
  301. displayBox.style.zIndex = "10000";
  302. displayBox.style.transition = "opacity 0.15s ease, transform 0.15s ease";
  303. displayBox.style.display = "none";
  304. displayBox.style.opacity = "0";
  305. displayBox.style.transform = "translateX(10px)";
  306. displayBox.style.border =
  307. "1px solid var(--border-light, rgba(0, 0, 0, 0.05))";
  308.  
  309. displayBox.innerHTML = `
  310. <div id="content">
  311. <div class="monitor-item">
  312. <div class="monitor-row">
  313. <span class="label">${t("service")}</span>
  314. <span id="user-type" class="value" data-tooltip="ChatGPT Account Type"></span>
  315. </div>
  316. </div>
  317.  
  318. <!-- Proof of Work Difficulty -->
  319. <div class="monitor-item">
  320. <div class="monitor-row">
  321. <span class="label">${t("pow")}</span>
  322. <div class="pow-container">
  323. <span id="difficulty" class="value monospace" data-tooltip="PoW Difficulty Value"></span>
  324. <span id="pow-level" class="value-tag" data-tooltip="Difficulty Level"></span>
  325. </div>
  326. </div>
  327. <div class="progress-wrapper" data-tooltip="${t("tooltips.powDifficulty")}">
  328. <div class="progress-container">
  329. <div id="pow-bar" class="progress-bar"></div>
  330. </div>
  331. <div class="progress-background"></div>
  332. </div>
  333. </div>
  334.  
  335. <!-- IP + IP Quality -->
  336. <div class="monitor-item">
  337. <div class="monitor-row">
  338. <span class="label">${t("ip")}</span>
  339. <div class="ip-container">
  340. <span id="ip-address" class="value monospace" data-tooltip="Click to copy IP address"></span>
  341. <span id="warp-badge" class="warp-badge"></span>
  342. <span id="ip-quality" class="value-tag" data-tooltip="IP Risk Info (Scamlytics)"></span>
  343. </div>
  344. </div>
  345. </div>
  346.  
  347. <!-- OpenAI System Status -->
  348. <div class="monitor-item">
  349. <div class="monitor-row">
  350. <span class="label">${t("status")}</span>
  351. <a id="status-description"
  352. href="https://status.openai.com"
  353. target="_blank"
  354. class="value"
  355. data-tooltip="Click to open status.openai.com">
  356. ${t("unknown")}
  357. </a>
  358. </div>
  359. </div>
  360. </div>
  361.  
  362. <style>
  363. :root {
  364. --warning-color: #FAB12F;
  365. --warning-background: rgba(251, 177, 47, 0.1);
  366. --error-color: #e63946;
  367. --error-background: rgba(230, 57, 70, 0.1);
  368. }
  369. .monitor-item {
  370. margin-bottom: 16px;
  371. }
  372. .monitor-item:last-child {
  373. margin-bottom: 0;
  374. }
  375. .monitor-row {
  376. display: flex;
  377. align-items: center;
  378. gap: 6px;
  379. margin-bottom: 6px;
  380. }
  381. .monitor-row:last-child {
  382. margin-bottom: 4px;
  383. }
  384. .label {
  385. font-size: 14px;
  386. color: var(--text-secondary, #6B7280);
  387. flex-shrink: 0;
  388. min-width: 40px;
  389. }
  390. .value {
  391. font-size: 14px;
  392. color: var(--text-primary, #374151);
  393. flex: 1;
  394. }
  395. .monospace {
  396. font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
  397. font-size: 14px;
  398. }
  399. .value-tag {
  400. font-size: 14px;
  401. color: var(--success-color, #10a37f);
  402. white-space: nowrap;
  403. font-weight: 500;
  404. transition: opacity 0.15s ease;
  405. cursor: pointer;
  406. display: inline-block;
  407. }
  408. .value-tag:hover {
  409. opacity: 0.8;
  410. }
  411. .progress-wrapper {
  412. position: relative;
  413. margin-left: 40px;
  414. margin-top: 4px;
  415. }
  416. .progress-container {
  417. position: relative;
  418. height: 4px;
  419. background: transparent;
  420. border-radius: 2px;
  421. overflow: hidden;
  422. z-index: 1;
  423. }
  424. .progress-background {
  425. position: absolute;
  426. top: 0;
  427. left: 0;
  428. right: 0;
  429. height: 4px;
  430. background: var(--surface-secondary, rgba(0, 0, 0, 0.08));
  431. border-radius: 2px;
  432. }
  433. .progress-bar {
  434. height: 100%;
  435. width: 0%;
  436. transition: all 0.3s ease;
  437. background: var(--success-color, #10a37f);
  438. }
  439. #status-description {
  440. text-decoration: none;
  441. color: inherit;
  442. }
  443. #status-description:hover {
  444. text-decoration: underline;
  445. }
  446. #ip-address {
  447. cursor: pointer;
  448. }
  449. #ip-address:hover {
  450. opacity: 0.7;
  451. }
  452. #user-type {
  453. font-weight: 500;
  454. }
  455. .ip-container,
  456. .pow-container {
  457. display: flex;
  458. align-items: center;
  459. gap: 6px;
  460. flex: 1;
  461. }
  462. /* Ensure IP risk level (ip-quality) is right-aligned, just like pow-level */
  463. #ip-quality {
  464. margin-left: auto;
  465. }
  466. .warp-badge {
  467. font-size: 12px;
  468. color: var(--success-color, #10a37f);
  469. background-color: var(--surface-secondary, rgba(16, 163, 127, 0.1));
  470. padding: 2px 4px;
  471. border-radius: 4px;
  472. font-weight: 500;
  473. cursor: help;
  474. display: none;
  475. transition: background-color 0.2s ease, color 0.2s ease;
  476. }
  477. .warp-badge:hover {
  478. opacity: 0.8;
  479. }
  480. .ip-container .value-tag {
  481. padding-right: 0;
  482. position: relative;
  483. }
  484. /* Special handling for IP Risk tooltip */
  485. .ip-container .value-tag[data-tooltip]::after {
  486. left: auto;
  487. right: 0;
  488. transform: translateY(4px);
  489. }
  490. .ip-container .value-tag[data-tooltip]:hover::after {
  491. transform: translateY(0);
  492. left: auto;
  493. right: 0;
  494. }
  495. /* General tooltip styles */
  496. [data-tooltip] {
  497. position: relative;
  498. cursor: help;
  499. }
  500. [data-tooltip]::after {
  501. content: attr(data-tooltip);
  502. position: absolute;
  503. bottom: 100%;
  504. left: 50%;
  505. transform: translateX(-50%) translateY(4px);
  506. background: var(--surface-primary, rgba(0, 0, 0, 0.8));
  507. color: #fff;
  508. padding: 12px 16px;
  509. border-radius: 6px;
  510. font-size: 12px;
  511. white-space: pre-line;
  512. width: max-content;
  513. max-width: 600px;
  514. min-width: 450px;
  515. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
  516. z-index: 1000;
  517. pointer-events: none;
  518. margin-bottom: 8px;
  519. opacity: 0;
  520. transition: opacity 0.15s ease, transform 0.15s ease;
  521. font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
  522. }
  523. [data-tooltip]:hover::after {
  524. opacity: 1;
  525. transform: translateX(-50%) translateY(0);
  526. }
  527. /* Arrow styles */
  528. [data-tooltip]::before {
  529. content: '';
  530. position: absolute;
  531. bottom: 100%;
  532. left: 50%;
  533. transform: translateX(-50%) translateY(4px);
  534. border: 6px solid transparent;
  535. border-top-color: var(--surface-primary, rgba(0, 0, 0, 0.8));
  536. margin-bottom: -4px;
  537. pointer-events: none;
  538. opacity: 0;
  539. transition: opacity 0.15s ease, transform 0.15s ease;
  540. }
  541. [data-tooltip]:hover::before {
  542. opacity: 1;
  543. transform: translateY(0);
  544. }
  545. /* Special handling for IP Risk tooltip arrow */
  546. .ip-container .value-tag[data-tooltip]::before {
  547. left: auto;
  548. right: 12px;
  549. transform: translateY(4px);
  550. }
  551. .ip-container .value-tag[data-tooltip]:hover::before {
  552. transform: translateY(0);
  553. left: auto;
  554. right: 12px;
  555. }
  556. /* Ensure tooltips don't get cut off at viewport edges */
  557. @media screen and (max-width: 768px) {
  558. [data-tooltip]::after {
  559. min-width: 300px;
  560. max-width: calc(100vw - 48px);
  561. }
  562. }
  563. </style>
  564. `;
  565. document.body.appendChild(displayBox);
  566.  
  567. collapsedIndicator = document.createElement("div");
  568. collapsedIndicator.style.position = "fixed";
  569. collapsedIndicator.style.bottom = "10px";
  570. collapsedIndicator.style.right = "15px";
  571. collapsedIndicator.style.width = "24px";
  572. collapsedIndicator.style.height = "24px";
  573. collapsedIndicator.style.backgroundColor = "transparent";
  574. collapsedIndicator.style.border =
  575. "1px solid var(--token-border-light, rgba(0, 0, 0, 0.1))";
  576. collapsedIndicator.style.borderRadius = "50%";
  577. collapsedIndicator.style.cursor = "pointer";
  578. collapsedIndicator.style.zIndex = "10000";
  579. collapsedIndicator.style.display = "flex";
  580. collapsedIndicator.style.alignItems = "center";
  581. collapsedIndicator.style.justifyContent = "center";
  582. collapsedIndicator.style.transition = "all 0.3s ease";
  583.  
  584. collapsedIndicator.innerHTML = `
  585. <svg width="24" height="24" viewBox="0 0 64 64">
  586. <defs>
  587. <linearGradient id="gradient" x1="0%" y1="0%" x2="100%" y2="100%">
  588. <stop offset="0%" style="stop-color:#666;stop-opacity:1" />
  589. <stop offset="100%" style="stop-color:#666;stop-opacity:0.8" />
  590. </linearGradient>
  591. <filter id="glow">
  592. <feGaussianBlur stdDeviation="1" result="coloredBlur"/>
  593. <feMerge>
  594. <feMergeNode in="coloredBlur"/>
  595. <feMergeNode in="SourceGraphic"/>
  596. </feMerge>
  597. </filter>
  598. </defs>
  599. <g id="icon-group" filter="url(#glow)" transform="rotate(165, 32, 32)">
  600. <circle cx="32" cy="32" r="28" fill="url(#gradient)" stroke="#fff" stroke-width="1"/>
  601. <circle cx="32" cy="32" r="20" fill="none" stroke="#fff" stroke-width="1"
  602. stroke-dasharray="80 40" transform="rotate(-90, 32, 32)">
  603. <animate attributeName="stroke-dashoffset"
  604. dur="4s"
  605. values="0;120"
  606. repeatCount="indefinite"
  607. id="outer-ring-anim"/>
  608. </circle>
  609. <circle cx="32" cy="32" r="12" fill="none" stroke="#fff" stroke-width="1">
  610. <animate attributeName="r"
  611. dur="2s"
  612. values="12;14;12"
  613. repeatCount="indefinite"
  614. id="middle-ring-anim"/>
  615. </circle>
  616. <circle id="center-dot" cx="32" cy="32" r="4" fill="#fff">
  617. <animate attributeName="r"
  618. dur="1s"
  619. values="4;5;4"
  620. repeatCount="indefinite"
  621. id="center-dot-anim"/>
  622. </circle>
  623. </g>
  624. </svg>
  625. `;
  626. document.body.appendChild(collapsedIndicator);
  627.  
  628. collapsedIndicator.addEventListener("mouseenter", () => {
  629. displayBox.style.display = "block";
  630. requestAnimationFrame(() => {
  631. displayBox.style.opacity = "1";
  632. displayBox.style.transform = "translateX(0)";
  633. });
  634. });
  635.  
  636. displayBox.addEventListener("mouseleave", () => {
  637. displayBox.style.opacity = "0";
  638. displayBox.style.transform = "translateX(10px)";
  639. setTimeout(() => {
  640. displayBox.style.display = "none";
  641. }, 150);
  642. });
  643.  
  644. const observer = new MutationObserver(updateTheme);
  645. observer.observe(document.documentElement, {
  646. attributes: true,
  647. attributeFilter: ["class"],
  648. });
  649.  
  650. fetchIPInfo();
  651. fetchChatGPTStatus();
  652. updateTheme();
  653. const statusCheckInterval = 60 * 60 * 1000;
  654. let statusCheckTimer = setInterval(fetchChatGPTStatus, statusCheckInterval);
  655.  
  656. document.addEventListener("visibilitychange", () => {
  657. if (document.visibilityState === "visible") {
  658. clearInterval(statusCheckTimer);
  659. fetchChatGPTStatus();
  660. statusCheckTimer = setInterval(fetchChatGPTStatus, statusCheckInterval);
  661. }
  662. });
  663. }
  664.  
  665. if (document.readyState !== "loading") {
  666. initUI();
  667. } else {
  668. document.addEventListener("DOMContentLoaded", initUI);
  669. }
  670.  
  671. function maskIP(ip) {
  672. if (!ip || ip === "Unknown") return ip;
  673. if (ip.includes(".")) {
  674. const parts = ip.split(".");
  675. if (parts.length === 4) {
  676. return `${parts[0]}.*.*.${parts[3]}`;
  677. }
  678. }
  679. if (ip.includes(":")) {
  680. const parts = ip.split(":");
  681. // Shorten IPv6 to just show first and last part
  682. if (parts.length > 2) {
  683. return `${parts[0]}:*:${parts[parts.length - 1]}`;
  684. }
  685. }
  686. return ip;
  687. }
  688.  
  689. async function fetchIPQuality(ip) {
  690. try {
  691. const response = await new Promise((resolve, reject) => {
  692. GM_xmlhttpRequest({
  693. method: "GET",
  694. url: `https://scamalytics.com/ip/${ip}`,
  695. timeout: 3000,
  696. onload: (r) =>
  697. r.status === 200
  698. ? resolve(r.responseText)
  699. : reject(new Error(`HTTP ${r.status}`)),
  700. onerror: reject,
  701. ontimeout: () => reject(new Error("Request timed out")),
  702. });
  703. });
  704. console.log("fetchIPQuality.response", response);
  705. const parser = new DOMParser();
  706. const doc = parser.parseFromString(response, "text/html");
  707. const scoreElement = doc.querySelector(".score_bar .score");
  708. const scoreMatch =
  709. scoreElement?.textContent.match(/Fraud Score:\s*(\d+)/i);
  710. if (!scoreMatch) {
  711. return {
  712. label: "Unknown",
  713. color: "#aaa",
  714. tooltip: "Could not determine IP quality",
  715. score: null,
  716. };
  717. }
  718. const score = parseInt(scoreMatch[1], 10);
  719. const riskElement = doc.querySelector(".panel_title");
  720. const riskText = riskElement?.textContent.trim() || "Unknown Risk";
  721. const panelColor = riskElement?.style.backgroundColor || "#aaa";
  722. const descriptionElement = doc.querySelector(".panel_body");
  723. const description = descriptionElement?.textContent.trim() || "";
  724. const trimmedDescription =
  725. description.length > 150
  726. ? `${description.substring(0, 147)}...`
  727. : description;
  728.  
  729. function extractTableValue(header) {
  730. const row = Array.from(doc.querySelectorAll("th")).find(
  731. (th) => th.textContent.trim() === header,
  732. )?.parentElement;
  733. return row?.querySelector("td")?.textContent.trim() || null;
  734. }
  735. function isRiskYes(header) {
  736. const row = Array.from(doc.querySelectorAll("th")).find(
  737. (th) => th.textContent.trim() === header,
  738. )?.parentElement;
  739. return row?.querySelector(".risk.yes") !== null;
  740. }
  741. const details = {
  742. location: extractTableValue("City") || "Unknown",
  743. state: extractTableValue("State / Province"),
  744. country: extractTableValue("Country Name"),
  745. isp: extractTableValue("ISP Name") || "Unknown",
  746. organization: extractTableValue("Organization Name"),
  747. isVPN: isRiskYes("Anonymizing VPN"),
  748. isTor: isRiskYes("Tor Exit Node"),
  749. isServer: isRiskYes("Server"),
  750. isProxy:
  751. isRiskYes("Public Proxy") ||
  752. isRiskYes("Web Proxy") ||
  753. isRiskYes("Proxy"),
  754. };
  755. let label, color;
  756. if (riskText && riskText !== "Unknown Risk") {
  757. label = riskText;
  758. color = panelColor !== "#aaa" ? panelColor : getColorForScore(score);
  759. } else {
  760. ({ label, color } = getLabelAndColorForScore(score));
  761. }
  762. const warnings = [];
  763. if (details.isVPN) warnings.push("VPN");
  764. if (details.isTor) warnings.push("Tor");
  765. if (details.isServer) warnings.push("Server");
  766. if (details.isProxy) warnings.push("Proxy");
  767. const location = [details.location, details.state, details.country]
  768. .filter(Boolean)
  769. .join(", ");
  770. const tooltip = [
  771. "IP Risk Info (Scamlytics):",
  772. label !== "Unknown" ? `Risk: ${label} (${score}/100)` : "",
  773. `Location: ${location}`,
  774. `ISP: ${details.isp}${details.organization ? ` (${details.organization})` : ""}`,
  775. warnings.length ? `Warnings: ${warnings.join(", ")}` : "",
  776. trimmedDescription ? `\n${trimmedDescription}` : "",
  777. "\nClick to view full analysis",
  778. ]
  779. .filter(Boolean)
  780. .join("\n");
  781. return { label, color, tooltip, score };
  782. } catch (error) {
  783. return {
  784. label: "Unknown",
  785. color: "#aaa",
  786. tooltip: "Could not check IP quality",
  787. score: null,
  788. };
  789. }
  790. }
  791.  
  792. function getColorForScore(score) {
  793. if (score < 25) return "#4CAF50";
  794. if (score < 50) return "#859F3D";
  795. if (score < 75) return "#FAB12F";
  796. return "#e63946";
  797. }
  798.  
  799. function getLabelAndColorForScore(score) {
  800. if (score < 25)
  801. return { label: t("riskLevels.veryEasy"), color: "#4CAF50" };
  802. if (score < 50) return { label: t("riskLevels.easy"), color: "#859F3D" };
  803. if (score < 75) return { label: t("riskLevels.medium"), color: "#FAB12F" };
  804. return { label: t("riskLevels.critical"), color: "#e63946" };
  805. }
  806.  
  807. function getIPLogs() {
  808. try {
  809. const logs = localStorage.getItem("chatgpt_ip_logs");
  810. return logs ? JSON.parse(logs) : [];
  811. } catch (error) {
  812. console.error("Error reading IP logs:", error);
  813. return [];
  814. }
  815. }
  816.  
  817. function addIPLog(ip, score, difficulty) {
  818. try {
  819. const logs = getIPLogs();
  820. const timestamp = new Date().toISOString();
  821. const newLog = { timestamp, ip, score, difficulty };
  822. if (logs.length > 0 && logs[0].ip === ip) {
  823. logs[0] = newLog;
  824. } else {
  825. logs.unshift(newLog);
  826. }
  827. const trimmedLogs = logs.slice(0, 10);
  828. localStorage.setItem("chatgpt_ip_logs", JSON.stringify(trimmedLogs));
  829. return trimmedLogs;
  830. } catch (error) {
  831. console.error("Error adding IP log:", error);
  832. return [];
  833. }
  834. }
  835.  
  836. function formatIPLogs(logs) {
  837. return logs
  838. .map((log) => {
  839. const date = new Date(log.timestamp);
  840. const formattedDate = date
  841. .toLocaleString("en-US", {
  842. year: "numeric",
  843. month: "2-digit",
  844. day: "2-digit",
  845. hour: "2-digit",
  846. minute: "2-digit",
  847. hour12: false,
  848. })
  849. .replace(/(\d+)\/(\d+)\/(\d+),\s(\d+):(\d+)/, "[$3-$1-$2 $4:$5]");
  850. const { color: powColor, level: powLevel } = getRiskColorAndLevel(
  851. log.difficulty,
  852. );
  853. const scoreDisplay =
  854. log.score !== null && log.score !== undefined ? log.score : "N/A";
  855. return `${formattedDate} ${log.ip}(${scoreDisplay}), ${log.difficulty || "N/A"}(${powLevel})`;
  856. })
  857. .join("\n");
  858. }
  859.  
  860. async function fetchIPInfo() {
  861. const fallbackServices = [
  862. {
  863. url: "https://chatgpt.com/cdn-cgi/trace",
  864. parser: (text) => {
  865. const data = text.split("\n").reduce((obj, line) => {
  866. const [key, value] = line.split("=");
  867. if (key && value) obj[key.trim()] = value.trim();
  868. return obj;
  869. }, {});
  870. return {
  871. ip: data.ip,
  872. warp: data.warp || "off",
  873. location: data.loc,
  874. colo: data.colo,
  875. };
  876. },
  877. },
  878. {
  879. url: "https://www.cloudflare.com/cdn-cgi/trace",
  880. parser: (text) => {
  881. const data = text.split("\n").reduce((obj, line) => {
  882. const [key, value] = line.split("=");
  883. if (key && value) obj[key.trim()] = value.trim();
  884. return obj;
  885. }, {});
  886. return {
  887. ip: data.ip,
  888. warp: data.warp || "off",
  889. location: data.loc,
  890. colo: data.colo,
  891. };
  892. },
  893. },
  894. {
  895. url: "https://ipinfo.io/json",
  896. parser: (text) => {
  897. const data = JSON.parse(text);
  898. return {
  899. ip: data.ip,
  900. warp: "off", // ipinfo.io doesn't provide WARP status
  901. location: data.loc,
  902. city: data.city,
  903. country: data.country,
  904. };
  905. },
  906. },
  907. ];
  908.  
  909. let lastError = null;
  910.  
  911. for (let i = 0; i < fallbackServices.length; i++) {
  912. const service = fallbackServices[i];
  913. try {
  914. console.log(
  915. `Attempting to fetch IP info from service ${i + 1}:`,
  916. service.url,
  917. );
  918.  
  919. const response = await new Promise((resolve, reject) => {
  920. GM_xmlhttpRequest({
  921. method: "GET",
  922. url: service.url,
  923. timeout: 5000,
  924. onload: (r) => {
  925. if (r.status === 200) {
  926. resolve(r.responseText);
  927. } else {
  928. reject(new Error(`HTTP ${r.status}: ${r.statusText}`));
  929. }
  930. },
  931. onerror: (err) =>
  932. reject(
  933. new Error(`Network error: ${err.message || "Unknown error"}`),
  934. ),
  935. ontimeout: () => reject(new Error("Request timed out")),
  936. });
  937. });
  938.  
  939. console.log(`Service ${i + 1} response:`, response);
  940. const data = service.parser(response);
  941. console.log(`Parsed data from service ${i + 1}:`, data);
  942.  
  943. if (!data.ip) {
  944. throw new Error("No IP address found in response");
  945. }
  946.  
  947. await updateIPDisplay(data);
  948. return; // Success, exit function
  949. } catch (error) {
  950. console.error(`Service ${i + 1} failed:`, error.message);
  951. lastError = error;
  952.  
  953. // If not the last service, wait a bit before trying next
  954. if (i < fallbackServices.length - 1) {
  955. await new Promise((resolve) => setTimeout(resolve, 1000));
  956. }
  957. }
  958. }
  959.  
  960. // All services failed
  961. console.error("All IP services failed. Last error:", lastError);
  962. await handleIPFetchFailure(lastError);
  963. }
  964.  
  965. async function updateIPDisplay(data) {
  966. const ipElement = document.getElementById("ip-address");
  967. const warpBadge = document.getElementById("warp-badge");
  968. const ipQualityElement = document.getElementById("ip-quality");
  969.  
  970. if (!ipElement || !warpBadge || !ipQualityElement) {
  971. throw new Error("IP display elements not found");
  972. }
  973.  
  974. const maskedIP = maskIP(data.ip);
  975. const fullIP = data.ip;
  976. const warpStatus = data.warp || "off";
  977.  
  978. ipElement.innerText = maskedIP;
  979. ipElement.dataset.fullIp = fullIP;
  980.  
  981. // Handle WARP display and warnings
  982. if (warpStatus === "on" || warpStatus === "plus") {
  983. warpBadge.style.display = "inline-flex";
  984. warpBadge.innerText = warpStatus === "plus" ? "warp+" : "warp";
  985. warpBadge.dataset.tooltip = t(
  986. `tooltips.${warpStatus === "plus" ? "warpPlus" : "warp"}`,
  987. );
  988. warpBadge.style.backgroundColor =
  989. "var(--success-color, rgba(16, 163, 127, 0.1))";
  990. warpBadge.style.color = "var(--success-color, #10a37f)";
  991. } else {
  992. // Show warning when WARP is not enabled
  993. warpBadge.style.display = "inline-flex";
  994. warpBadge.innerText = "no warp";
  995. warpBadge.dataset.tooltip =
  996. "⚠️ WARP not enabled - Consider enabling Cloudflare WARP for better privacy and potentially improved IP quality";
  997. warpBadge.style.backgroundColor =
  998. "var(--warning-background, rgba(251, 177, 47, 0.1))";
  999. warpBadge.style.color = "var(--warning-color, #FAB12F)";
  1000. }
  1001.  
  1002. // Fetch IP quality
  1003. try {
  1004. const { label, color, tooltip, score } = await fetchIPQuality(fullIP);
  1005. // console.log("IP Quality result:", { label, color, tooltip, score });
  1006.  
  1007. ipElement.style.color = color;
  1008. ipQualityElement.innerText =
  1009. score !== null ? `${label} (${score})` : label;
  1010. ipQualityElement.style.color = color;
  1011. ipQualityElement.dataset.score = score;
  1012. ipQualityElement.dataset.tooltip = tooltip;
  1013.  
  1014. // Update IP logs
  1015. const difficultyElement = document.getElementById("difficulty");
  1016. const currentDifficulty = difficultyElement?.innerText || "N/A";
  1017. const logs = addIPLog(fullIP, score, currentDifficulty);
  1018. const formattedLogs = formatIPLogs(logs);
  1019. const ipContainerTooltip = [
  1020. t("tooltips.ipHistory"),
  1021. formattedLogs,
  1022. "\n---",
  1023. t("tooltips.clickToCopy"),
  1024. ].join("\n");
  1025. ipElement.dataset.tooltip = ipContainerTooltip;
  1026.  
  1027. // Add click handlers
  1028. ipQualityElement.onclick = () =>
  1029. window.open(`https://scamalytics.com/ip/${fullIP}`, "_blank");
  1030.  
  1031. const copyHandler = async () => {
  1032. try {
  1033. const logs = getIPLogs();
  1034. const formattedHistory = formatIPLogs(logs);
  1035. await navigator.clipboard.writeText(formattedHistory);
  1036. const originalText = ipElement.innerText;
  1037. ipElement.innerText = t("historyCopied");
  1038. setTimeout(() => {
  1039. ipElement.innerText = originalText;
  1040. }, 1000);
  1041. } catch (err) {
  1042. console.error("Copy failed:", err);
  1043. const originalText = ipElement.innerText;
  1044. ipElement.innerText = t("copyFailed");
  1045. setTimeout(() => {
  1046. ipElement.innerText = originalText;
  1047. }, 1000);
  1048. }
  1049. };
  1050.  
  1051. ipElement.removeEventListener("click", copyHandler);
  1052. ipElement.addEventListener("click", copyHandler);
  1053. } catch (qualityError) {
  1054. console.error("Failed to fetch IP quality:", qualityError);
  1055. ipQualityElement.innerText = "Quality check failed";
  1056. ipQualityElement.style.color = "#aaa";
  1057. ipQualityElement.dataset.tooltip = `Could not check IP quality: ${qualityError.message}`;
  1058. }
  1059. }
  1060.  
  1061. async function handleIPFetchFailure(error) {
  1062. console.error("All IP fetch attempts failed:", error);
  1063.  
  1064. const ipElement = document.getElementById("ip-address");
  1065. const warpBadge = document.getElementById("warp-badge");
  1066. const ipQualityElement = document.getElementById("ip-quality");
  1067.  
  1068. if (ipElement) {
  1069. ipElement.innerText = "Failed to fetch";
  1070. ipElement.style.color = "#e63946";
  1071. ipElement.dataset.tooltip = `IP fetch failed: ${error?.message || "Unknown error"}\nTry refreshing the page`;
  1072. }
  1073.  
  1074. if (warpBadge) {
  1075. warpBadge.style.display = "inline-flex";
  1076. warpBadge.innerText = "error";
  1077. warpBadge.dataset.tooltip =
  1078. "Could not determine WARP status due to network error";
  1079. warpBadge.style.backgroundColor =
  1080. "var(--error-background, rgba(230, 57, 70, 0.1))";
  1081. warpBadge.style.color = "var(--error-color, #e63946)";
  1082. }
  1083.  
  1084. if (ipQualityElement) {
  1085. ipQualityElement.innerText = "Network Error";
  1086. ipQualityElement.style.color = "#e63946";
  1087. ipQualityElement.dataset.tooltip =
  1088. "Could not check IP quality due to network error";
  1089. }
  1090. }
  1091.  
  1092. async function fetchChatGPTStatus() {
  1093. try {
  1094. if (typeof GM_xmlhttpRequest === "undefined") {
  1095. throw new Error("GM_xmlhttpRequest not supported");
  1096. }
  1097. return new Promise((resolve, reject) => {
  1098. GM_xmlhttpRequest({
  1099. method: "GET",
  1100. url: "https://status.openai.com/api/v2/status.json",
  1101. timeout: 3000,
  1102. ontimeout: () => reject(new Error("Status check timed out")),
  1103. onload: (response) => {
  1104. if (response.status === 200) {
  1105. try {
  1106. const data = JSON.parse(response.responseText);
  1107. const status = data.status;
  1108. const statusDescription =
  1109. document.getElementById("status-description");
  1110. const statusMonitorItem =
  1111. statusDescription?.closest(".monitor-item");
  1112. if (!statusDescription || !statusMonitorItem) {
  1113. reject(new Error("Status UI elements not found"));
  1114. return;
  1115. }
  1116. statusMonitorItem.style.display = "block";
  1117. if (status) {
  1118. const indicator = (status.indicator || "").toLowerCase();
  1119. const description =
  1120. status.description || "All Systems Operational";
  1121. const indicatorColors = {
  1122. none: "var(--success-color, #10a37f)",
  1123. minor: "#FAB12F",
  1124. major: "#FFA500",
  1125. critical: "#e63946",
  1126. };
  1127. if (description === "All Systems Operational") {
  1128. statusDescription.style.color =
  1129. "var(--success-color, #10a37f)";
  1130. } else {
  1131. statusDescription.style.color =
  1132. indicatorColors[indicator] || "#aaa";
  1133. }
  1134. statusDescription.textContent = description;
  1135. }
  1136. resolve();
  1137. } catch (err) {
  1138. reject(err);
  1139. }
  1140. } else {
  1141. reject(new Error(`HTTP error: ${response.status}`));
  1142. }
  1143. },
  1144. onerror: (err) => reject(err),
  1145. });
  1146. });
  1147. } catch (error) {
  1148. const statusDescription = document.getElementById("status-description");
  1149. const statusMonitorItem = statusDescription?.closest(".monitor-item");
  1150. if (statusMonitorItem) statusMonitorItem.style.display = "none";
  1151. }
  1152. }
  1153.  
  1154. function updateTheme() {
  1155. const isDark =
  1156. document.documentElement.classList.contains("dark") ||
  1157. localStorage.getItem("theme") === "dark" ||
  1158. document.documentElement.dataset.theme === "dark";
  1159. displayBox.style.backgroundColor = isDark
  1160. ? "var(--surface-primary, rgba(0, 0, 0, 0.8))"
  1161. : "var(--surface-primary, rgba(255, 255, 255, 0.9))";
  1162. displayBox.style.color = isDark
  1163. ? "var(--text-primary, #fff)"
  1164. : "var(--text-primary, #000)";
  1165. displayBox.querySelectorAll(".label").forEach((label) => {
  1166. label.style.color = isDark
  1167. ? "var(--text-secondary, #aaa)"
  1168. : "var(--text-secondary, #666)";
  1169. });
  1170. }
  1171. })();