您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
监控 ChatGPT 服务状态、IP 质量和 PoW 难度
- // ==UserScript==
- // @name ChatGPT Degraded
- // @name:zh-CN ChatGPT 服务降级监控
- // @name:zh-TW ChatGPT 服務降級監控
- // @namespace https://github.com/lroolle/chatgpt-degraded
- // @version 0.2.8
- // @description Monitor ChatGPT service level, IP quality and PoW difficulty
- // @description:zh-CN 监控 ChatGPT 服务状态、IP 质量和 PoW 难度
- // @description:zh-TW 監控 ChatGPT 服務狀態、IP 質量和 PoW 難度
- // @author lroolle
- // @license AGPL-3.0
- // @match *://chat.openai.com/*
- // @match *://chatgpt.com/*
- // @connect status.openai.com
- // @connect scamalytics.com
- // @grant GM_xmlhttpRequest
- // @grant unsafeWindow
- // @run-at document-start
- // @icon data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI2NCIgaGVpZ2h0PSI2NCIgdmlld0JveD0iMCAwIDY0IDY0Ij4KICA8ZGVmcz4KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iZ3JhZGllbnQiIHgxPSIwJSIgeTE9IjAlIiB4Mj0iMTAwJSIgeTI9IjEwMCUiPgogICAgICA8c3RvcCBvZmZzZXQ9IjAlIiBzdHlsZT0ic3RvcC1jb2xvcjojMmE5ZDhmO3N0b3Atb3BhY2l0eToxIi8+CiAgICAgIDxzdG9wIG9mZnNldD0iMTAwJSIgc3R5bGU9InN0b3AtY29sb3I6IzJhOWQ4ZjtzdG9wLW9wYWNpdHk6MC44Ii8+CiAgICA8L2xpbmVhckdyYWRpZW50PgogIDwvZGVmcz4KICA8Zz4KICAgIDxjaXJjbGUgY3g9IjMyIiBjeT0iMzIiIHI9IjI4IiBmaWxsPSJ1cmwoI2dyYWRpZW50KSIgc3Ryb2tlPSIjZmZmIiBzdHJva2Utd2lkdGg9IjEiLz4KPCEtLU91dGVyIGNpcmNsZSBtb2RpZmllZCB0byBsb29rIGxpa2UgIkMiLS0+CiAgICA8Y2lyY2xlIGN4PSIzMiIgY3k9IjMyIiByPSIyMCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZmZmIiBzdHJva2Utd2lkdGg9IjEiIHN0cm9rZS1kYXNoYXJyYXk9IjEyNSA1NSIgc3Ryb2tlLWRhc2hvZmZzZXQ9IjIwIi8+CiAgICA8Y2lyY2xlIGN4PSIzMiIgY3k9IjMyIiByPSIxMiIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZmZmIiBzdHJva2Utd2lkdGg9IjEiLz4KICAgIDxjaXJjbGUgY3g9IjMyIiBjeT0iMzIiIHI9IjQiIGZpbGw9IiNmZmYiLz4KICA8L2c+Cjwvc3ZnPg==
- // @homepageURL https://github.com/lroolle/chatgpt-degraded
- // @supportURL https://github.com/lroolle/chatgpt-degraded/issues
- // ==/UserScript==
- (function () {
- "use strict";
- let displayBox, collapsedIndicator;
- const i18n = {
- en: {
- service: "Service",
- ip: "IP",
- pow: "PoW",
- status: "Status",
- unknown: "Unknown",
- copyHistory: "Click to copy history",
- historyCopied: "History copied!",
- copyFailed: "Copy failed",
- riskLevels: {
- veryEasy: "Very Easy",
- easy: "Easy",
- medium: "Medium",
- hard: "Hard",
- critical: "Critical",
- },
- tooltips: {
- powDifficulty: "PoW Difficulty: Lower (green) means faster responses.",
- ipHistory: "IP History (recent 10):",
- warpPlus: "Protected by Cloudflare WARP+",
- warp: "Protected by Cloudflare WARP",
- clickToCopy: "Click to copy full history",
- },
- },
- "zh-CN": {
- service: "服务",
- ip: "IP",
- pow: "算力",
- status: "状态",
- unknown: "未知",
- copyHistory: "点击复制历史",
- historyCopied: "已复制历史!",
- copyFailed: "复制失败",
- riskLevels: {
- veryEasy: "非常容易",
- easy: "容易",
- medium: "中等",
- hard: "困难",
- critical: "严重",
- },
- tooltips: {
- powDifficulty: "PoW 难度:越低(绿色)响应越快",
- ipHistory: "IP 历史(最近10条):",
- warpPlus: "已启用 Cloudflare WARP+",
- warp: "已启用 Cloudflare WARP",
- clickToCopy: "点击复制完整历史",
- },
- },
- "zh-TW": {
- service: "服務",
- ip: "IP",
- pow: "算力",
- status: "狀態",
- unknown: "未知",
- copyHistory: "點擊複製歷史",
- historyCopied: "已複製歷史!",
- copyFailed: "複製失敗",
- riskLevels: {
- veryEasy: "非常容易",
- easy: "容易",
- medium: "中等",
- hard: "困難",
- critical: "嚴重",
- },
- tooltips: {
- powDifficulty: "PoW 難度:越低(綠色)回應越快",
- ipHistory: "IP 歷史(最近10筆):",
- warpPlus: "已啟用 Cloudflare WARP+",
- warp: "已啟用 Cloudflare WARP",
- clickToCopy: "點擊複製完整歷史",
- },
- },
- };
- // Get user language
- const userLang = (navigator.language || "en").toLowerCase();
- const lang = i18n[userLang]
- ? userLang
- : userLang.startsWith("zh-tw")
- ? "zh-TW"
- : userLang.startsWith("zh")
- ? "zh-CN"
- : "en";
- const t = (key) => {
- const keys = key.split(".");
- return (
- keys.reduce((obj, k) => obj?.[k], i18n[lang]) ||
- i18n.en[keys[keys.length - 1]]
- );
- };
- function updateUserType(type) {
- const userTypeElement = document.getElementById("user-type");
- if (!userTypeElement) return;
- const isPaid =
- type &&
- (type === "plus" ||
- type === "chatgpt-paid" ||
- type.includes("paid") ||
- type.includes("premium") ||
- type.includes("pro"));
- userTypeElement.textContent = isPaid ? "Paid" : "Free";
- userTypeElement.dataset.tooltip = `ChatGPT Account Type: ${isPaid ? "Paid" : "Free"}`;
- userTypeElement.style.color = isPaid
- ? "var(--success-color, #10a37f)"
- : "var(--text-primary, #374151)";
- }
- function getRiskColorAndLevel(difficulty) {
- if (!difficulty || difficulty === "N/A") {
- return { color: "#e63946", level: "Unknown", percentage: 0 };
- }
- const cleanDifficulty = difficulty.replace(/^0x/, "").replace(/^0+/, "");
- const hexLength = cleanDifficulty.length;
- if (hexLength <= 2) {
- return { color: "#e63946", level: "Critical", percentage: 100 };
- } else if (hexLength <= 3) {
- return { color: "#FAB12F", level: "Hard", percentage: 75 };
- } else if (hexLength <= 4) {
- return { color: "#859F3D", level: "Medium", percentage: 50 };
- } else if (hexLength <= 5) {
- return { color: "#2a9d8f", level: "Easy", percentage: 25 };
- } else {
- return { color: "#4CAF50", level: "Very Easy", percentage: 0 };
- }
- }
- function setProgressBar(bar, label, percentage, text, gradient, title) {
- bar.style.width = "100%";
- bar.style.background = gradient;
- bar.dataset.tooltip = title;
- label.innerText = text;
- }
- function updateProgressBars(difficulty) {
- const powBar = document.getElementById("pow-bar");
- const powLevel = document.getElementById("pow-level");
- const difficultyElement = document.getElementById("difficulty");
- if (!powBar || !powLevel || !difficultyElement) return;
- const { color, level, percentage } = getRiskColorAndLevel(difficulty);
- const gradient = `linear-gradient(90deg, ${color} ${percentage}%, rgba(255, 255, 255, 0.1) ${percentage}%)`;
- setProgressBar(
- powBar,
- powLevel,
- percentage,
- level,
- gradient,
- "PoW Difficulty: Lower (green) means faster responses.",
- );
- difficultyElement.style.color = color;
- powLevel.style.color = color;
- // Update icon animation based on difficulty level
- if (collapsedIndicator) {
- const outerRingAnim =
- collapsedIndicator.querySelector("#outer-ring-anim");
- const middleRingAnim =
- collapsedIndicator.querySelector("#middle-ring-anim");
- const centerDotAnim =
- collapsedIndicator.querySelector("#center-dot-anim");
- const gradientStops = collapsedIndicator.querySelector("#gradient");
- // Adjust animation speed based on difficulty level
- const animationSpeed = percentage < 25 ? 0.5 : percentage / 25; // Make it more still when easy
- if (outerRingAnim)
- outerRingAnim.setAttribute("dur", `${8 / animationSpeed}s`);
- if (middleRingAnim)
- middleRingAnim.setAttribute("dur", `${4 / animationSpeed}s`);
- if (centerDotAnim) {
- centerDotAnim.setAttribute("dur", `${2 / animationSpeed}s`);
- // Smaller pulse for easy difficulty
- centerDotAnim.setAttribute(
- "values",
- percentage < 25 ? "4;4.5;4" : "4;5;4",
- );
- }
- // Update color
- if (gradientStops) {
- gradientStops.innerHTML = `
- <stop offset="0%" style="stop-color:${color};stop-opacity:1" />
- <stop offset="100%" style="stop-color:${color};stop-opacity:0.8" />
- `;
- }
- }
- }
- const originalFetch = unsafeWindow.fetch;
- unsafeWindow.fetch = async function (resource, options) {
- const response = await originalFetch(resource, options);
- const url = typeof resource === "string" ? resource : resource?.url;
- // console.log("Fetch URL:", url);
- // console.log("Fetch options:", options);
- const isChatRequirements =
- url &&
- (url.includes("/backend-api/sentinel/chat-requirements") ||
- url.includes("/backend-anon/sentinel/chat-requirements") ||
- url.includes("/api/sentinel/chat-requirements"));
- // const method = options?.method?.toUpperCase() || "GET";
- // console.log("Method:", method, "isChatRequirements URL match:", isChatRequirements);
- // Check if this is a chat requirements request (regardless of method for now)
- if (isChatRequirements) {
- // console.log("Processing chat requirements request with method:", method);
- try {
- const clonedResponse = response.clone();
- const data = await clonedResponse.json();
- // console.log("Chat requirements response data:", data);
- const difficulty = data?.proofofwork?.difficulty;
- const userType = data?.persona || data?.user_type || data?.account_type;
- const difficultyElement = document.getElementById("difficulty");
- if (difficultyElement) {
- if (difficulty) {
- difficultyElement.innerText = difficulty;
- difficultyElement.dataset.tooltip = `Raw Difficulty Value: ${difficulty}`;
- // Update IP log with new PoW difficulty
- const ipElement = document.getElementById("ip-address");
- if (ipElement) {
- const fullIP = ipElement.dataset.fullIp;
- const ipQualityElement = document.getElementById("ip-quality");
- const score = ipQualityElement
- ? parseInt(ipQualityElement.dataset.score)
- : null;
- if (fullIP) {
- const logs = addIPLog(fullIP, score, difficulty);
- const formattedLogs = formatIPLogs(logs);
- const ipContainerTooltip = [
- "IP History (recent 10):",
- formattedLogs,
- "\n---",
- "Click to copy history",
- ].join("\n");
- ipElement.dataset.tooltip = ipContainerTooltip;
- }
- }
- } else {
- difficultyElement.innerText = "N/A";
- difficultyElement.dataset.tooltip = "No difficulty value found";
- }
- }
- updateUserType(userType || "free");
- updateProgressBars(difficulty || "N/A");
- } catch (error) {
- console.error("Error processing chat requirements:", error);
- const difficultyElement = document.getElementById("difficulty");
- if (difficultyElement) {
- difficultyElement.innerText = "N/A";
- difficultyElement.dataset.tooltip = `Error: ${error.message}`;
- }
- updateUserType("free");
- updateProgressBars("N/A");
- }
- }
- return response;
- };
- function initUI() {
- displayBox = document.createElement("div");
- displayBox.style.position = "fixed";
- displayBox.style.bottom = "10px";
- displayBox.style.right = "55px";
- displayBox.style.width = "360px";
- displayBox.style.padding = "24px";
- displayBox.style.backgroundColor =
- "var(--surface-primary, rgb(255, 255, 255))";
- displayBox.style.color = "var(--text-primary, #374151)";
- displayBox.style.fontSize = "14px";
- displayBox.style.borderRadius = "16px";
- displayBox.style.boxShadow = "0 4px 24px rgba(0, 0, 0, 0.08)";
- displayBox.style.zIndex = "10000";
- displayBox.style.transition = "opacity 0.15s ease, transform 0.15s ease";
- displayBox.style.display = "none";
- displayBox.style.opacity = "0";
- displayBox.style.transform = "translateX(10px)";
- displayBox.style.border =
- "1px solid var(--border-light, rgba(0, 0, 0, 0.05))";
- displayBox.innerHTML = `
- <div id="content">
- <div class="monitor-item">
- <div class="monitor-row">
- <span class="label">${t("service")}</span>
- <span id="user-type" class="value" data-tooltip="ChatGPT Account Type"></span>
- </div>
- </div>
- <!-- Proof of Work Difficulty -->
- <div class="monitor-item">
- <div class="monitor-row">
- <span class="label">${t("pow")}</span>
- <div class="pow-container">
- <span id="difficulty" class="value monospace" data-tooltip="PoW Difficulty Value"></span>
- <span id="pow-level" class="value-tag" data-tooltip="Difficulty Level"></span>
- </div>
- </div>
- <div class="progress-wrapper" data-tooltip="${t("tooltips.powDifficulty")}">
- <div class="progress-container">
- <div id="pow-bar" class="progress-bar"></div>
- </div>
- <div class="progress-background"></div>
- </div>
- </div>
- <!-- IP + IP Quality -->
- <div class="monitor-item">
- <div class="monitor-row">
- <span class="label">${t("ip")}</span>
- <div class="ip-container">
- <span id="ip-address" class="value monospace" data-tooltip="Click to copy IP address"></span>
- <span id="warp-badge" class="warp-badge"></span>
- <span id="ip-quality" class="value-tag" data-tooltip="IP Risk Info (Scamlytics)"></span>
- </div>
- </div>
- </div>
- <!-- OpenAI System Status -->
- <div class="monitor-item">
- <div class="monitor-row">
- <span class="label">${t("status")}</span>
- <a id="status-description"
- href="https://status.openai.com"
- target="_blank"
- class="value"
- data-tooltip="Click to open status.openai.com">
- ${t("unknown")}
- </a>
- </div>
- </div>
- </div>
- <style>
- :root {
- --warning-color: #FAB12F;
- --warning-background: rgba(251, 177, 47, 0.1);
- --error-color: #e63946;
- --error-background: rgba(230, 57, 70, 0.1);
- }
- .monitor-item {
- margin-bottom: 16px;
- }
- .monitor-item:last-child {
- margin-bottom: 0;
- }
- .monitor-row {
- display: flex;
- align-items: center;
- gap: 6px;
- margin-bottom: 6px;
- }
- .monitor-row:last-child {
- margin-bottom: 4px;
- }
- .label {
- font-size: 14px;
- color: var(--text-secondary, #6B7280);
- flex-shrink: 0;
- min-width: 40px;
- }
- .value {
- font-size: 14px;
- color: var(--text-primary, #374151);
- flex: 1;
- }
- .monospace {
- font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
- font-size: 14px;
- }
- .value-tag {
- font-size: 14px;
- color: var(--success-color, #10a37f);
- white-space: nowrap;
- font-weight: 500;
- transition: opacity 0.15s ease;
- cursor: pointer;
- display: inline-block;
- }
- .value-tag:hover {
- opacity: 0.8;
- }
- .progress-wrapper {
- position: relative;
- margin-left: 40px;
- margin-top: 4px;
- }
- .progress-container {
- position: relative;
- height: 4px;
- background: transparent;
- border-radius: 2px;
- overflow: hidden;
- z-index: 1;
- }
- .progress-background {
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- height: 4px;
- background: var(--surface-secondary, rgba(0, 0, 0, 0.08));
- border-radius: 2px;
- }
- .progress-bar {
- height: 100%;
- width: 0%;
- transition: all 0.3s ease;
- background: var(--success-color, #10a37f);
- }
- #status-description {
- text-decoration: none;
- color: inherit;
- }
- #status-description:hover {
- text-decoration: underline;
- }
- #ip-address {
- cursor: pointer;
- }
- #ip-address:hover {
- opacity: 0.7;
- }
- #user-type {
- font-weight: 500;
- }
- .ip-container,
- .pow-container {
- display: flex;
- align-items: center;
- gap: 6px;
- flex: 1;
- }
- /* Ensure IP risk level (ip-quality) is right-aligned, just like pow-level */
- #ip-quality {
- margin-left: auto;
- }
- .warp-badge {
- font-size: 12px;
- color: var(--success-color, #10a37f);
- background-color: var(--surface-secondary, rgba(16, 163, 127, 0.1));
- padding: 2px 4px;
- border-radius: 4px;
- font-weight: 500;
- cursor: help;
- display: none;
- transition: background-color 0.2s ease, color 0.2s ease;
- }
- .warp-badge:hover {
- opacity: 0.8;
- }
- .ip-container .value-tag {
- padding-right: 0;
- position: relative;
- }
- /* Special handling for IP Risk tooltip */
- .ip-container .value-tag[data-tooltip]::after {
- left: auto;
- right: 0;
- transform: translateY(4px);
- }
- .ip-container .value-tag[data-tooltip]:hover::after {
- transform: translateY(0);
- left: auto;
- right: 0;
- }
- /* General tooltip styles */
- [data-tooltip] {
- position: relative;
- cursor: help;
- }
- [data-tooltip]::after {
- content: attr(data-tooltip);
- position: absolute;
- bottom: 100%;
- left: 50%;
- transform: translateX(-50%) translateY(4px);
- background: var(--surface-primary, rgba(0, 0, 0, 0.8));
- color: #fff;
- padding: 12px 16px;
- border-radius: 6px;
- font-size: 12px;
- white-space: pre-line;
- width: max-content;
- max-width: 600px;
- min-width: 450px;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
- z-index: 1000;
- pointer-events: none;
- margin-bottom: 8px;
- opacity: 0;
- transition: opacity 0.15s ease, transform 0.15s ease;
- font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
- }
- [data-tooltip]:hover::after {
- opacity: 1;
- transform: translateX(-50%) translateY(0);
- }
- /* Arrow styles */
- [data-tooltip]::before {
- content: '';
- position: absolute;
- bottom: 100%;
- left: 50%;
- transform: translateX(-50%) translateY(4px);
- border: 6px solid transparent;
- border-top-color: var(--surface-primary, rgba(0, 0, 0, 0.8));
- margin-bottom: -4px;
- pointer-events: none;
- opacity: 0;
- transition: opacity 0.15s ease, transform 0.15s ease;
- }
- [data-tooltip]:hover::before {
- opacity: 1;
- transform: translateY(0);
- }
- /* Special handling for IP Risk tooltip arrow */
- .ip-container .value-tag[data-tooltip]::before {
- left: auto;
- right: 12px;
- transform: translateY(4px);
- }
- .ip-container .value-tag[data-tooltip]:hover::before {
- transform: translateY(0);
- left: auto;
- right: 12px;
- }
- /* Ensure tooltips don't get cut off at viewport edges */
- @media screen and (max-width: 768px) {
- [data-tooltip]::after {
- min-width: 300px;
- max-width: calc(100vw - 48px);
- }
- }
- </style>
- `;
- document.body.appendChild(displayBox);
- collapsedIndicator = document.createElement("div");
- collapsedIndicator.style.position = "fixed";
- collapsedIndicator.style.bottom = "10px";
- collapsedIndicator.style.right = "15px";
- collapsedIndicator.style.width = "24px";
- collapsedIndicator.style.height = "24px";
- collapsedIndicator.style.backgroundColor = "transparent";
- collapsedIndicator.style.border =
- "1px solid var(--token-border-light, rgba(0, 0, 0, 0.1))";
- collapsedIndicator.style.borderRadius = "50%";
- collapsedIndicator.style.cursor = "pointer";
- collapsedIndicator.style.zIndex = "10000";
- collapsedIndicator.style.display = "flex";
- collapsedIndicator.style.alignItems = "center";
- collapsedIndicator.style.justifyContent = "center";
- collapsedIndicator.style.transition = "all 0.3s ease";
- collapsedIndicator.innerHTML = `
- <svg width="24" height="24" viewBox="0 0 64 64">
- <defs>
- <linearGradient id="gradient" x1="0%" y1="0%" x2="100%" y2="100%">
- <stop offset="0%" style="stop-color:#666;stop-opacity:1" />
- <stop offset="100%" style="stop-color:#666;stop-opacity:0.8" />
- </linearGradient>
- <filter id="glow">
- <feGaussianBlur stdDeviation="1" result="coloredBlur"/>
- <feMerge>
- <feMergeNode in="coloredBlur"/>
- <feMergeNode in="SourceGraphic"/>
- </feMerge>
- </filter>
- </defs>
- <g id="icon-group" filter="url(#glow)" transform="rotate(165, 32, 32)">
- <circle cx="32" cy="32" r="28" fill="url(#gradient)" stroke="#fff" stroke-width="1"/>
- <circle cx="32" cy="32" r="20" fill="none" stroke="#fff" stroke-width="1"
- stroke-dasharray="80 40" transform="rotate(-90, 32, 32)">
- <animate attributeName="stroke-dashoffset"
- dur="4s"
- values="0;120"
- repeatCount="indefinite"
- id="outer-ring-anim"/>
- </circle>
- <circle cx="32" cy="32" r="12" fill="none" stroke="#fff" stroke-width="1">
- <animate attributeName="r"
- dur="2s"
- values="12;14;12"
- repeatCount="indefinite"
- id="middle-ring-anim"/>
- </circle>
- <circle id="center-dot" cx="32" cy="32" r="4" fill="#fff">
- <animate attributeName="r"
- dur="1s"
- values="4;5;4"
- repeatCount="indefinite"
- id="center-dot-anim"/>
- </circle>
- </g>
- </svg>
- `;
- document.body.appendChild(collapsedIndicator);
- collapsedIndicator.addEventListener("mouseenter", () => {
- displayBox.style.display = "block";
- requestAnimationFrame(() => {
- displayBox.style.opacity = "1";
- displayBox.style.transform = "translateX(0)";
- });
- });
- displayBox.addEventListener("mouseleave", () => {
- displayBox.style.opacity = "0";
- displayBox.style.transform = "translateX(10px)";
- setTimeout(() => {
- displayBox.style.display = "none";
- }, 150);
- });
- const observer = new MutationObserver(updateTheme);
- observer.observe(document.documentElement, {
- attributes: true,
- attributeFilter: ["class"],
- });
- fetchIPInfo();
- fetchChatGPTStatus();
- updateTheme();
- const statusCheckInterval = 60 * 60 * 1000;
- let statusCheckTimer = setInterval(fetchChatGPTStatus, statusCheckInterval);
- document.addEventListener("visibilitychange", () => {
- if (document.visibilityState === "visible") {
- clearInterval(statusCheckTimer);
- fetchChatGPTStatus();
- statusCheckTimer = setInterval(fetchChatGPTStatus, statusCheckInterval);
- }
- });
- }
- if (document.readyState !== "loading") {
- initUI();
- } else {
- document.addEventListener("DOMContentLoaded", initUI);
- }
- function maskIP(ip) {
- if (!ip || ip === "Unknown") return ip;
- if (ip.includes(".")) {
- const parts = ip.split(".");
- if (parts.length === 4) {
- return `${parts[0]}.*.*.${parts[3]}`;
- }
- }
- if (ip.includes(":")) {
- const parts = ip.split(":");
- // Shorten IPv6 to just show first and last part
- if (parts.length > 2) {
- return `${parts[0]}:*:${parts[parts.length - 1]}`;
- }
- }
- return ip;
- }
- async function fetchIPQuality(ip) {
- try {
- const response = await new Promise((resolve, reject) => {
- GM_xmlhttpRequest({
- method: "GET",
- url: `https://scamalytics.com/ip/${ip}`,
- timeout: 3000,
- onload: (r) =>
- r.status === 200
- ? resolve(r.responseText)
- : reject(new Error(`HTTP ${r.status}`)),
- onerror: reject,
- ontimeout: () => reject(new Error("Request timed out")),
- });
- });
- console.log("fetchIPQuality.response", response);
- const parser = new DOMParser();
- const doc = parser.parseFromString(response, "text/html");
- const scoreElement = doc.querySelector(".score_bar .score");
- const scoreMatch =
- scoreElement?.textContent.match(/Fraud Score:\s*(\d+)/i);
- if (!scoreMatch) {
- return {
- label: "Unknown",
- color: "#aaa",
- tooltip: "Could not determine IP quality",
- score: null,
- };
- }
- const score = parseInt(scoreMatch[1], 10);
- const riskElement = doc.querySelector(".panel_title");
- const riskText = riskElement?.textContent.trim() || "Unknown Risk";
- const panelColor = riskElement?.style.backgroundColor || "#aaa";
- const descriptionElement = doc.querySelector(".panel_body");
- const description = descriptionElement?.textContent.trim() || "";
- const trimmedDescription =
- description.length > 150
- ? `${description.substring(0, 147)}...`
- : description;
- function extractTableValue(header) {
- const row = Array.from(doc.querySelectorAll("th")).find(
- (th) => th.textContent.trim() === header,
- )?.parentElement;
- return row?.querySelector("td")?.textContent.trim() || null;
- }
- function isRiskYes(header) {
- const row = Array.from(doc.querySelectorAll("th")).find(
- (th) => th.textContent.trim() === header,
- )?.parentElement;
- return row?.querySelector(".risk.yes") !== null;
- }
- const details = {
- location: extractTableValue("City") || "Unknown",
- state: extractTableValue("State / Province"),
- country: extractTableValue("Country Name"),
- isp: extractTableValue("ISP Name") || "Unknown",
- organization: extractTableValue("Organization Name"),
- isVPN: isRiskYes("Anonymizing VPN"),
- isTor: isRiskYes("Tor Exit Node"),
- isServer: isRiskYes("Server"),
- isProxy:
- isRiskYes("Public Proxy") ||
- isRiskYes("Web Proxy") ||
- isRiskYes("Proxy"),
- };
- let label, color;
- if (riskText && riskText !== "Unknown Risk") {
- label = riskText;
- color = panelColor !== "#aaa" ? panelColor : getColorForScore(score);
- } else {
- ({ label, color } = getLabelAndColorForScore(score));
- }
- const warnings = [];
- if (details.isVPN) warnings.push("VPN");
- if (details.isTor) warnings.push("Tor");
- if (details.isServer) warnings.push("Server");
- if (details.isProxy) warnings.push("Proxy");
- const location = [details.location, details.state, details.country]
- .filter(Boolean)
- .join(", ");
- const tooltip = [
- "IP Risk Info (Scamlytics):",
- label !== "Unknown" ? `Risk: ${label} (${score}/100)` : "",
- `Location: ${location}`,
- `ISP: ${details.isp}${details.organization ? ` (${details.organization})` : ""}`,
- warnings.length ? `Warnings: ${warnings.join(", ")}` : "",
- trimmedDescription ? `\n${trimmedDescription}` : "",
- "\nClick to view full analysis",
- ]
- .filter(Boolean)
- .join("\n");
- return { label, color, tooltip, score };
- } catch (error) {
- return {
- label: "Unknown",
- color: "#aaa",
- tooltip: "Could not check IP quality",
- score: null,
- };
- }
- }
- function getColorForScore(score) {
- if (score < 25) return "#4CAF50";
- if (score < 50) return "#859F3D";
- if (score < 75) return "#FAB12F";
- return "#e63946";
- }
- function getLabelAndColorForScore(score) {
- if (score < 25)
- return { label: t("riskLevels.veryEasy"), color: "#4CAF50" };
- if (score < 50) return { label: t("riskLevels.easy"), color: "#859F3D" };
- if (score < 75) return { label: t("riskLevels.medium"), color: "#FAB12F" };
- return { label: t("riskLevels.critical"), color: "#e63946" };
- }
- function getIPLogs() {
- try {
- const logs = localStorage.getItem("chatgpt_ip_logs");
- return logs ? JSON.parse(logs) : [];
- } catch (error) {
- console.error("Error reading IP logs:", error);
- return [];
- }
- }
- function addIPLog(ip, score, difficulty) {
- try {
- const logs = getIPLogs();
- const timestamp = new Date().toISOString();
- const newLog = { timestamp, ip, score, difficulty };
- if (logs.length > 0 && logs[0].ip === ip) {
- logs[0] = newLog;
- } else {
- logs.unshift(newLog);
- }
- const trimmedLogs = logs.slice(0, 10);
- localStorage.setItem("chatgpt_ip_logs", JSON.stringify(trimmedLogs));
- return trimmedLogs;
- } catch (error) {
- console.error("Error adding IP log:", error);
- return [];
- }
- }
- function formatIPLogs(logs) {
- return logs
- .map((log) => {
- const date = new Date(log.timestamp);
- const formattedDate = date
- .toLocaleString("en-US", {
- year: "numeric",
- month: "2-digit",
- day: "2-digit",
- hour: "2-digit",
- minute: "2-digit",
- hour12: false,
- })
- .replace(/(\d+)\/(\d+)\/(\d+),\s(\d+):(\d+)/, "[$3-$1-$2 $4:$5]");
- const { color: powColor, level: powLevel } = getRiskColorAndLevel(
- log.difficulty,
- );
- const scoreDisplay =
- log.score !== null && log.score !== undefined ? log.score : "N/A";
- return `${formattedDate} ${log.ip}(${scoreDisplay}), ${log.difficulty || "N/A"}(${powLevel})`;
- })
- .join("\n");
- }
- async function fetchIPInfo() {
- const fallbackServices = [
- {
- url: "https://chatgpt.com/cdn-cgi/trace",
- parser: (text) => {
- const data = text.split("\n").reduce((obj, line) => {
- const [key, value] = line.split("=");
- if (key && value) obj[key.trim()] = value.trim();
- return obj;
- }, {});
- return {
- ip: data.ip,
- warp: data.warp || "off",
- location: data.loc,
- colo: data.colo,
- };
- },
- },
- {
- url: "https://www.cloudflare.com/cdn-cgi/trace",
- parser: (text) => {
- const data = text.split("\n").reduce((obj, line) => {
- const [key, value] = line.split("=");
- if (key && value) obj[key.trim()] = value.trim();
- return obj;
- }, {});
- return {
- ip: data.ip,
- warp: data.warp || "off",
- location: data.loc,
- colo: data.colo,
- };
- },
- },
- {
- url: "https://ipinfo.io/json",
- parser: (text) => {
- const data = JSON.parse(text);
- return {
- ip: data.ip,
- warp: "off", // ipinfo.io doesn't provide WARP status
- location: data.loc,
- city: data.city,
- country: data.country,
- };
- },
- },
- ];
- let lastError = null;
- for (let i = 0; i < fallbackServices.length; i++) {
- const service = fallbackServices[i];
- try {
- console.log(
- `Attempting to fetch IP info from service ${i + 1}:`,
- service.url,
- );
- const response = await new Promise((resolve, reject) => {
- GM_xmlhttpRequest({
- method: "GET",
- url: service.url,
- timeout: 5000,
- onload: (r) => {
- if (r.status === 200) {
- resolve(r.responseText);
- } else {
- reject(new Error(`HTTP ${r.status}: ${r.statusText}`));
- }
- },
- onerror: (err) =>
- reject(
- new Error(`Network error: ${err.message || "Unknown error"}`),
- ),
- ontimeout: () => reject(new Error("Request timed out")),
- });
- });
- console.log(`Service ${i + 1} response:`, response);
- const data = service.parser(response);
- console.log(`Parsed data from service ${i + 1}:`, data);
- if (!data.ip) {
- throw new Error("No IP address found in response");
- }
- await updateIPDisplay(data);
- return; // Success, exit function
- } catch (error) {
- console.error(`Service ${i + 1} failed:`, error.message);
- lastError = error;
- // If not the last service, wait a bit before trying next
- if (i < fallbackServices.length - 1) {
- await new Promise((resolve) => setTimeout(resolve, 1000));
- }
- }
- }
- // All services failed
- console.error("All IP services failed. Last error:", lastError);
- await handleIPFetchFailure(lastError);
- }
- async function updateIPDisplay(data) {
- const ipElement = document.getElementById("ip-address");
- const warpBadge = document.getElementById("warp-badge");
- const ipQualityElement = document.getElementById("ip-quality");
- if (!ipElement || !warpBadge || !ipQualityElement) {
- throw new Error("IP display elements not found");
- }
- const maskedIP = maskIP(data.ip);
- const fullIP = data.ip;
- const warpStatus = data.warp || "off";
- ipElement.innerText = maskedIP;
- ipElement.dataset.fullIp = fullIP;
- // Handle WARP display and warnings
- if (warpStatus === "on" || warpStatus === "plus") {
- warpBadge.style.display = "inline-flex";
- warpBadge.innerText = warpStatus === "plus" ? "warp+" : "warp";
- warpBadge.dataset.tooltip = t(
- `tooltips.${warpStatus === "plus" ? "warpPlus" : "warp"}`,
- );
- warpBadge.style.backgroundColor =
- "var(--success-color, rgba(16, 163, 127, 0.1))";
- warpBadge.style.color = "var(--success-color, #10a37f)";
- } else {
- // Show warning when WARP is not enabled
- warpBadge.style.display = "inline-flex";
- warpBadge.innerText = "no warp";
- warpBadge.dataset.tooltip =
- "⚠️ WARP not enabled - Consider enabling Cloudflare WARP for better privacy and potentially improved IP quality";
- warpBadge.style.backgroundColor =
- "var(--warning-background, rgba(251, 177, 47, 0.1))";
- warpBadge.style.color = "var(--warning-color, #FAB12F)";
- }
- // Fetch IP quality
- try {
- const { label, color, tooltip, score } = await fetchIPQuality(fullIP);
- // console.log("IP Quality result:", { label, color, tooltip, score });
- ipElement.style.color = color;
- ipQualityElement.innerText =
- score !== null ? `${label} (${score})` : label;
- ipQualityElement.style.color = color;
- ipQualityElement.dataset.score = score;
- ipQualityElement.dataset.tooltip = tooltip;
- // Update IP logs
- const difficultyElement = document.getElementById("difficulty");
- const currentDifficulty = difficultyElement?.innerText || "N/A";
- const logs = addIPLog(fullIP, score, currentDifficulty);
- const formattedLogs = formatIPLogs(logs);
- const ipContainerTooltip = [
- t("tooltips.ipHistory"),
- formattedLogs,
- "\n---",
- t("tooltips.clickToCopy"),
- ].join("\n");
- ipElement.dataset.tooltip = ipContainerTooltip;
- // Add click handlers
- ipQualityElement.onclick = () =>
- window.open(`https://scamalytics.com/ip/${fullIP}`, "_blank");
- const copyHandler = async () => {
- try {
- const logs = getIPLogs();
- const formattedHistory = formatIPLogs(logs);
- await navigator.clipboard.writeText(formattedHistory);
- const originalText = ipElement.innerText;
- ipElement.innerText = t("historyCopied");
- setTimeout(() => {
- ipElement.innerText = originalText;
- }, 1000);
- } catch (err) {
- console.error("Copy failed:", err);
- const originalText = ipElement.innerText;
- ipElement.innerText = t("copyFailed");
- setTimeout(() => {
- ipElement.innerText = originalText;
- }, 1000);
- }
- };
- ipElement.removeEventListener("click", copyHandler);
- ipElement.addEventListener("click", copyHandler);
- } catch (qualityError) {
- console.error("Failed to fetch IP quality:", qualityError);
- ipQualityElement.innerText = "Quality check failed";
- ipQualityElement.style.color = "#aaa";
- ipQualityElement.dataset.tooltip = `Could not check IP quality: ${qualityError.message}`;
- }
- }
- async function handleIPFetchFailure(error) {
- console.error("All IP fetch attempts failed:", error);
- const ipElement = document.getElementById("ip-address");
- const warpBadge = document.getElementById("warp-badge");
- const ipQualityElement = document.getElementById("ip-quality");
- if (ipElement) {
- ipElement.innerText = "Failed to fetch";
- ipElement.style.color = "#e63946";
- ipElement.dataset.tooltip = `IP fetch failed: ${error?.message || "Unknown error"}\nTry refreshing the page`;
- }
- if (warpBadge) {
- warpBadge.style.display = "inline-flex";
- warpBadge.innerText = "error";
- warpBadge.dataset.tooltip =
- "Could not determine WARP status due to network error";
- warpBadge.style.backgroundColor =
- "var(--error-background, rgba(230, 57, 70, 0.1))";
- warpBadge.style.color = "var(--error-color, #e63946)";
- }
- if (ipQualityElement) {
- ipQualityElement.innerText = "Network Error";
- ipQualityElement.style.color = "#e63946";
- ipQualityElement.dataset.tooltip =
- "Could not check IP quality due to network error";
- }
- }
- async function fetchChatGPTStatus() {
- try {
- if (typeof GM_xmlhttpRequest === "undefined") {
- throw new Error("GM_xmlhttpRequest not supported");
- }
- return new Promise((resolve, reject) => {
- GM_xmlhttpRequest({
- method: "GET",
- url: "https://status.openai.com/api/v2/status.json",
- timeout: 3000,
- ontimeout: () => reject(new Error("Status check timed out")),
- onload: (response) => {
- if (response.status === 200) {
- try {
- const data = JSON.parse(response.responseText);
- const status = data.status;
- const statusDescription =
- document.getElementById("status-description");
- const statusMonitorItem =
- statusDescription?.closest(".monitor-item");
- if (!statusDescription || !statusMonitorItem) {
- reject(new Error("Status UI elements not found"));
- return;
- }
- statusMonitorItem.style.display = "block";
- if (status) {
- const indicator = (status.indicator || "").toLowerCase();
- const description =
- status.description || "All Systems Operational";
- const indicatorColors = {
- none: "var(--success-color, #10a37f)",
- minor: "#FAB12F",
- major: "#FFA500",
- critical: "#e63946",
- };
- if (description === "All Systems Operational") {
- statusDescription.style.color =
- "var(--success-color, #10a37f)";
- } else {
- statusDescription.style.color =
- indicatorColors[indicator] || "#aaa";
- }
- statusDescription.textContent = description;
- }
- resolve();
- } catch (err) {
- reject(err);
- }
- } else {
- reject(new Error(`HTTP error: ${response.status}`));
- }
- },
- onerror: (err) => reject(err),
- });
- });
- } catch (error) {
- const statusDescription = document.getElementById("status-description");
- const statusMonitorItem = statusDescription?.closest(".monitor-item");
- if (statusMonitorItem) statusMonitorItem.style.display = "none";
- }
- }
- function updateTheme() {
- const isDark =
- document.documentElement.classList.contains("dark") ||
- localStorage.getItem("theme") === "dark" ||
- document.documentElement.dataset.theme === "dark";
- displayBox.style.backgroundColor = isDark
- ? "var(--surface-primary, rgba(0, 0, 0, 0.8))"
- : "var(--surface-primary, rgba(255, 255, 255, 0.9))";
- displayBox.style.color = isDark
- ? "var(--text-primary, #fff)"
- : "var(--text-primary, #000)";
- displayBox.querySelectorAll(".label").forEach((label) => {
- label.style.color = isDark
- ? "var(--text-secondary, #aaa)"
- : "var(--text-secondary, #666)";
- });
- }
- })();