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