Sniffies Integrated Location Spoofer

Integrate location spoofing with Sniffies' existing Travel Mode button using natural language inputs.

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      1.3
// @description  Integrate location spoofing with Sniffies' existing Travel Mode button using natural language inputs.
// @author       Your Name
// @match        https://sniffies.com/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    // Configuration
    const STORAGE_KEY = 'sniffiesLocationSpoofer';
    const GEOCODING_API_URL = 'https://nominatim.openstreetmap.org/search';

    // Utility Functions
    function getStoredLocation() {
        const data = localStorage.getItem(STORAGE_KEY);
        return data ? JSON.parse(data) : { enabled: false, locationName: '', latitude: 37.7749, longitude: -122.4194 }; // Default: San Francisco
    }

    function setStoredLocation(locationData) {
        localStorage.setItem(STORAGE_KEY, JSON.stringify(locationData));
    }

    // Geocoding Function
    async function geocodeLocation(locationName) {
        const params = new URLSearchParams({
            q: locationName,
            format: 'json',
            limit: 1
        });

        try {
            const response = await fetch(`${GEOCODING_API_URL}?${params.toString()}`, {
                method: 'GET',
                headers: {
                    'Accept': 'application/json'
                }
            });

            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) {
            throw new Error(`Failed to geocode location: ${error.message}`);
        }
    }

    // Override Geolocation API
    function overrideGeolocation(spoofedLocation) {
        const originalGeolocation = navigator.geolocation;

        const spoofedGeolocation = {
            getCurrentPosition: function (success, error, options) {
                success({
                    coords: {
                        latitude: spoofedLocation.latitude,
                        longitude: spoofedLocation.longitude,
                        altitude: null,
                        accuracy: 100,
                        altitudeAccuracy: null,
                        heading: null,
                        speed: null
                    },
                    timestamp: Date.now()
                });
            },
            watchPosition: function (success, error, options) {
                // For simplicity, behave like getCurrentPosition
                return originalGeolocation.watchPosition.call(navigator.geolocation, success, error, options);
            },
            clearWatch: function (id) {
                originalGeolocation.clearWatch.call(navigator.geolocation, id);
            }
        };

        Object.defineProperty(navigator, 'geolocation', {
            get: function () {
                return spoofedGeolocation;
            }
        });
    }

    // UI Elements

    // Create Modal Dialog
    function createModal() {
        // Overlay
        const overlay = document.createElement('div');
        overlay.id = 'spoofModalOverlay';
        overlay.style.position = 'fixed';
        overlay.style.top = '0';
        overlay.style.left = '0';
        overlay.style.width = '100%';
        overlay.style.height = '100%';
        overlay.style.backgroundColor = 'rgba(0, 0, 0, 0.5)';
        overlay.style.display = 'none';
        overlay.style.alignItems = 'center';
        overlay.style.justifyContent = 'center';
        overlay.style.zIndex = '10000';

        // Modal Container
        const modal = document.createElement('div');
        modal.id = 'spoofModal';
        modal.style.width = '90%';
        modal.style.maxWidth = '400px';
        modal.style.backgroundColor = '#fff';
        modal.style.borderRadius = '8px';
        modal.style.padding = '20px';
        modal.style.boxShadow = '0 5px 15px rgba(0,0,0,0.3)';
        modal.style.position = 'relative';

        // Close Button
        const closeButton = document.createElement('span');
        closeButton.innerHTML = '×';
        closeButton.style.position = 'absolute';
        closeButton.style.top = '10px';
        closeButton.style.right = '15px';
        closeButton.style.fontSize = '24px';
        closeButton.style.cursor = 'pointer';
        closeButton.style.color = '#aaa';
        closeButton.style.transition = 'color 0.3s';
        closeButton.addEventListener('mouseover', () => {
            closeButton.style.color = '#000';
        });
        closeButton.addEventListener('mouseout', () => {
            closeButton.style.color = '#aaa';
        });
        modal.appendChild(closeButton);

        // Title
        const title = document.createElement('h2');
        title.textContent = 'Location Spoofer';
        title.style.marginTop = '0';
        title.style.textAlign = 'center';
        modal.appendChild(title);

        // Enable Checkbox
        const enableLabel = document.createElement('label');
        enableLabel.style.display = 'block';
        enableLabel.style.marginBottom = '15px';
        enableLabel.style.cursor = 'pointer';

        const enableCheckbox = document.createElement('input');
        enableCheckbox.type = 'checkbox';
        enableCheckbox.id = 'spoofEnableCheckbox';
        enableCheckbox.style.marginRight = '8px';
        enableLabel.appendChild(enableCheckbox);

        const enableText = document.createElement('span');
        enableText.textContent = 'Enable Location Spoofing';
        enableLabel.appendChild(enableText);

        modal.appendChild(enableLabel);

        // Location Name Input
        const locationLabel = document.createElement('label');
        locationLabel.textContent = 'Enter Location:';
        locationLabel.style.display = 'block';
        locationLabel.style.marginBottom = '5px';
        modal.appendChild(locationLabel);

        const locationInput = document.createElement('input');
        locationInput.type = 'text';
        locationInput.id = 'spoofLocationName';
        locationInput.placeholder = 'e.g., Washington DC, London, Tokyo';
        locationInput.style.width = '100%';
        locationInput.style.padding = '10px';
        locationInput.style.marginBottom = '15px';
        locationInput.style.border = '1px solid #ccc';
        locationInput.style.borderRadius = '4px';
        modal.appendChild(locationInput);

        // Preset Locations
        const presetLabel = document.createElement('label');
        presetLabel.textContent = 'Or Select a Preset Location:';
        presetLabel.style.display = 'block';
        presetLabel.style.marginBottom = '5px';
        modal.appendChild(presetLabel);

        const presetSelect = document.createElement('select');
        presetSelect.id = 'spoofPresetSelect';
        presetSelect.style.width = '100%';
        presetSelect.style.padding = '10px';
        presetSelect.style.marginBottom = '15px';
        presetSelect.style.border = '1px solid #ccc';
        presetSelect.style.borderRadius = '4px';

        const presets = {
            'Select a preset...': '',
            'New York City, NY, USA': 'New York City, NY, USA',
            'Los Angeles, CA, USA': 'Los Angeles, CA, USA',
            'London, UK': 'London, UK',
            'Tokyo, Japan': 'Tokyo, Japan',
            'Sydney, Australia': 'Sydney, Australia',
            'Paris, France': 'Paris, France',
            'Berlin, Germany': 'Berlin, Germany',
            'Toronto, Canada': 'Toronto, Canada',
            'São Paulo, Brazil': 'São Paulo, Brazil',
            'Cape Town, South Africa': 'Cape Town, South Africa',
            'Washington DC, USA': 'Washington DC, USA'
        };

        for (const [name, query] of Object.entries(presets)) {
            const option = document.createElement('option');
            option.value = query;
            option.textContent = name;
            presetSelect.appendChild(option);
        }

        modal.appendChild(presetSelect);

        // Save Button
        const saveButton = document.createElement('button');
        saveButton.textContent = 'Save & Apply';
        saveButton.style.width = '100%';
        saveButton.style.padding = '10px';
        saveButton.style.backgroundColor = '#007bff';
        saveButton.style.color = '#fff';
        saveButton.style.border = 'none';
        saveButton.style.borderRadius = '4px';
        saveButton.style.cursor = 'pointer';
        saveButton.style.fontSize = '16px';
        saveButton.style.transition = 'background-color 0.3s';
        saveButton.addEventListener('mouseover', () => {
            saveButton.style.backgroundColor = '#0069d9';
        });
        saveButton.addEventListener('mouseout', () => {
            saveButton.style.backgroundColor = '#007bff';
        });
        modal.appendChild(saveButton);

        // Status Message
        const statusMsg = document.createElement('div');
        statusMsg.id = 'spoofStatusMsg';
        statusMsg.style.marginTop = '15px';
        statusMsg.style.fontSize = '14px';
        statusMsg.style.textAlign = 'center';
        modal.appendChild(statusMsg);

        overlay.appendChild(modal);
        document.body.appendChild(overlay);

        return { overlay, modal, closeButton, enableCheckbox, locationInput, presetSelect, saveButton, statusMsg };
    }

    // Initialize Modal and Event Listeners
    function initializeUI() {
        const { overlay, modal, closeButton, enableCheckbox, locationInput, presetSelect, saveButton, statusMsg } = createModal();

        // Function to open modal
        function openModal() {
            overlay.style.display = 'flex';
        }

        // Function to close modal
        function closeModal() {
            overlay.style.display = 'none';
        }

        // Attach event listeners to close modal
        closeButton.addEventListener('click', closeModal);
        overlay.addEventListener('click', (e) => {
            if (e.target === overlay) {
                closeModal();
            }
        });

        // Handle Preset Selection
        presetSelect.addEventListener('change', function () {
            const selected = presetSelect.value;
            if (selected) {
                locationInput.value = selected;
            }
        });

        // Handle Save Button Click
        saveButton.addEventListener('click', async function () {
            const enabled = enableCheckbox.checked;
            const locationName = locationInput.value.trim();

            if (enabled && locationName === '') {
                statusMsg.textContent = 'Please enter a location name or select a preset.';
                statusMsg.style.color = 'red';
                return;
            }

            if (enabled) {
                statusMsg.textContent = 'Resolving location...';
                statusMsg.style.color = '#ffc107'; // Amber

                try {
                    const coords = await geocodeLocation(locationName);
                    setStoredLocation({
                        enabled: true,
                        locationName: locationName,
                        latitude: coords.latitude,
                        longitude: coords.longitude
                    });
                    statusMsg.textContent = 'Location spoofed successfully!';
                    statusMsg.style.color = '#28a745'; // Green

                    // Override Geolocation
                    overrideGeolocation({
                        latitude: coords.latitude,
                        longitude: coords.longitude
                    });

                    // Optionally, reload the page to apply changes
                    setTimeout(() => {
                        window.location.reload();
                    }, 1500);
                } catch (error) {
                    statusMsg.textContent = error.message;
                    statusMsg.style.color = 'red';
                }
            } else {
                setStoredLocation({
                    enabled: false,
                    locationName: '',
                    latitude: 37.7749,
                    longitude: -122.4194
                });
                statusMsg.textContent = 'Location spoofing disabled.';
                statusMsg.style.color = '#6c757d'; // Gray

                // Restore original Geolocation
                setTimeout(() => {
                    window.location.reload();
                }, 1000);
            }
        });

        // Observe DOM changes to target the existing button
        const observer = new MutationObserver((mutations, obs) => {
            const targetButton = document.querySelector('i[data-testid="travelModeIcon"]');
            if (targetButton) {
                const parentButton = targetButton.closest('.lower-map-icon.travel-on-map');
                if (parentButton) {
                    // Prevent multiple initializations
                    if (!parentButton.dataset.spoofInitialized) {
                        parentButton.dataset.spoofInitialized = 'true';

                        // Modify button appearance to indicate spoofing functionality
                        targetButton.classList.remove('fa-plane');
                        targetButton.classList.add('fa-map-marker-alt'); // Change icon to map marker
                        parentButton.title = 'Enable Location Spoofing';
                        parentButton.setAttribute('aria-label', 'Enable Location Spoofing');

                        // Attach click event to open spoofing modal
                        parentButton.addEventListener('click', (e) => {
                            e.preventDefault();
                            e.stopPropagation();
                            openModal();
                        });

                        // Optionally, disable original functionality if needed
                        // For example, remove existing click listeners
                        // This depends on how Sniffies implements the original button
                    }
                }
                // Once the button is found and initialized, disconnect the observer
                if (targetButton) {
                    obs.disconnect();
                }
            }
        });

        // Start observing the body for added nodes
        observer.observe(document.body, { childList: true, subtree: true });

        // Initialize UI with stored values
        const storedLocation = getStoredLocation();
        if (storedLocation.enabled) {
            overrideGeolocation({
                latitude: storedLocation.latitude,
                longitude: storedLocation.longitude
            });
        }
    }

    // Presets for checking preset selection
    const presets = {
        'Select a preset...': '',
        'New York City, NY, USA': 'New York City, NY, USA',
        'Los Angeles, CA, USA': 'Los Angeles, CA, USA',
        'London, UK': 'London, UK',
        'Tokyo, Japan': 'Tokyo, Japan',
        'Sydney, Australia': 'Sydney, Australia',
        'Paris, France': 'Paris, France',
        'Berlin, Germany': 'Berlin, Germany',
        'Toronto, Canada': 'Toronto, Canada',
        'São Paulo, Brazil': 'São Paulo, Brazil',
        'Cape Town, South Africa': 'Cape Town, South Africa',
        'Washington DC, USA': 'Washington DC, USA'
    };

    // Initialize the script
    function init() {
        initializeUI();
    }

    // Wait for the DOM to load
    window.addEventListener('load', init);
})();