IP Location Finder

Finds the geographical location of IP addresses on a page.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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();
})();