您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Finds the geographical location of IP addresses on a page.
// ==UserScript== // @name IP Location Finder // @namespace https://github.com/Yanel85/IP-Location-Finder/ // @version 1.4 // @description Finds the geographical location of IP addresses on a page. // @author Perry Yen // @match *://*/* // @grant GM_xmlhttpRequest // @grant GM_getValue // @grant GM_setValue // @connect ipapi.co // @connect geo.ipify.org // @connect ip-api.com // @icon https://raw.githubusercontent.com/Yanel85/IP-Location-Finder/refs/heads/main/extension/icon.svg // @license GPL3 // ==/UserScript== (function () { 'use strict'; let currentSelectedText; let tooltip; let locationSpanElementMap = new Map(); const ipIconUrl = "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8cGF0aCBkPSJNMTEuOTk5NyAxLjk5OTk2QzguMTMzOSAxLjk5OTk2IDQuOTk5NzQgNS4xMzQwMiA0Ljk5OTc0IDguOTk5OTZDMjkuMTkzNSA4Ljk5OTk2IDExLjk5OTcgMTYuMDAwMyAxMS45OTk3IDE2LjAwMDNDMTEuOTk5NyAxNi4wMDAzIDE0Ljg2NTQgOC45OTk5NiAyMC45OTk3IDguOTk5OTZDMTkuMTkzNSA4Ljk5OTk2IDE1Ljk5OTcgNS4xMzQwMiAxMS45OTk3IDEuOTk5OTZaIiBmaWxsPSIjMTA5NkRiIi8+CiAgPHBhdGggZD0iTTEyIDExLjk5OTlDMTMuNjU2OSAxMS45OTk5IDE1IDEwLjY1NjggMTUgOC45OTk5M0MxNSA3LjM0MzA5IDEzLjY1NjkgNi4wMDAwNyAxMiA2LjAwMDA3QzEwLjM0MzEgNi4wMDAwNyA5IDcuMzQzMDkgOSA4Ljk5OTkzQzkgMTAuNjU2OCA4LjM0MzE0IDExLjk5OTkgMTIgMTEuOTk5OVoiIGZpbGw9IiMxMDk2RGIiLz4KICA8cGF0aCBkPSJNMTEuOTk5NyAxNy45OTk2QzkuMzI3NjcgMTcuOTk5NiAzLjMzOTYyIDE5LjMzMTcgMy4zMzk2MiAyMi45OTk2VjIzLjOTk2SDIxLjY1OTVWMjIuOTk5NkMzMS42NTk1IDIwLjMzMTcgMTYuNjkxNCAxNy45OTk2IDExLjk5OTcgMTcuOTk5NloiIGZpbGw9IiMxMDk2RGIiLz4KPC9zdmc+"; // API URLs const apiUrls = { "ipapi.co": "https://ipapi.co/{ip}/json", "geoIpify": "https://geo.ipify.org/api/v2/country,city?apiKey=at_9kY03l6G3CExGRBVfAqHQLIvOSj2m&ipAddress={ip}", // 需要替换API Key "ip-api.com": "http://ip-api.com/json/{ip}", "custom": "custom" // 添加自定义选项 }; let currentApiUrl = GM_getValue("apiUrl", "http://ip-api.com/json/{ip}"); const cache = {}; // Event listener for mouseup document.addEventListener('mouseup', handleMouseUp); // Handle mouseup event function handleMouseUp() { removeTooltip(); //removeIcon(currentSelectedText); const selectedText = window.getSelection().toString().trim(); if (selectedText && isValidIP(selectedText)) { currentSelectedText = selectedText; //showIcon(); queryIpLocation(currentSelectedText); } } // Send IP location query async function queryIpLocation(ip) { // 检查缓存 if (cache[ip]) { const { countryCode, city } = cache[ip]; insertLocation(countryCode, city); return; } try { let apiUrl = currentApiUrl; if (currentApiUrl !== apiUrls["custom"]) { apiUrl = currentApiUrl.replace("{ip}", ip); } const response = await GM_xmlhttpRequestPromise(apiUrl); if (response.status !== 200) { throw new Error(`HTTP error! Status: ${response.status}`); } let data = JSON.parse(response.responseText); if (currentApiUrl === apiUrls["geoIpify"]) { cache[ip] = { countryCode: data.location.country, city: data.location.city }; // 缓存结果 insertLocation(data.location.country, data.location.city); } else if (currentApiUrl === apiUrls["ip-api.com"]) { cache[ip] = { countryCode: data.countryCode, city: data.city }; // 缓存结果 insertLocation(data.countryCode, data.city); } else if (currentApiUrl === apiUrls["bigDataCloud"]) { cache[ip] = { countryCode: data.countryCode, city: data.city }; // 缓存结果 insertLocation(data.countryCode, data.city); } else { cache[ip] = { countryCode: data.country, city: data.city }; // 缓存结果 insertLocation(data.country, data.city); } } catch (error) { showTooltip(`error: ${error.message}`, true); } } // Display tooltip message function showTooltip(text, isError = false) { removeTooltip(); const selection = window.getSelection(); if (!selection.rangeCount) return; const range = selection.getRangeAt(0); const rect = range.getBoundingClientRect(); tooltip = document.createElement("div"); tooltip.style.position = "absolute"; tooltip.style.background = isError ? "red" : "lightgreen"; tooltip.style.color = "black"; tooltip.style.padding = "3px"; tooltip.style.border = "1px solid #ccc"; tooltip.style.borderRadius = "4px"; tooltip.style.fontSize = "0.6em"; tooltip.style.zIndex = "9999"; // Ensure tooltip is above all elements tooltip.textContent = text; tooltip.style.top = rect.bottom + window.scrollY + "px"; tooltip.style.left = rect.left + window.scrollX + "px"; document.body.appendChild(tooltip); setTimeout(() => { removeTooltip(); }, 3000); } // Remove tooltip function removeTooltip() { if (tooltip) { tooltip.remove(); tooltip = null; } } // Insert IP location into the page function insertLocation(countryCode, city) { const selection = window.getSelection(); if (!selection.rangeCount) return; const range = selection.getRangeAt(0); const selectedTextNode = range.startContainer; if (selectedTextNode.nodeType !== Node.TEXT_NODE) return; const selectedText = selectedTextNode.textContent; const ipIndex = selectedText.indexOf(currentSelectedText); if (ipIndex === -1) return; removeLocationSpan(currentSelectedText); locationSpanElementMap.set(currentSelectedText, document.createElement('span')); let locationSpan = locationSpanElementMap.get(currentSelectedText); locationSpan.style.color = 'red'; locationSpan.style.fontWeight = 'bold'; locationSpan.style.fontSize = '0.6em'; let locationText = ""; if (countryCode) { const flagIconUrl = `https://flagcdn.com/24x18/${countryCode.toLowerCase()}.png`; const flagImage = `<img src="${flagIconUrl}" style="display:inline-block;vertical-align:middle;margin-right:2px; width:18px; height:13px;">`; locationText = `(${flagImage}${countryCode}`; if (city && city.trim()) { locationText = `${locationText},${city})`; } else { locationText = `${locationText})`; } } locationSpan.innerHTML = locationText; const beforeIpTextNode = document.createTextNode(selectedText.substring(0, ipIndex + currentSelectedText.length)); const afterIpTextNode = document.createTextNode(selectedText.substring(ipIndex + currentSelectedText.length)); selectedTextNode.textContent = ''; selectedTextNode.parentNode.insertBefore(beforeIpTextNode, selectedTextNode); selectedTextNode.parentNode.insertBefore(locationSpan, selectedTextNode); selectedTextNode.parentNode.insertBefore(afterIpTextNode, selectedTextNode); window.getSelection().empty(); } function removeLocationSpan(ipText) { if (locationSpanElementMap.has(ipText)) { let locationSpan = locationSpanElementMap.get(ipText) locationSpan.remove(); locationSpanElementMap.delete(ipText); } } // IP address validation function isValidIP(ip) { const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/; const ipv6Regex = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))$/; const ipv4CidrRegex = /^(\d{1,3}\.){3}\d{1,3}\/([0-9]|[1-2][0-9]|3[0-2])$/; if (ipv4CidrRegex.test(ip)) { ip = ip.split('/')[0]; // 去除CIDR部分 } return ipv4Regex.test(ip) || ipv6Regex.test(ip); } // Helper function for GM_xmlhttpRequest function GM_xmlhttpRequestPromise(url) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ url: url, method: "GET", onload: (response) => { resolve(response); }, onerror: (error) => { reject(error); } }); }); } function createSettingsUI() { const settingsDiv = document.createElement('div') settingsDiv.style.position = "fixed" settingsDiv.style.bottom = "10px" settingsDiv.style.right = "80px" settingsDiv.style.padding = "10px" settingsDiv.style.background = "white" settingsDiv.style.border = "1px solid black" settingsDiv.style.zIndex = "9999999" settingsDiv.style.display = "flex" settingsDiv.style.flexDirection = "column" settingsDiv.style.width = "200px" settingsDiv.style.fontSize = "12px" const apiUrlLabel = document.createElement('label') apiUrlLabel.textContent = "IP Location Finder API option:" const apiUrlSelect = document.createElement('select'); Object.keys(apiUrls).forEach(key => { const option = document.createElement("option"); option.value = apiUrls[key] option.text = key apiUrlSelect.add(option) }) apiUrlSelect.value = currentApiUrl; settingsDiv.append(apiUrlLabel, apiUrlSelect) const customApiInput = document.createElement('input'); customApiInput.type = "text" customApiInput.placeholder = "Replace the API's IP with {ip}." customApiInput.style.display = (currentApiUrl === "custom" ? "block" : "none"); settingsDiv.append(customApiInput); const saveButton = document.createElement("button") saveButton.textContent = "Save" settingsDiv.append(saveButton) saveButton.addEventListener("click", () => { const selectedValue = apiUrlSelect.value if (selectedValue === 'custom') { currentApiUrl = customApiInput.value; } else { currentApiUrl = selectedValue; } GM_setValue("apiUrl", currentApiUrl); customApiInput.style.display = (currentApiUrl === "custom" ? "block" : "none"); statusDiv.textContent = "Settings Saved" setTimeout(() => { statusDiv.textContent = ""; }, 1000); }) const statusDiv = document.createElement("div") settingsDiv.append(statusDiv) apiUrlSelect.addEventListener("change", (event) => { const selectedValue = event.target.value; customApiInput.style.display = (selectedValue === "custom" ? "block" : "none"); }) document.body.appendChild(settingsDiv) } createSettingsUI(); })();