// ==UserScript==
// @name ChatGPT Degraded
// @name:zh-CN ChatGPT 服务降级监控
// @name:zh-TW ChatGPT 服務降級監控
// @namespace https://github.com/lroolle/chatgpt-degraded
// @version 0.2.7
// @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
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;
const isChatRequirements =
url &&
(url.includes("/backend-api/sentinel/chat-requirements") ||
url.includes("/backend-anon/sentinel/chat-requirements") ||
url.includes("/api/sentinel/chat-requirements")) &&
options?.method === "POST";
if (isChatRequirements) {
try {
const clonedResponse = response.clone();
const data = await clonedResponse.json();
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) {
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 = "80px";
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>
.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;
}
.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 = "40px";
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")),
});
});
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() {
try {
const response = await fetch("https://chatgpt.com/cdn-cgi/trace");
const text = await response.text();
const data = text.split("\n").reduce((obj, line) => {
const [key, value] = line.split("=");
if (key && value) obj[key.trim()] = value.trim();
return obj;
}, {});
const ipElement = document.getElementById("ip-address");
const warpBadge = document.getElementById("warp-badge");
const ipQualityElement = document.getElementById("ip-quality");
if (!ipElement || !warpBadge || !ipQualityElement) return;
const maskedIP = maskIP(data.ip);
const fullIP = data.ip || "Unknown";
const warpStatus = data.warp || "off";
ipElement.innerText = maskedIP;
ipElement.dataset.fullIp = fullIP;
if (warpStatus === "on" || warpStatus === "plus") {
warpBadge.style.display = "inline-flex";
warpBadge.innerText = warpStatus === "plus" ? "warp+" : "warp";
warpBadge.dataset.tooltip = `Protected by Cloudflare WARP${warpStatus === "plus" ? "+" : ""}`;
} else {
warpBadge.style.display = "none";
}
const { label, color, tooltip, score } = await fetchIPQuality(fullIP);
ipElement.style.color = color;
ipQualityElement.innerText = score !== null ? `${label} (${score})` : label;
ipQualityElement.style.color = color;
ipQualityElement.dataset.score = score;
const difficultyElement = document.getElementById("difficulty");
const currentDifficulty = difficultyElement?.innerText || "N/A";
const logs = addIPLog(fullIP, score, currentDifficulty);
const formattedLogs = formatIPLogs(logs);
const ipContainerTooltip = [
"IP History (recent 10):",
formattedLogs,
"\n---",
"Click to copy full history"
].join('\n');
ipElement.dataset.tooltip = ipContainerTooltip;
ipQualityElement.dataset.tooltip = tooltip;
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 = "History copied!";
setTimeout(() => {
ipElement.innerText = originalText;
}, 1000);
} catch (err) {
ipElement.innerText = "Copy failed";
setTimeout(() => {
ipElement.innerText = maskedIP;
}, 1000);
}
};
ipElement.removeEventListener("click", copyHandler);
ipElement.addEventListener("click", copyHandler);
} catch (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";
if (warpBadge) warpBadge.style.display = "none";
if (ipQualityElement) {
ipQualityElement.innerText = "Unknown";
ipQualityElement.style.color = "#aaa";
ipQualityElement.dataset.tooltip = "Could not check IP quality";
}
}
}
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)";
});
}
})();