Sniffies Integrated Location Spoofer

Integrate location spoofing with Sniffies' existing Travel Mode button using natural language inputs. Disable native Travel Mode and provide a custom spoofing popup with reset functionality in dark mode, including customizable quick location presets and an integrated spoofing indicator.

Tính đến 15-12-2024. Xem phiên bản mới nhất.

// ==UserScript==
// @name         Sniffies Integrated Location Spoofer
// @namespace    https://sniffies.com/
// @version      2.8
// @description  Integrate location spoofing with Sniffies' existing Travel Mode button using natural language inputs. Disable native Travel Mode and provide a custom spoofing popup with reset functionality in dark mode, including customizable quick location presets and an integrated spoofing indicator.
// @author       Your Name
// @match        https://sniffies.com/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    // Constants
    const STORAGE_KEY = 'sniffiesLocationSpoofer';
    const PRESETS_KEY = 'sniffiesLocationPresets';
    const GEOCODING_API_URL = 'https://nominatim.openstreetmap.org/search';
    const MODAL_ID = 'spoofModalOverlay';
    const TRAVEL_MODE_ICON_SELECTOR = 'i[data-testid="travelModeIcon"]';

    // Default Preset Locations
    const DEFAULT_PRESET_LOCATIONS = [
        { name: 'New York, USA', latitude: 40.7128, longitude: -74.0060 },
        { name: 'London, UK', latitude: 51.5074, longitude: -0.1278 },
        { name: 'Tokyo, Japan', latitude: 35.6762, longitude: 139.6503 },
        { name: 'Sydney, Australia', latitude: -33.8688, longitude: 151.2093 },
        { name: 'Paris, France', latitude: 48.8566, longitude: 2.3522 },
        { name: 'Berlin, Germany', latitude: 52.5200, longitude: 13.4050 },
    ];

    // Default Location Data
    const DEFAULT_LOCATION = {
        enabled: false,
        locationName: '',
        latitude: null,
        longitude: null,
    };

    /**
     * Retrieves stored location data from localStorage.
     * @returns {Object} Stored location data or default if none exists.
     */
    function getStoredLocation() {
        try {
            const data = localStorage.getItem(STORAGE_KEY);
            return data ? JSON.parse(data) : { ...DEFAULT_LOCATION };
        } catch (error) {
            console.error('Error retrieving stored location:', error);
            return { ...DEFAULT_LOCATION };
        }
    }

    /**
     * Saves location data to localStorage.
     * @param {Object} locationData - The location data to store.
     */
    function setStoredLocation(locationData) {
        try {
            localStorage.setItem(STORAGE_KEY, JSON.stringify(locationData));
        } catch (error) {
            console.error('Error setting stored location:', error);
        }
    }

    /**
     * Clears stored location data from localStorage.
     */
    function clearStoredLocation() {
        try {
            localStorage.removeItem(STORAGE_KEY);
        } catch (error) {
            console.error('Error clearing stored location:', error);
        }
    }

    /**
     * Retrieves preset locations from localStorage.
     * @returns {Array} Array of preset location objects.
     */
    function getPresetLocations() {
        try {
            const data = localStorage.getItem(PRESETS_KEY);
            return data ? JSON.parse(data) : [...DEFAULT_PRESET_LOCATIONS];
        } catch (error) {
            console.error('Error retrieving preset locations:', error);
            return [...DEFAULT_PRESET_LOCATIONS];
        }
    }

    /**
     * Saves preset locations to localStorage.
     * @param {Array} presets - Array of preset location objects.
     */
    function setPresetLocations(presets) {
        try {
            localStorage.setItem(PRESETS_KEY, JSON.stringify(presets));
        } catch (error) {
            console.error('Error setting preset locations:', error);
        }
    }

    /**
     * Geocodes a location name to latitude and longitude using Nominatim.
     * @param {string} locationName - The location name to geocode.
     * @returns {Promise<Object>} A promise that resolves to an object containing latitude and longitude.
     */
    async function geocodeLocation(locationName) {
        const params = new URLSearchParams({
            q: locationName,
            format: 'json',
            limit: 1,
        });

        try {
            const response = await fetch(`${GEOCODING_API_URL}?${params.toString()}`, {
                headers: {
                    'User-Agent': 'SniffiesLocationSpoofer/1.0 (+https://yourwebsite.com/)',
                },
            });
            if (!response.ok) {
                throw new Error(`Geocoding API error: ${response.statusText}`);
            }
            const data = await response.json();
            if (data.length === 0) {
                throw new Error('Location not found. Please try a different query.');
            }

            return {
                latitude: parseFloat(data[0].lat),
                longitude: parseFloat(data[0].lon),
            };
        } catch (error) {
            console.error(`Failed to geocode location: ${error.message}`);
            throw new Error('Failed to fetch location. Please try again later.');
        }
    }

    /**
     * Overrides the navigator.geolocation object with spoofed coordinates.
     * @param {Object} spoofedLocation - The spoofed latitude and longitude.
     */
    function overrideGeolocation(spoofedLocation) {
        // Store the original navigator.geolocation if not already stored
        if (!window.__originalGeolocation) {
            window.__originalGeolocation = navigator.geolocation;
        }

        const spoofedGeolocation = {
            getCurrentPosition: function (success, error, options) {
                if (!spoofedLocation.latitude || !spoofedLocation.longitude) {
                    error && error(new Error('Spoofed location is invalid.'));
                    return;
                }
                success && success({
                    coords: {
                        latitude: spoofedLocation.latitude,
                        longitude: spoofedLocation.longitude,
                        accuracy: 100,
                        altitude: null,
                        altitudeAccuracy: null,
                        heading: null,
                        speed: null,
                    },
                    timestamp: Date.now(),
                });
            },
            watchPosition: function (success, error, options) {
                if (!spoofedLocation.latitude || !spoofedLocation.longitude) {
                    error && error(new Error('Spoofed location is invalid.'));
                    return;
                }
                // Return a mock watch ID
                return setInterval(() => {
                    success && success({
                        coords: {
                            latitude: spoofedLocation.latitude,
                            longitude: spoofedLocation.longitude,
                            accuracy: 100,
                            altitude: null,
                            altitudeAccuracy: null,
                            heading: null,
                            speed: null,
                        },
                        timestamp: Date.now(),
                    });
                }, 1000);
            },
            clearWatch: function (id) {
                clearInterval(id);
            },
            // Other geolocation methods can be added if necessary
        };

        // Override the navigator.geolocation
        Object.defineProperty(navigator, 'geolocation', {
            get: () => spoofedGeolocation,
            configurable: true,
        });
    }

    /**
     * Restores the original navigator.geolocation object.
     */
    function restoreOriginalGeolocation() {
        if (window.__originalGeolocation) {
            Object.defineProperty(navigator, 'geolocation', {
                get: () => window.__originalGeolocation,
                configurable: true,
            });
            delete window.__originalGeolocation;
        }
    }

    /**
     * Creates and returns the spoofing modal element with enhanced dark mode styling and customizable presets.
     * @returns {HTMLElement} The modal overlay element.
     */
    function createModal() {
        // Create overlay
        const overlay = document.createElement('div');
        overlay.id = MODAL_ID;
        overlay.style.cssText = `
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background-color: rgba(11, 17, 31, 0.85); /* Reduced opacity for less darkened background */
            display: none;
            align-items: center;
            justify-content: center;
            z-index: 10000;
        `;

        // Create modal container
        const modal = document.createElement('div');
        modal.style.cssText = `
            background-color: #111d33;
            border-radius: 8px;
            padding: 20px 25px;
            width: 90%;
            max-width: 400px;
            position: relative;
            box-shadow: 0 4px 16px rgba(0,0,0,0.5);
            font-family: Arial, sans-serif;
            color: #ffffff; /* White text for readability */
        `;

        // Close button
        const closeButton = document.createElement('span');
        closeButton.innerHTML = '&times;';
        closeButton.style.cssText = `
            position: absolute;
            top: 15px;
            right: 20px;
            font-size: 24px;
            cursor: pointer;
            color: #ffffff;
            transition: color 0.3s;
        `;
        closeButton.addEventListener('mouseover', () => {
            closeButton.style.color = '#304065';
        });
        closeButton.addEventListener('mouseout', () => {
            closeButton.style.color = '#ffffff';
        });
        closeButton.addEventListener('click', () => {
            overlay.style.display = 'none';
        });

        // Modal title aligned to the left
        const title = document.createElement('h2');
        title.textContent = 'Location Spoofer';
        title.style.cssText = `
            margin: 0;
            margin-bottom: 15px;
            color: #ffffff;
            font-size: 20px;
            text-align: left;
        `;

        // Spoofing Indicator Container
        const spoofingIndicatorContainer = document.createElement('div');
        spoofingIndicatorContainer.id = 'spoofingIndicator';
        spoofingIndicatorContainer.style.cssText = `
            margin-bottom: 15px;
            padding: 10px;
            background-color: #304065;
            border-radius: 10px;
            display: none; /* Hidden by default */
            align-items: center;
            justify-content: space-between;
            cursor: pointer;
            transition: background-color 0.3s;
        `;
        spoofingIndicatorContainer.title = 'Click to disable spoofing';
        spoofingIndicatorContainer.addEventListener('mouseover', () => {
            spoofingIndicatorContainer.style.backgroundColor = '#1e2a4a';
        });
        spoofingIndicatorContainer.addEventListener('mouseout', () => {
            spoofingIndicatorContainer.style.backgroundColor = '#304065';
        });
        spoofingIndicatorContainer.addEventListener('click', () => {
            if (confirm('Do you want to disable location spoofing?')) {
                clearStoredLocation();
                restoreOriginalGeolocation();
                updateSpoofingIndicator();
                alert('Location spoofing has been disabled.');
            }
        });

        const spoofingStatus = document.createElement('span');
        spoofingStatus.textContent = 'Spoofing: On';
        spoofingStatus.style.cssText = `
            font-weight: bold;
        `;

        const spoofingLocation = document.createElement('span');
        spoofingLocation.id = 'spoofingLocation';
        spoofingLocation.textContent = ''; // To be updated dynamically

        spoofingIndicatorContainer.appendChild(spoofingStatus);
        spoofingIndicatorContainer.appendChild(spoofingLocation);

        // Preset Locations Container
        const presetsContainer = document.createElement('div');
        presetsContainer.style.cssText = `
            display: flex;
            flex-wrap: wrap;
            gap: 10px;
            justify-content: center;
            margin-bottom: 15px;
        `;

        // Function to create a preset button
        function createPresetButton(preset) {
            const presetButton = document.createElement('button');
            presetButton.textContent = preset.name;
            presetButton.style.cssText = `
                padding: 6px 12px;
                background-color: #304065;
                color: #ffffff;
                border: none;
                border-radius: 20px;
                cursor: pointer;
                font-size: 14px;
                transition: background-color 0.3s;
                display: flex;
                align-items: center;
                gap: 5px;
            `;
            presetButton.addEventListener('mouseover', () => {
                presetButton.style.backgroundColor = '#1e2a4a';
            });
            presetButton.addEventListener('mouseout', () => {
                presetButton.style.backgroundColor = '#304065';
            });
            presetButton.addEventListener('click', () => {
                setStoredLocation({
                    enabled: true,
                    locationName: preset.name,
                    latitude: preset.latitude,
                    longitude: preset.longitude,
                });
                overrideGeolocation({
                    latitude: preset.latitude,
                    longitude: preset.longitude,
                });
                updateSpoofingIndicator();
                alert(`Location spoofed to ${preset.name}.`);
            });

            // Remove button for custom presets only
            if (!DEFAULT_PRESET_LOCATIONS.some(p => p.name === preset.name)) {
                const removeButton = document.createElement('span');
                removeButton.innerHTML = '&times;';
                removeButton.style.cssText = `
                    font-size: 12px;
                    cursor: pointer;
                    color: #ff4d4d;
                    margin-left: 5px;
                `;
                removeButton.addEventListener('click', (e) => {
                    e.stopPropagation();
                    removePreset(preset.name);
                });
                presetButton.appendChild(removeButton);
            }

            return presetButton;
        }

        // Load and display preset buttons
        const presets = getPresetLocations();
        presets.forEach(preset => {
            const presetButton = createPresetButton(preset);
            presetsContainer.appendChild(presetButton);
        });

        // Add Preset Button (with "+" sign)
        const addPresetButton = document.createElement('button');
        addPresetButton.innerHTML = '+';
        addPresetButton.title = 'Add Preset';
        addPresetButton.style.cssText = `
            padding: 6px 12px;
            background-color: #304065;
            color: #ffffff;
            border: none;
            border-radius: 20px;
            cursor: pointer;
            font-size: 16px;
            transition: background-color 0.3s;
            display: flex;
            align-items: center;
            justify-content: center;
            width: 36px;
            height: 36px;
        `;
        addPresetButton.addEventListener('mouseover', () => {
            addPresetButton.style.backgroundColor = '#1e2a4a';
        });
        addPresetButton.addEventListener('mouseout', () => {
            addPresetButton.style.backgroundColor = '#304065';
        });
        addPresetButton.addEventListener('click', () => {
            // Prompt the user to enter a new preset location
            const presetName = prompt('Enter the name of the new preset location:');
            if (presetName) {
                addNewPreset(presetName.trim());
            }
        });
        presetsContainer.appendChild(addPresetButton);

        /**
         * Adds a new preset location.
         * @param {string} presetName - The name of the preset location.
         */
        async function addNewPreset(presetName) {
            if (!presetName) {
                alert('Preset name cannot be empty.');
                return;
            }
            const currentPresets = getPresetLocations();
            // Check for duplicates
            if (currentPresets.some(p => p.name.toLowerCase() === presetName.toLowerCase())) {
                alert('A preset with this name already exists.');
                return;
            }
            try {
                const coords = await geocodeLocation(presetName);
                const newPreset = {
                    name: presetName,
                    latitude: coords.latitude,
                    longitude: coords.longitude,
                };
                currentPresets.push(newPreset);
                setPresetLocations(currentPresets);
                // Add the new preset button
                const newPresetButton = createPresetButton(newPreset);
                // Insert before the addPresetButton
                presetsContainer.insertBefore(newPresetButton, addPresetButton);
                alert(`Preset "${presetName}" added successfully.`);
            } catch (error) {
                alert(error.message);
            }
        }

        /**
         * Removes a preset location by name.
         * @param {string} presetName - The name of the preset to remove.
         */
        function removePreset(presetName) {
            const currentPresets = getPresetLocations();
            const updatedPresets = currentPresets.filter(p => p.name !== presetName);
            setPresetLocations(updatedPresets);
            // Remove the preset button from the modal
            const allPresetButtons = document.querySelectorAll(`#${MODAL_ID} div > button`);
            allPresetButtons.forEach(button => {
                if (button.textContent.startsWith(presetName)) {
                    button.remove();
                }
            });
            alert(`Preset "${presetName}" removed successfully.`);
        }

        // Location Input
        const locationInput = document.createElement('input');
        locationInput.type = 'text';
        locationInput.id = 'locationInput';
        locationInput.placeholder = 'Enter location...';
        locationInput.style.cssText = `
            width: 100%;
            padding: 10px 15px;
            margin: 10px 0;
            border: none;
            border-radius: 4px;
            background-color: #0b111f;
            color: #ffffff;
            font-size: 16px;
        `;
        locationInput.addEventListener('focus', () => {
            locationInput.style.backgroundColor = '#304065';
            locationInput.style.outline = 'none';
        });
        locationInput.addEventListener('blur', () => {
            locationInput.style.backgroundColor = '#0b111f';
        });

        // Action Buttons Container
        const buttonsContainer = document.createElement('div');
        buttonsContainer.style.cssText = `
            display: flex;
            justify-content: space-between;
            flex-wrap: wrap;
            gap: 10px;
            margin-top: 10px;
        `;

        // Save & Apply Button
        const saveButton = document.createElement('button');
        saveButton.textContent = 'Save & Apply';
        saveButton.style.cssText = `
            flex: 1;
            padding: 10px 0;
            background-color: #304065;
            color: #ffffff;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 16px;
            transition: background-color 0.3s;
        `;
        saveButton.addEventListener('mouseover', () => {
            saveButton.style.backgroundColor = '#1e2a4a';
        });
        saveButton.addEventListener('mouseout', () => {
            saveButton.style.backgroundColor = '#304065';
        });
        saveButton.addEventListener('click', async () => {
            const locationName = locationInput.value.trim();
            if (!locationName) {
                alert('Please enter a location.');
                return;
            }
            try {
                const coords = await geocodeLocation(locationName);
                setStoredLocation({
                    enabled: true,
                    locationName,
                    latitude: coords.latitude,
                    longitude: coords.longitude,
                });
                overrideGeolocation({
                    latitude: coords.latitude,
                    longitude: coords.longitude,
                });
                updateSpoofingIndicator();
                alert('Location spoofing applied.');
            } catch (error) {
                alert(error.message);
            }
        });

        // Reset Button
        const resetButton = document.createElement('button');
        resetButton.textContent = 'Reset';
        resetButton.style.cssText = `
            flex: 1;
            padding: 10px 0;
            background-color: #dc3545;
            color: #ffffff;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 16px;
            transition: background-color 0.3s;
        `;
        resetButton.addEventListener('mouseover', () => {
            resetButton.style.backgroundColor = '#a71d2a';
        });
        resetButton.addEventListener('mouseout', () => {
            resetButton.style.backgroundColor = '#dc3545';
        });
        resetButton.addEventListener('click', () => {
            if (confirm('Are you sure you want to reset to your actual location?')) {
                clearStoredLocation();
                restoreOriginalGeolocation();
                updateSpoofingIndicator();
                alert('Location reset to actual position.');
            }
        });

        // Append buttons to buttons container
        buttonsContainer.appendChild(saveButton);
        buttonsContainer.appendChild(resetButton);

        // Append elements to modal
        modal.appendChild(closeButton);
        modal.appendChild(title);
        modal.appendChild(spoofingIndicatorContainer);
        modal.appendChild(presetsContainer);
        modal.appendChild(locationInput);
        modal.appendChild(buttonsContainer);

        // Append modal to overlay
        overlay.appendChild(modal);

        // Append overlay to body
        document.body.appendChild(overlay);

        return overlay;
    }

    /**
     * Attaches the spoofing modal to the Travel Mode button.
     */
    function attachModalToTravelMode() {
        const overlay = createModal();

        // Function to handle Travel Mode button clicks
        const handleTravelModeClick = (event) => {
            event.preventDefault();
            event.stopPropagation();
            overlay.style.display = 'flex';
        };

        // Wait for the Travel Mode icon to be available in the DOM
        const observer = new MutationObserver((mutations, obs) => {
            const travelModeIcon = document.querySelector(TRAVEL_MODE_ICON_SELECTOR);
            if (travelModeIcon) {
                // Find the closest clickable parent (assuming it's a button or clickable element)
                const travelModeButton = travelModeIcon.closest('button, a, div');
                if (travelModeButton) {
                    // Remove existing event listeners by cloning the node
                    const newTravelModeButton = travelModeButton.cloneNode(true);
                    travelModeButton.parentNode.replaceChild(newTravelModeButton, travelModeButton);

                    // Attach new event listener
                    newTravelModeButton.addEventListener('click', handleTravelModeClick);

                    // Optionally, change the icon to indicate spoofing functionality
                    // Example: Change plane icon to a location pin
                    travelModeIcon.classList.remove('fa-plane');
                    travelModeIcon.classList.add('fa-map-marker-alt'); // Ensure FontAwesome supports this class
                }
                obs.disconnect();
            }
        });

        observer.observe(document.body, { childList: true, subtree: true });
    }

    /**
     * Updates the spoofing indicator within the modal based on spoofing status.
     */
    function updateSpoofingIndicator() {
        const storedLocation = getStoredLocation();
        const indicatorContainer = document.getElementById('spoofingIndicator');
        const spoofingLocation = document.getElementById('spoofingLocation');

        if (storedLocation.enabled && storedLocation.locationName) {
            indicatorContainer.style.display = 'flex';
            spoofingLocation.textContent = `📍 ${storedLocation.locationName}`;
        } else {
            indicatorContainer.style.display = 'none';
            spoofingLocation.textContent = '';
        }
    }

    /**
     * Overrides the Geolocation API if spoofing is enabled.
     */
    function initSpoofing() {
        const storedLocation = getStoredLocation();

        if (storedLocation.enabled && storedLocation.latitude && storedLocation.longitude) {
            overrideGeolocation({
                latitude: storedLocation.latitude,
                longitude: storedLocation.longitude,
            });
            updateSpoofingIndicator();
            console.log(`Location spoofed to: ${storedLocation.locationName} (${storedLocation.latitude}, ${storedLocation.longitude})`);
        } else {
            // Ensure the original Geolocation API is restored
            restoreOriginalGeolocation();
            updateSpoofingIndicator();
        }
    }

    /**
     * Initializes the userscript by setting up the UI and spoofing functionality.
     */
    function init() {
        attachModalToTravelMode();
        initSpoofing();
    }

    // Wait for the page to fully load before initializing
    window.addEventListener('load', init);
})();