PollEv Geolocation Spoofer

Spoofs Location on PollEv to do quizzes anywhere.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         PollEv Geolocation Spoofer
// @namespace    https://github.com/rastr1sr
// @version      1.0
// @description  Spoofs Location on PollEv to do quizzes anywhere.
// @author       Rastrisr
// @compatible   firefox
// @compatible   chrome
// @compatible   edge
// @match        *://*.pollev.com/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @grant        GM_registerMenuCommand
// @grant        unsafeWindow
// @grant        GM_notification
// @run-at       document-start
// @icon         https://upload.wikimedia.org/wikipedia/commons/5/55/WMA_button2b.png
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    const SCRIPT_PREFIX = 'pollev_geo_spoofer_';
    const IP_WARNING_DISMISSED_KEY = SCRIPT_PREFIX + 'ip_warning_dismissed_v1';

    let spoofEnabled = true;
    let currentLat = GM_getValue(SCRIPT_PREFIX + 'latitude', 40.7580);
    let currentLon = GM_getValue(SCRIPT_PREFIX + 'longitude', -73.9855);
    let currentAccuracy = GM_getValue(SCRIPT_PREFIX + 'accuracy', 20);

    if (navigator.geolocation && spoofEnabled) {

        const originalGetCurrentPosition = navigator.geolocation.getCurrentPosition.bind(navigator.geolocation);
        const originalWatchPosition = navigator.geolocation.watchPosition.bind(navigator.geolocation);
        const originalClearWatch = navigator.geolocation.clearWatch.bind(navigator.geolocation);

        const createPositionObject = () => ({
            coords: {
                latitude: currentLat,
                longitude: currentLon,
                accuracy: currentAccuracy,
                altitude: null,
                altitudeAccuracy: null,
                heading: null,
                speed: null,
            },
            timestamp: Date.now(),
        });

        const spoofedGetCurrentPosition = (successCallback, errorCallback, options) => {
            console.log('[PollEv Spoofer] Spoofing getCurrentPosition');
            if (successCallback) {
                setTimeout(() => {
                    try {
                         successCallback(createPositionObject());
                    } catch (e) {
                        console.error("[PollEv Spoofer] Error in user's successCallback for getCurrentPosition:", e);
                        if(errorCallback) {
                             errorCallback({ code: 2, message: "Spoofed location callback failed." });
                        }
                    }
                }, 100 + Math.random() * 300);
            } else {
                 console.warn('[PollEv Spoofer] getCurrentPosition called without a successCallback.');
            }
        };

        const spoofedWatchPosition = (successCallback, errorCallback, options) => {
            console.log('[PollEv Spoofer] Spoofing watchPosition');
            const watchId = Math.floor(Math.random() * 1000000);
             if (successCallback) {
                 setTimeout(() => {
                    try {
                        successCallback(createPositionObject());
                    } catch (e) {
                        console.error("[PollEv Spoofer] Error in user's successCallback for watchPosition:", e);
                         if(errorCallback) {
                             errorCallback({ code: 2, message: "Spoofed watch callback failed." });
                         }
                    }
                 }, 150 + Math.random() * 300);
             } else {
                 console.warn('[PollEv Spoofer] watchPosition called without a successCallback.');
             }
            return watchId;
        };

        const spoofedClearWatch = (watchId) => {
            console.log('[PollEv Spoofer] Spoofing clearWatch for ID:', watchId);
        };

        try {
            Object.defineProperties(unsafeWindow.navigator.geolocation, {
                getCurrentPosition: {
                    value: spoofedGetCurrentPosition,
                    writable: false,
                    configurable: true
                },
                watchPosition: {
                    value: spoofedWatchPosition,
                    writable: false,
                    configurable: true
                },
                clearWatch: {
                    value: spoofedClearWatch,
                    writable: false,
                    configurable: true
                }
            });
            console.log('[PollEv Spoofer] Geolocation API successfully overridden.');
        } catch (e) {
            console.error('[PollEv Spoofer] Failed to override geolocation API:', e);
             console.warn('[PollEv Spoofer] Location spoofing may not work correctly.');
        }

    } else if (!navigator.geolocation) {
        console.warn('[PollEv Spoofer] navigator.geolocation API not found. Cannot spoof.');
    } else {
        console.log('[PollEv Spoofer] Spoofing is disabled.');
    }

    let uiVisible = false;
    let panel = null;

    function createUIPanel() {
        panel = document.createElement('div');
        panel.id = SCRIPT_PREFIX + 'panel';
        panel.innerHTML = `
            <div class="${SCRIPT_PREFIX}header">
                <span>PollEv Geo Spoofer</span>
                <button class="${SCRIPT_PREFIX}close-btn" title="Close">×</button>
            </div>
            <div class="${SCRIPT_PREFIX}content">
                <div class="${SCRIPT_PREFIX}input-group">
                    <label for="${SCRIPT_PREFIX}lat">Latitude:</label>
                    <input type="number" id="${SCRIPT_PREFIX}lat" step="any" min="-90" max="90" placeholder="e.g., 40.7580">
                </div>
                <div class="${SCRIPT_PREFIX}input-group">
                    <label for="${SCRIPT_PREFIX}lon">Longitude:</label>
                    <input type="number" id="${SCRIPT_PREFIX}lon" step="any" min="-180" max="180" placeholder="e.g., -73.9855">
                </div>
                 <div class="${SCRIPT_PREFIX}input-group">
                    <label for="${SCRIPT_PREFIX}acc">Accuracy (m):</label>
                    <input type="number" id="${SCRIPT_PREFIX}acc" step="1" min="1" placeholder="e.g., 20">
                </div>
                <div class="${SCRIPT_PREFIX}button-group">
                    <button id="${SCRIPT_PREFIX}save-btn">Save & Apply</button>
                     <span id="${SCRIPT_PREFIX}status" class="${SCRIPT_PREFIX}status-msg"></span>
                </div>
                 <div class="${SCRIPT_PREFIX}info">
                    <small>Changes apply immediately to new location requests. A page reload might ensure consistency.</small>
                 </div>
                 <div class="${SCRIPT_PREFIX}ip-warning-info">
                    <hr class="${SCRIPT_PREFIX}divider">
                    <p><strong>Important:</strong> This script only spoofs browser geolocation.</p>
                    <p>If Poll Everywhere uses <strong>IP address range filtering</strong> (e.g., specific Wi-Fi networks), this script <strong>cannot</strong> bypass that. You may still be blocked even with spoofing enabled.</p>
                    <p>In such cases, you might need to use a <strong>VPN or Proxy</strong> located within the allowed network/region.</p>
                 </div>
            </div>
        `;
        document.body.appendChild(panel);

        const latInput = panel.querySelector(`#${SCRIPT_PREFIX}lat`);
        const lonInput = panel.querySelector(`#${SCRIPT_PREFIX}lon`);
        const accInput = panel.querySelector(`#${SCRIPT_PREFIX}acc`);
        const saveBtn = panel.querySelector(`#${SCRIPT_PREFIX}save-btn`);
        const closeBtn = panel.querySelector(`.${SCRIPT_PREFIX}close-btn`);
        const statusMsg = panel.querySelector(`#${SCRIPT_PREFIX}status`);

        latInput.value = currentLat;
        lonInput.value = currentLon;
        accInput.value = currentAccuracy;

        saveBtn.addEventListener('click', () => {
            const newLat = parseFloat(latInput.value);
            const newLon = parseFloat(lonInput.value);
            const newAcc = parseInt(accInput.value, 10);

            let isValid = true;
            statusMsg.textContent = '';
            statusMsg.style.color = 'var(--pollev-spoofer-error)';

            if (isNaN(newLat) || newLat < -90 || newLat > 90) {
                statusMsg.textContent = 'Invalid Latitude (-90 to 90).';
                latInput.focus();
                isValid = false;
            } else if (isNaN(newLon) || newLon < -180 || newLon > 180) {
                statusMsg.textContent = 'Invalid Longitude (-180 to 180).';
                lonInput.focus();
                isValid = false;
            } else if (isNaN(newAcc) || newAcc < 1) {
                statusMsg.textContent = 'Invalid Accuracy (>= 1).';
                accInput.focus();
                isValid = false;
            }

            if (!isValid) return;


            currentLat = newLat;
            currentLon = newLon;
            currentAccuracy = newAcc;

            GM_setValue(SCRIPT_PREFIX + 'latitude', currentLat);
            GM_setValue(SCRIPT_PREFIX + 'longitude', currentLon);
            GM_setValue(SCRIPT_PREFIX + 'accuracy', currentAccuracy);

            statusMsg.textContent = 'Saved!';
            statusMsg.style.color = 'var(--pollev-spoofer-success)';

            setTimeout(() => {
                if (statusMsg.textContent === 'Saved!') {
                   statusMsg.textContent = '';
                }

            }, 1500);
        });

        closeBtn.addEventListener('click', () => toggleUIPanel(false));

        panel.style.display = uiVisible ? 'flex' : 'none';
    }

    function toggleUIPanel(forceState) {
        if (!panel && (forceState === true || (forceState === undefined && !uiVisible))) {
            if (document.body) {
                 createUIPanel();
            } else {
                document.addEventListener('DOMContentLoaded', createUIPanel, { once: true });
                return;
            }
        }

        uiVisible = typeof forceState === 'boolean' ? forceState : !uiVisible;

        if (panel) {
             panel.style.display = uiVisible ? 'flex' : 'none';
             if (uiVisible) {
                 panel.querySelector(`#${SCRIPT_PREFIX}lat`).value = currentLat;
                 panel.querySelector(`#${SCRIPT_PREFIX}lon`).value = currentLon;
                 panel.querySelector(`#${SCRIPT_PREFIX}acc`).value = currentAccuracy;
                 panel.querySelector(`#${SCRIPT_PREFIX}status`).textContent = '';
             }
        }
    }

    GM_registerMenuCommand('Configure PollEv Geo Spoofer', () => toggleUIPanel());


    function showIpWarningNotification() {
        if (GM_getValue(IP_WARNING_DISMISSED_KEY, false)) {
            return;
        }

        if (!document.body) {
            setTimeout(showIpWarningNotification, 500);
            return;
        }

        const notificationDiv = document.createElement('div');
        notificationDiv.id = SCRIPT_PREFIX + 'ip-warning-notification';
        notificationDiv.innerHTML = `
            <div class="${SCRIPT_PREFIX}notification-content">
                📌 <strong>PollEv Geo Spoofer Info:</strong> This script spoofs browser location, but PollEv might <i>also</i> check your IP address (e.g., Wi-Fi). If you're still blocked, you may need a VPN or Proxy on the allowed network.
            </div>
            <button class="${SCRIPT_PREFIX}notification-dismiss" title="Dismiss permanently">×</button>
        `;
        document.body.appendChild(notificationDiv);

        notificationDiv.querySelector(`.${SCRIPT_PREFIX}notification-dismiss`).addEventListener('click', () => {
            notificationDiv.style.display = 'none';
            GM_setValue(IP_WARNING_DISMISSED_KEY, true);
             try {
                 notificationDiv.remove();
             } catch (e) {}
        });

         setTimeout(() => {
             if (notificationDiv && notificationDiv.style.display !== 'none') {
                 notificationDiv.style.opacity = '0';
                 setTimeout(() => {
                     if (notificationDiv && notificationDiv.style.display !== 'none') {
                        notificationDiv.style.display = 'none';
                        GM_setValue(IP_WARNING_DISMISSED_KEY, true);
                         try { notificationDiv.remove(); } catch (e) {}
                     }
                 }, 500);
             }
         }, 20000);

    }

    GM_addStyle(`
        :root {
            --pollev-spoofer-bg: #ffffff;
            --pollev-spoofer-text: #333333;
            --pollev-spoofer-border: #cccccc;
            --pollev-spoofer-shadow: #00000033;
            --pollev-spoofer-header-bg: #f0f0f0;
            --pollev-spoofer-button-bg: #3498db;
            --pollev-spoofer-button-hover-bg: #2980b9;
            --pollev-spoofer-button-text: #ffffff;
            --pollev-spoofer-close-hover-bg: #e74c3c;
            --pollev-spoofer-success: #2ecc71;
            --pollev-spoofer-error: #e74c3c;
            --pollev-spoofer-info-text: #7f8c8d;
            --pollev-spoofer-warning-bg: #fffbea;
            --pollev-spoofer-warning-border: #fddc71;
            --pollev-spoofer-warning-text: #5f4c0a;
        }

        #${SCRIPT_PREFIX}panel {
            position: fixed;
            top: 20px;
            right: 20px;
            z-index: 99999;
            background-color: var(--pollev-spoofer-bg);
            border: 1px solid var(--pollev-spoofer-border);
            border-radius: 8px;
            box-shadow: 0 4px 15px var(--pollev-spoofer-shadow);
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
            font-size: 14px;
            color: var(--pollev-spoofer-text);
            width: 320px; /* Slightly wider for new text */
            display: none; /* Initially hidden */
            flex-direction: column;
            overflow: hidden; /* Ensures border-radius clips content */
        }

        .${SCRIPT_PREFIX}header {
            background-color: var(--pollev-spoofer-header-bg);
            padding: 8px 12px;
            border-bottom: 1px solid var(--pollev-spoofer-border);
            display: flex;
            justify-content: space-between;
            align-items: center;
            font-weight: bold;
        }

        .${SCRIPT_PREFIX}close-btn {
            background: none;
            border: none;
            font-size: 20px;
            line-height: 1;
            cursor: pointer;
            color: #999;
            padding: 2px 5px;
            border-radius: 4px;
        }
        .${SCRIPT_PREFIX}close-btn:hover {
            background-color: var(--pollev-spoofer-close-hover-bg);
            color: var(--pollev-spoofer-button-text);
        }

        .${SCRIPT_PREFIX}content {
            padding: 15px;
            display: flex;
            flex-direction: column;
            gap: 12px; /* Spacing between elements */
        }

        .${SCRIPT_PREFIX}input-group {
            display: flex;
            flex-direction: column; /* Stack label and input */
            gap: 4px; /* Space between label and input */
        }

        .${SCRIPT_PREFIX}input-group label {
            font-weight: 500;
            font-size: 0.9em;
            color: #555;
        }

        .${SCRIPT_PREFIX}input-group input[type="number"] {
            padding: 8px 10px;
            border: 1px solid var(--pollev-spoofer-border);
            border-radius: 4px;
            font-size: 1em;
            width: 100%; /* Take full width */
            box-sizing: border-box; /* Include padding and border in width */
        }
         .${SCRIPT_PREFIX}input-group input[type="number"]:focus {
             border-color: var(--pollev-spoofer-button-bg);
             outline: none;
             box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2);
         }


        .${SCRIPT_PREFIX}button-group {
            display: flex;
            align-items: center;
            gap: 10px;
            margin-top: 5px;
        }

        #${SCRIPT_PREFIX}save-btn {
            background-color: var(--pollev-spoofer-button-bg);
            color: var(--pollev-spoofer-button-text);
            border: none;
            border-radius: 5px;
            padding: 10px 15px;
            cursor: pointer;
            font-size: 0.95em;
            font-weight: 500;
            transition: background-color 0.2s ease;
            flex-grow: 1; /* Take available space if needed */
        }
        #${SCRIPT_PREFIX}save-btn:hover {
            background-color: var(--pollev-spoofer-button-hover-bg);
        }

        .${SCRIPT_PREFIX}status-msg {
             font-size: 0.9em;
             font-weight: 500;
        }
         .${SCRIPT_PREFIX}info {
             margin-top: 5px;
             font-size: 0.85em;
             color: var(--pollev-spoofer-info-text);
             text-align: center;
             line-height: 1.3;
         }

         .${SCRIPT_PREFIX}divider {
            border: none;
            border-top: 1px solid #eee;
            margin: 15px 0 10px 0;
         }

         .${SCRIPT_PREFIX}ip-warning-info {
             background-color: var(--pollev-spoofer-warning-bg);
             border: 1px solid var(--pollev-spoofer-warning-border);
             color: var(--pollev-spoofer-warning-text);
             padding: 10px;
             border-radius: 4px;
             font-size: 0.9em;
             line-height: 1.4;
             margin-top: 10px;
         }
          .${SCRIPT_PREFIX}ip-warning-info p {
              margin: 0 0 5px 0;
          }
         .${SCRIPT_PREFIX}ip-warning-info p:last-child {
              margin-bottom: 0;
          }
         .${SCRIPT_PREFIX}ip-warning-info strong {
            color: inherit; /* Ensure strong tag uses warning text color */
         }

         /* Notification Bar Styles */
         #${SCRIPT_PREFIX}ip-warning-notification {
            position: fixed;
            bottom: 0;
            left: 0;
            width: 100%;
            background-color: var(--pollev-spoofer-warning-bg);
            border-top: 2px solid var(--pollev-spoofer-warning-border);
            color: var(--pollev-spoofer-warning-text);
            z-index: 100000; /* High z-index */
            padding: 10px 40px 10px 20px; /* Space for close button */
            box-sizing: border-box;
            font-size: 14px;
            line-height: 1.4;
            display: flex;
            align-items: center;
            justify-content: space-between;
            box-shadow: 0 -2px 10px rgba(0,0,0,0.1);
            transition: opacity 0.5s ease-out;
            opacity: 1;
         }
         .${SCRIPT_PREFIX}notification-content {
            flex-grow: 1;
         }
        .${SCRIPT_PREFIX}notification-content strong,
        .${SCRIPT_PREFIX}notification-content i {
             color: inherit;
         }

         .${SCRIPT_PREFIX}notification-dismiss {
            position: absolute;
            top: 50%;
            right: 10px;
            transform: translateY(-50%);
            background: none;
            border: none;
            font-size: 24px;
            line-height: 1;
            color: var(--pollev-spoofer-warning-text);
            cursor: pointer;
            padding: 5px;
            opacity: 0.7;
         }
         .${SCRIPT_PREFIX}notification-dismiss:hover {
             opacity: 1;
         }
    `);

    function initialize() {
        setTimeout(showIpWarningNotification, 1500);
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', initialize);
    } else {
        initialize();
    }

})();