Sniffies Native Location Spoofer (Integrated)

Integrate location spoofing directly into Sniffies' Travel Mode UI (city search, draggable pin).

נכון ליום 15-12-2024. ראה הגרסה האחרונה.

// ==UserScript==
// @name         Sniffies Native Location Spoofer (Integrated)
// @namespace    https://sniffies.com/
// @version      2.0
// @description  Integrate location spoofing directly into Sniffies' Travel Mode UI (city search, draggable pin).
// @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';
    const TRAVEL_MODE_INPUT_SELECTOR = 'input[data-testid="travel-mode-location-input"]';
    const MAP_PIN_SELECTOR = '.map-pin-selector'; 
    const TRAVEL_MODE_BUTTON_SELECTOR = '.lower-map-icon.travel-on-map';

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

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

    async function geocodeLocation(locationName) {
        const params = new URLSearchParams({
            q: locationName,
            format: 'json',
            limit: 1
        });
        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)
        };
    }

    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) {
                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;
            }
        });
    }

    function applySpoofIfEnabled() {
        const stored = getStoredLocation();
        if (stored.enabled) {
            overrideGeolocation({ latitude: stored.latitude, longitude: stored.longitude });
        }
    }

    // Attach a handler to the Travel Mode city search input
    function attachSearchInputHandler(input) {
        let lastQuery = '';
        input.addEventListener('change', async () => {
            const query = input.value.trim();
            if (query && query !== lastQuery) {
                lastQuery = query;
                try {
                    const coords = await geocodeLocation(query);
                    const stored = getStoredLocation();
                    stored.enabled = true;
                    stored.locationName = query;
                    stored.latitude = coords.latitude;
                    stored.longitude = coords.longitude;
                    setStoredLocation(stored);
                    overrideGeolocation({ latitude: coords.latitude, longitude: coords.longitude });
                } catch (e) {
                    console.error('Failed to geocode:', e.message);
                }
            }
        });
    }

    // Observe when the Travel Mode UI is available, then attach handlers
    function waitForTravelModeUI() {
        const observer = new MutationObserver((mutations, obs) => {
            const searchInput = document.querySelector(TRAVEL_MODE_INPUT_SELECTOR);
            if (searchInput) {
                obs.disconnect();
                attachSearchInputHandler(searchInput);
            }
        });
        observer.observe(document.body, { childList: true, subtree: true });
    }

    // Watch the draggable pin for updates in location
    function watchPinPosition() {
        const pinObserver = new MutationObserver(() => {
            const pinElement = document.querySelector(MAP_PIN_SELECTOR);
            if (pinElement) {
                const lat = parseFloat(pinElement.getAttribute('data-lat'));
                const lng = parseFloat(pinElement.getAttribute('data-lng'));
                if (!isNaN(lat) && !isNaN(lng)) {
                    const stored = getStoredLocation();
                    stored.enabled = true;
                    stored.latitude = lat;
                    stored.longitude = lng;
                    setStoredLocation(stored);
                    overrideGeolocation({ latitude: lat, longitude: lng });
                }
            }
        });

        // Observe attribute changes in entire body, filter in callback
        pinObserver.observe(document.body, { attributes: true, subtree: true, childList: true });
    }

    // Optional: Add a toggle button next to Travel Mode to enable/disable spoofing
    function addSpoofToggle() {
        const checkButtonInterval = setInterval(() => {
            const travelModeButton = document.querySelector(TRAVEL_MODE_BUTTON_SELECTOR);
            if (travelModeButton) {
                clearInterval(checkButtonInterval);

                // Create a toggle button
                const btn = document.createElement('button');
                btn.textContent = 'Spoof: OFF';
                btn.style.marginLeft = '10px';
                btn.style.cursor = 'pointer';
                btn.style.padding = '5px';
                btn.style.fontSize = '14px';
                btn.style.background = '#007bff';
                btn.style.color = '#fff';
                btn.style.border = 'none';
                btn.style.borderRadius = '4px';

                const updateButtonState = () => {
                    const stored = getStoredLocation();
                    btn.textContent = `Spoof: ${stored.enabled ? 'ON' : 'OFF'}`;
                    btn.style.background = stored.enabled ? '#28a745' : '#007bff';
                };

                btn.addEventListener('click', () => {
                    const stored = getStoredLocation();
                    stored.enabled = !stored.enabled;
                    setStoredLocation(stored);
                    if (!stored.enabled) {
                        // Reload to restore real geolocation
                        window.location.reload();
                    } else {
                        // Re-apply the override
                        overrideGeolocation({
                            latitude: stored.latitude,
                            longitude: stored.longitude
                        });
                    }
                    updateButtonState();
                });

                travelModeButton.parentNode.appendChild(btn);
                updateButtonState();
            }
        }, 1000);
    }

    function init() {
        applySpoofIfEnabled();
        waitForTravelModeUI();
        watchPinPosition();
        addSpoofToggle();
    }

    window.addEventListener('load', init);
})();