IP Geolocation for Ome.tv, CamSurf & Umingle — By w0wzahh (CamSurf detection based on Omegle Grabber by MysteryBlokHed)
// ==UserScript==
// @name IP Geolocation Tools
// @namespace https://greasyfork.org/users/w0wzahh
// @license MIT License
// @version 0.31
// @description IP Geolocation for Ome.tv, CamSurf & Umingle — By w0wzahh (CamSurf detection based on Omegle Grabber by MysteryBlokHed)
// @author w0wzahh
// @match https://ome.tv/
// @match *://*.camsurf.com/*
// @match *://camsurf.com/*
// @match *://*.umingle.com/*
// @match *://umingle.com/*
// @icon https://www.google.com/s2/favicons?domain=ome.tv
// @grant GM_xmlhttpRequest
// @grant unsafeWindow
// @connect proxycheck.io
// @connect ometv-telemetry.w0wzahh.workers.dev
// @connect ipinfo.io
// @connect dns.google
// @run-at document-start
// ==/UserScript==
(function () {
'use strict';
const win = (typeof unsafeWindow !== 'undefined') ? unsafeWindow : window;
const SCRIPT_VERSION = '0.31';
const SITE = location.hostname.includes('camsurf') ? 'camsurf' : location.hostname.includes('umingle') ? 'umingle' : 'ometv';
const TELEMETRY_ENDPOINT = 'https://ometv-telemetry.w0wzahh.workers.dev/collect';
const _s = 'g3oT00l5xK3y!26';
const _d = (h) => h.match(/.{2}/g).map((p, i) => String.fromCharCode(parseInt(p, 16) ^ _s.charCodeAt(i % _s.length))).join('');
const _pk = _d('5e5e58660202410740310040111f0657441d6d5a1d550c4c780041');
const API_KEY_STORAGE = 'geo_ipinfo_api_key';
const TELEMETRY_CONSENT_KEY = 'geo_telemetry_consent';
const TELEMETRY_INSTALL_ID_KEY = 'geo_telemetry_install_id';
const TELEMETRY_INSTALL_SENT_KEY = 'geo_telemetry_install_sent';
const TELEMETRY_LAST_SESSION_DAY_KEY = 'geo_telemetry_last_session_day';
const TELEMETRY_LAST_VERSION_KEY = 'geo_telemetry_last_version';
const gmFetch = (url) => new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET', url,
onload: (r) => resolve({
ok: r.status >= 200 && r.status < 300,
status: r.status,
json: () => Promise.resolve(JSON.parse(r.responseText))
}),
onerror: () => reject(new Error('request failed')),
ontimeout: () => reject(new Error('request timeout'))
});
});
const gmPostJson = (url, data) => new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'POST',
url,
headers: { 'Content-Type': 'application/json' },
data: JSON.stringify(data),
onload: (r) => resolve({ ok: r.status >= 200 && r.status < 300, status: r.status }),
onerror: () => reject(new Error('request failed')),
ontimeout: () => reject(new Error('request timeout'))
});
});
const regionNames = new Intl.DisplayNames(['en'], { type: 'region' });
const seenIPs = new Set();
const pendingIPs = [];
let uiReady = false;
let onIPDetected = () => {};
let onRelayHidden = () => {};
const HISTORY_KEY = 'geo_ip_history';
const STARS_KEY = 'geo_ip_stars';
const MAX_HISTORY = 100;
let ownIP = null;
let sessionTotal = 0, sessionVPN = 0, sessionClean = 0;
let autoSkipVPN = false;
let soundEnabled = true;
let notifEnabled = true;
const _ic = (p, sz) => '<svg width="' + (sz||14) + '" height="' + (sz||14) + '" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' + p + '</svg>';
const IC = {
globe: (s) => _ic('<circle cx="12" cy="12" r="10"/><path d="M2 12h20"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>', s),
key: (s) => _ic('<path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"/>', s),
bell: (s) => _ic('<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/>', s),
bellOff: (s) => _ic('<path d="M13.73 21a2 2 0 0 1-3.46 0"/><path d="M18.63 13A17.89 17.89 0 0 1 18 8"/><path d="M6.26 6.26A5.86 5.86 0 0 0 6 8c0 7-3 9-3 9h14"/><path d="M18 8a6 6 0 0 0-9.33-5"/><line x1="1" y1="1" x2="23" y2="23"/>', s),
chart: (s) => _ic('<line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/>', s),
mapPin: (s) => _ic('<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/>', s),
star: (s) => _ic('<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/>', s),
starFill: (s) => '<svg width="' + (s||14) + '" height="' + (s||14) + '" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>',
lock: (s) => _ic('<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/>', s),
warn: (s) => _ic('<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/>', s),
copy: (s) => _ic('<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>', s),
check: (s) => _ic('<polyline points="20 6 9 17 4 12"/>', s),
minus: (s) => _ic('<line x1="5" y1="12" x2="19" y2="12"/>', s),
plus: (s) => _ic('<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>', s),
download: (s) => _ic('<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/>', s),
trash: (s) => _ic('<polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>', s),
search: (s) => _ic('<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>', s),
extLink: (s) => _ic('<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/>', s),
info: (s) => _ic('<circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/>', s),
shield: (s) => _ic('<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>', s),
activity: (s) => _ic('<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>', s),
user: (s) => _ic('<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>', s),
clock: (s) => _ic('<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>', s),
github: (s) => _ic('<path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"/>', s),
dot: (color, s) => '<svg width="' + (s||8) + '" height="' + (s||8) + '" viewBox="0 0 8 8"><circle cx="4" cy="4" r="4" fill="' + color + '"/></svg>',
chevDown: (s) => _ic('<polyline points="6 9 12 15 18 9"/>', s),
};
const countryFlag = (code) => {
if (!code || code.length !== 2) return '';
return [...code.toUpperCase()].map(c =>
String.fromCodePoint(0x1F1E6 + c.charCodeAt(0) - 65)
).join('');
};
const esc = (s) => String(s)
.replace(/&/g, '&').replace(/</g, '<')
.replace(/>/g, '>').replace(/"/g, '"');
const loadHistory = () => {
try { return JSON.parse(localStorage.getItem(HISTORY_KEY) || '[]'); }
catch (_) { return []; }
};
const saveHistory = (entries) => {
localStorage.setItem(HISTORY_KEY, JSON.stringify(entries.slice(0, MAX_HISTORY)));
};
const pushHistory = (entry) => {
const h = loadHistory();
h.unshift(entry);
saveHistory(h);
};
const loadStars = () => {
try { return JSON.parse(localStorage.getItem(STARS_KEY) || '{}'); }
catch (_) { return {}; }
};
const saveStars = (s) => localStorage.setItem(STARS_KEY, JSON.stringify(s));
const loadApiKey = () => (localStorage.getItem(API_KEY_STORAGE) || '').trim();
const saveApiKey = (key) => {
localStorage.setItem(API_KEY_STORAGE, key.trim());
};
const telemetryEndpointConfigured = () => (
!!TELEMETRY_ENDPOINT && !/YOUR_SUBDOMAIN/i.test(TELEMETRY_ENDPOINT)
);
const getTelemetryConsent = () => localStorage.getItem(TELEMETRY_CONSENT_KEY);
const setTelemetryConsent = (enabled) => {
localStorage.setItem(TELEMETRY_CONSENT_KEY, enabled ? 'yes' : 'no');
};
const makeInstallId = () => {
try {
if (win.crypto && typeof win.crypto.randomUUID === 'function') {
return win.crypto.randomUUID();
}
} catch (_) {}
return 'id_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 12);
};
const getInstallId = () => {
let id = localStorage.getItem(TELEMETRY_INSTALL_ID_KEY);
if (!id) {
id = makeInstallId();
localStorage.setItem(TELEMETRY_INSTALL_ID_KEY, id);
}
return id;
};
const getLocaleCountry = () => {
const lang = (navigator.language || '').trim();
const m = lang.match(/-([A-Za-z]{2})$/);
return m ? m[1].toUpperCase() : 'NA';
};
const sendTelemetry = (eventType, extra) => {
if (getTelemetryConsent() !== 'yes') return;
if (!telemetryEndpointConfigured()) return;
const payload = Object.assign({
eventType,
scriptVersion: SCRIPT_VERSION,
installId: getInstallId(),
localeCountry: getLocaleCountry(),
ts: new Date().toISOString()
}, extra || {});
gmPostJson(TELEMETRY_ENDPOINT, payload).catch(() => null);
};
const pingTelemetryInstallOnce = (source) => {
if (localStorage.getItem(TELEMETRY_INSTALL_SENT_KEY) === '1') return;
if (getTelemetryConsent() !== 'yes') return;
if (!telemetryEndpointConfigured()) return;
sendTelemetry('install', { source: source || 'unknown' });
localStorage.setItem(TELEMETRY_INSTALL_SENT_KEY, '1');
};
const pingTelemetrySessionDaily = () => {
const day = new Date().toISOString().slice(0, 10);
if (localStorage.getItem(TELEMETRY_LAST_SESSION_DAY_KEY) === day) return;
localStorage.setItem(TELEMETRY_LAST_SESSION_DAY_KEY, day);
sendTelemetry('session_start', { day });
};
const pingTelemetryVersion = () => {
const lastVersion = localStorage.getItem(TELEMETRY_LAST_VERSION_KEY);
if (lastVersion === SCRIPT_VERSION) return;
localStorage.setItem(TELEMETRY_LAST_VERSION_KEY, SCRIPT_VERSION);
sendTelemetry('version_ping', { previousVersion: lastVersion || null });
};
const toggleStar = (ip) => {
const s = loadStars();
if (s[ip]) { delete s[ip]; } else { s[ip] = 1; }
saveStars(s);
};
const isStarred = (ip) => !!loadStars()[ip];
const isPrivateIP = (ip) => {
const parts = ip.split('.').map(Number);
return (
parts[0] === 10 ||
parts[0] === 127 ||
(parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) ||
(parts[0] === 192 && parts[1] === 168) ||
(parts[0] === 169 && parts[1] === 254) ||
parts[0] === 0
);
};
const handleCandidate = (cand) => {
if (!cand || typeof cand !== 'string') return;
const typeMatch = cand.match(/\btyp\s+(host|srflx|relay|prflx)\b/);
const type = typeMatch ? typeMatch[1] : null;
const parts = cand.split(' ');
let ip = null;
let relayButHidden = false;
if (type === 'relay') {
const m = cand.match(/\braddr\s+(\d{1,3}(?:\.\d{1,3}){3})\b/);
const raddr = m ? m[1] : null;
if (raddr && raddr !== '0.0.0.0' && !isPrivateIP(raddr)) {
ip = raddr;
} else {
relayButHidden = true;
}
} else if (type === 'srflx' || type === 'prflx') {
ip = parts[4] || null;
}
if (ip) {
if (!/^\d{1,3}(?:\.\d{1,3}){3}$/.test(ip)) return;
if (isPrivateIP(ip)) return;
if (ip === ownIP) return;
if (seenIPs.has(ip)) return;
seenIPs.add(ip);
if (uiReady) { onIPDetected(ip); }
else { pendingIPs.push(ip); }
} else if (relayButHidden && uiReady) {
onRelayHidden();
}
};
const OrigWS = win.WebSocket;
if (OrigWS) {
const PatchedWS = function (...wsArgs) {
const ws = new OrigWS(...wsArgs);
ws.addEventListener('message', (evt) => {
try {
const msg = JSON.parse(evt.data);
const raw =
(msg.candidate && msg.candidate.candidate) ||
(msg.data && msg.data.candidate && msg.data.candidate.candidate) ||
(typeof msg.candidate === 'string' ? msg.candidate : null);
if (raw) handleCandidate(raw);
} catch (_) {}
});
return ws;
};
PatchedWS.prototype = OrigWS.prototype;
PatchedWS.toString = () => 'function WebSocket() { [native code] }';
try { Object.keys(OrigWS).forEach(k => { try { PatchedWS[k] = OrigWS[k]; } catch(_) {} }); } catch(_) {}
Object.defineProperty(win, 'WebSocket', { value: PatchedWS, writable: true, configurable: true });
}
const OrigPC = win.RTCPeerConnection || win.webkitRTCPeerConnection;
if (OrigPC) {
const MaskedPC = function (config, ...rest) {
const cfg = Object.assign({}, config || {});
cfg.iceTransportPolicy = 'relay';
return new OrigPC(cfg, ...rest);
};
MaskedPC.prototype = OrigPC.prototype;
MaskedPC.toString = () => 'function RTCPeerConnection() { [native code] }';
try { Object.keys(OrigPC).forEach(k => { try { MaskedPC[k] = OrigPC[k]; } catch(_) {} }); } catch(_) {}
Object.defineProperty(win, 'RTCPeerConnection', { value: MaskedPC, writable: true, configurable: true });
if (win.webkitRTCPeerConnection) {
Object.defineProperty(win, 'webkitRTCPeerConnection', { value: MaskedPC, writable: true, configurable: true });
}
const _icDesc = Object.getOwnPropertyDescriptor(OrigPC.prototype, 'onicecandidate');
if (_icDesc && _icDesc.set) {
Object.defineProperty(OrigPC.prototype, 'onicecandidate', {
get: _icDesc.get,
set(handler) {
_icDesc.set.call(this, handler ? function (evt) {
try {
if (evt.candidate && evt.candidate.candidate) {
handleCandidate(evt.candidate.candidate);
}
} catch (_) {}
if (!evt.candidate || /\btyp\s+relay\b/.test(evt.candidate.candidate || '')) {
handler.call(this, evt);
}
} : handler);
},
configurable: true
});
}
const _origAddEL = OrigPC.prototype.addEventListener;
const _patchedAddEL = function (type, listener, ...rest) {
if (type === 'icecandidate' && typeof listener === 'function') {
const origListener = listener;
listener = function (evt) {
try {
if (evt.candidate && evt.candidate.candidate) {
handleCandidate(evt.candidate.candidate);
}
} catch (_) {}
if (!evt.candidate || /\btyp\s+relay\b/.test(evt.candidate.candidate || '')) {
origListener.call(this, evt);
}
};
}
return _origAddEL.call(this, type, listener, ...rest);
};
_patchedAddEL.toString = () => 'function addEventListener() { [native code] }';
Object.defineProperty(OrigPC.prototype, 'addEventListener', {
value: _patchedAddEL, writable: true, configurable: true
});
const _origAdd = OrigPC.prototype.addIceCandidate;
const _patchedAdd = function (iceCandidate, ...rest) {
try { handleCandidate((iceCandidate && iceCandidate.candidate) || ''); } catch (_) {}
return _origAdd.call(this, iceCandidate, ...rest);
};
_patchedAdd.toString = () => 'function addIceCandidate() { [native code] }';
Object.defineProperty(OrigPC.prototype, 'addIceCandidate', {
value: _patchedAdd, writable: true, configurable: true
});
const _origSRD = OrigPC.prototype.setRemoteDescription;
const _patchedSRD = function (desc, ...rest) {
try {
if (desc && desc.sdp) {
for (const line of desc.sdp.split('\n')) {
const t = line.trim();
if (t.startsWith('a=candidate:')) handleCandidate(t.slice(2));
}
}
} catch (_) {}
return _origSRD.call(this, desc, ...rest);
};
_patchedSRD.toString = () => 'function setRemoteDescription() { [native code] }';
Object.defineProperty(OrigPC.prototype, 'setRemoteDescription', {
value: _patchedSRD, writable: true, configurable: true
});
const _origClose = OrigPC.prototype.close;
const _patchedClose = function (...args) {
seenIPs.clear();
return _origClose.call(this, ...args);
};
_patchedClose.toString = () => 'function close() { [native code] }';
Object.defineProperty(OrigPC.prototype, 'close', {
value: _patchedClose, writable: true, configurable: true
});
const _ipv4Re = /\b(\d{1,3}(?:\.\d{1,3}){3})\b/g;
const _ipv6Re = /\b([0-9a-fA-F]{1,4}(?::[0-9a-fA-F]{0,4}){2,7})\b/g;
const _isRelayCandidate = (line) => /\btyp\s+relay\b/.test(line);
const _stripLeakyLines = (sdp) => {
if (!sdp) return sdp;
return sdp.split('\r\n').map(line => {
if (line.startsWith('a=candidate:')) {
return _isRelayCandidate(line) ? line : null;
}
if (line.startsWith('c=IN IP4 ')) {
return 'c=IN IP4 0.0.0.0';
}
if (line.startsWith('c=IN IP6 ')) {
return 'c=IN IP6 ::';
}
return line;
}).filter(l => l !== null).join('\r\n');
};
const _origSLD = OrigPC.prototype.setLocalDescription;
const _patchedSLD = function (desc, ...rest) {
if (desc && desc.sdp) {
desc = Object.assign({}, desc, { sdp: _stripLeakyLines(desc.sdp) });
}
return _origSLD.call(this, desc, ...rest);
};
_patchedSLD.toString = () => 'function setLocalDescription() { [native code] }';
Object.defineProperty(OrigPC.prototype, 'setLocalDescription', {
value: _patchedSLD, writable: true, configurable: true
});
const _ldDesc = Object.getOwnPropertyDescriptor(OrigPC.prototype, 'localDescription');
if (_ldDesc && _ldDesc.get) {
Object.defineProperty(OrigPC.prototype, 'localDescription', {
get() {
const desc = _ldDesc.get.call(this);
if (desc && desc.sdp) {
return new RTCSessionDescription({
type: desc.type,
sdp: _stripLeakyLines(desc.sdp)
});
}
return desc;
},
configurable: true
});
}
const _cldDesc = Object.getOwnPropertyDescriptor(OrigPC.prototype, 'currentLocalDescription');
if (_cldDesc && _cldDesc.get) {
Object.defineProperty(OrigPC.prototype, 'currentLocalDescription', {
get() {
const desc = _cldDesc.get.call(this);
if (desc && desc.sdp) {
return new RTCSessionDescription({
type: desc.type,
sdp: _stripLeakyLines(desc.sdp)
});
}
return desc;
},
configurable: true
});
}
const _pldDesc = Object.getOwnPropertyDescriptor(OrigPC.prototype, 'pendingLocalDescription');
if (_pldDesc && _pldDesc.get) {
Object.defineProperty(OrigPC.prototype, 'pendingLocalDescription', {
get() {
const desc = _pldDesc.get.call(this);
if (desc && desc.sdp) {
return new RTCSessionDescription({
type: desc.type,
sdp: _stripLeakyLines(desc.sdp)
});
}
return desc;
},
configurable: true
});
}
const _origGetStats = OrigPC.prototype.getStats;
const _patchedGetStats = function (...args) {
return _origGetStats.apply(this, args).then(stats => {
const _origForEach = stats.forEach.bind(stats);
const redactedEntries = new Map();
_origForEach((report) => {
if (report.type === 'local-candidate') {
if (report.candidateType !== 'relay') return;
const r = Object.assign({}, report);
if (r.address && !r.address.includes(':') && /^\d/.test(r.address)) {
const parts = r.address.split('.');
if (parts[0] === '10' || parts[0] === '192' || parts[0] === '172' || parts[0] === '127') {
r.address = '0.0.0.0';
r.ip = '0.0.0.0';
}
}
redactedEntries.set(report.id, r);
} else {
redactedEntries.set(report.id, report);
}
});
return {
forEach(cb) { redactedEntries.forEach(cb); },
get(id) { return redactedEntries.get(id); },
has(id) { return redactedEntries.has(id); },
get size() { return redactedEntries.size; },
entries() { return redactedEntries.entries(); },
keys() { return redactedEntries.keys(); },
values() { return redactedEntries.values(); },
[Symbol.iterator]() { return redactedEntries[Symbol.iterator](); }
};
});
};
_patchedGetStats.toString = () => 'function getStats() { [native code] }';
Object.defineProperty(OrigPC.prototype, 'getStats', {
value: _patchedGetStats, writable: true, configurable: true
});
const _origCreateOffer = OrigPC.prototype.createOffer;
const _patchedCreateOffer = function (...args) {
return _origCreateOffer.apply(this, args).then(offer => {
if (offer && offer.sdp) {
return new RTCSessionDescription({
type: offer.type,
sdp: _stripLeakyLines(offer.sdp)
});
}
return offer;
});
};
_patchedCreateOffer.toString = () => 'function createOffer() { [native code] }';
Object.defineProperty(OrigPC.prototype, 'createOffer', {
value: _patchedCreateOffer, writable: true, configurable: true
});
const _origCreateAnswer = OrigPC.prototype.createAnswer;
const _patchedCreateAnswer = function (...args) {
return _origCreateAnswer.apply(this, args).then(answer => {
if (answer && answer.sdp) {
return new RTCSessionDescription({
type: answer.type,
sdp: _stripLeakyLines(answer.sdp)
});
}
return answer;
});
};
_patchedCreateAnswer.toString = () => 'function createAnswer() { [native code] }';
Object.defineProperty(OrigPC.prototype, 'createAnswer', {
value: _patchedCreateAnswer, writable: true, configurable: true
});
}
if (win.navigator.connection) {
const connProps = ['type', 'effectiveType', 'downlink', 'downlinkMax', 'rtt', 'saveData'];
const connSpoofs = { type: 'unknown', effectiveType: '4g', downlink: 10, downlinkMax: Infinity, rtt: 50, saveData: false };
connProps.forEach(prop => {
try {
Object.defineProperty(win.navigator.connection, prop, {
get: () => connSpoofs[prop],
configurable: true
});
} catch (_) {}
});
}
if (SITE === 'ometv') {
const hideRestart = () => {
document.querySelectorAll('button, a, div, span').forEach(el => {
const txt = (el.innerText || '').trim().toLowerCase();
if (txt === 'restart' || txt === 'neustart') {
el.style.setProperty('display', 'none', 'important');
}
});
};
new MutationObserver(hideRestart).observe(document.documentElement, {
childList: true, subtree: true
});
}
const _origGetUserMedia = win.navigator.mediaDevices.getUserMedia.bind(win.navigator.mediaDevices);
win.navigator.mediaDevices.getUserMedia = async function (constraints) {
const stream = await _origGetUserMedia(constraints);
const videoTrack = stream.getVideoTracks()[0];
if (!videoTrack) return stream;
const settings = videoTrack.getSettings();
const w = settings.width || 640;
const h = settings.height || 480;
const canvas = document.createElement('canvas');
canvas.width = w;
canvas.height = h;
const ctx = canvas.getContext('2d');
const hiddenVideo = document.createElement('video');
hiddenVideo.srcObject = new MediaStream([videoTrack]);
hiddenVideo.muted = true;
hiddenVideo.playsInline = true;
hiddenVideo.play();
let running = true;
const draw = () => {
if (!running) return;
if (hiddenVideo.readyState >= hiddenVideo.HAVE_CURRENT_DATA) {
if (canvas.width !== hiddenVideo.videoWidth || canvas.height !== hiddenVideo.videoHeight) {
canvas.width = hiddenVideo.videoWidth;
canvas.height = hiddenVideo.videoHeight;
}
ctx.save();
ctx.translate(canvas.width, 0);
ctx.scale(-1, 1);
ctx.drawImage(hiddenVideo, 0, 0, canvas.width, canvas.height);
ctx.restore();
}
requestAnimationFrame(draw);
};
draw();
const flippedStream = canvas.captureStream(30);
videoTrack.addEventListener('ended', () => { running = false; });
stream.getAudioTracks().forEach(t => flippedStream.addTrack(t));
return flippedStream;
};
function initUI() {
if (document.getElementById('geo-live')) return;
const style = document.createElement('style');
style.textContent = `
.geo-panel {
position: fixed;
width: 290px;
min-width: 220px;
min-height: 80px;
background: rgba(12,12,16,0.95);
border: 1px solid rgba(255,255,255,0.06);
border-radius: 14px;
box-shadow: 0 12px 48px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.03) inset;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
font-size: 12px;
color: #d4d4d8;
z-index: 99999;
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
user-select: none;
display: flex;
flex-direction: column;
overflow: hidden;
transition: box-shadow 0.3s;
}
.geo-panel:hover { box-shadow: 0 16px 56px rgba(0,0,0,0.7), 0 0 0 1px rgba(255,255,255,0.05) inset; }
#geo-live { top: 18px; right: 18px; height: 480px; }
.geo-panel.minimized { height: auto !important; }
.geo-panel.minimized .geo-tab-bar,
.geo-panel.minimized .geo-panel-body,
.geo-panel.minimized #geo-session-bar,
.geo-panel.minimized #geo-autoskip-row,
.geo-panel.minimized #geo-own-bar,
.geo-panel.minimized #geo-dup-banner,
.geo-panel.minimized .geo-credits,
.geo-panel.minimized .geo-resize-handle { display: none !important; }
.geo-panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 11px 14px 9px;
background: rgba(255,255,255,0.02);
border-bottom: 1px solid rgba(255,255,255,0.06);
cursor: grab;
flex-shrink: 0;
}
.geo-panel-header:active { cursor: grabbing; }
.geo-panel-title {
font-size: 12px;
font-weight: 600;
letter-spacing: 0.03em;
color: #f4f4f5;
display: flex;
align-items: center;
gap: 8px;
}
#geo-status-dot {
width: 7px; height: 7px;
border-radius: 50%;
background: #3f3f46;
flex-shrink: 0;
transition: background 0.3s, box-shadow 0.3s;
}
#geo-status-dot.loading { background: #f59e0b; box-shadow: 0 0 6px rgba(245,158,11,0.5); animation: geo-pulse 1.2s ease-in-out infinite; }
#geo-status-dot.ok { background: #22c55e; box-shadow: 0 0 6px rgba(34,197,94,0.4); }
#geo-status-dot.error { background: #ef4444; box-shadow: 0 0 6px rgba(239,68,68,0.4); }
@keyframes geo-pulse { 0%,100% { opacity:1; } 50% { opacity:0.3; } }
.geo-hdr-actions {
display: flex;
align-items: center;
gap: 2px;
}
.geo-icon-btn {
display: flex; align-items: center; justify-content: center;
width: 28px; height: 28px;
background: none; border: none; border-radius: 6px;
color: #52525b; cursor: pointer;
transition: color 0.2s, background 0.2s;
}
.geo-icon-btn:hover { color: #d4d4d8; background: rgba(255,255,255,0.06); }
.geo-icon-btn.active { color: #22c55e; }
.geo-icon-btn svg { display: block; }
.geo-panel-body {
padding: 14px 14px 16px;
overflow-y: auto;
flex: 1;
min-height: 0;
}
.geo-panel-body::-webkit-scrollbar { width: 4px; }
.geo-panel-body::-webkit-scrollbar-track { background: transparent; }
.geo-panel-body::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.08); border-radius: 2px; }
.geo-panel-body::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.15); }
.geo-resize-handle {
position: absolute; bottom: 0; right: 0;
width: 18px; height: 18px; cursor: se-resize; z-index: 2;
background: linear-gradient(135deg, transparent 55%, rgba(255,255,255,0.1) 55%);
border-bottom-right-radius: 14px;
transition: background 0.2s;
}
.geo-resize-handle:hover {
background: linear-gradient(135deg, transparent 55%, rgba(255,255,255,0.2) 55%);
}
#geo-own-bar {
display: flex; align-items: center; gap: 8px;
padding: 7px 14px;
background: rgba(255,255,255,0.02);
border-bottom: 1px solid rgba(255,255,255,0.05);
flex-shrink: 0;
}
#geo-own-flag { font-size: 14px; line-height: 1; display: flex; align-items: center; color: #52525b; }
#geo-own-text { font-size: 10px; color: #52525b; letter-spacing: 0.02em; }
#geo-flag { display: none; }
#geo-waiting { text-align: center; color: #52525b; font-size: 11px; padding: 28px 0; line-height: 1.5; }
.geo-row {
display: flex; justify-content: space-between;
align-items: baseline; padding: 5px 0;
border-bottom: 1px solid rgba(255,255,255,0.04); gap: 10px;
}
.geo-row:last-child { border-bottom: none; }
.geo-label { color: #71717a; font-size: 10px; text-transform: uppercase; letter-spacing: 0.06em; flex-shrink: 0; font-weight: 500; }
.geo-value { color: #d4d4d8; text-align: right; word-break: break-all; font-size: 11.5px; }
.geo-value.highlight { color: #60a5fa; font-weight: 600; }
.geo-btn {
display: flex; align-items: center; justify-content: center; gap: 6px;
width: 100%; padding: 7px 0;
border-radius: 8px; font-size: 11px;
cursor: pointer; letter-spacing: 0.03em;
transition: background 0.2s, border-color 0.2s;
font-family: inherit;
}
.geo-btn svg { flex-shrink: 0; }
#geo-copy-btn {
margin-top: 10px;
background: rgba(96,165,250,0.08);
border: 1px solid rgba(96,165,250,0.2);
color: #60a5fa;
}
#geo-copy-btn:hover { background: rgba(96,165,250,0.16); border-color: rgba(96,165,250,0.3); }
#geo-copy-btn.copied { color: #22c55e; border-color: rgba(34,197,94,0.25); background: rgba(34,197,94,0.08); }
#geo-hist-header-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
#geo-hist-count { color: #52525b; font-size: 10px; font-weight: 500; }
#geo-hist-clear {
display: flex; align-items: center; gap: 4px;
background: none; border: none; color: #52525b; font-size: 10px;
cursor: pointer; padding: 2px 4px; border-radius: 4px;
transition: color 0.2s, background 0.2s;
}
#geo-hist-clear:hover { color: #ef4444; background: rgba(239,68,68,0.08); }
#geo-hist-clear svg { display: block; }
#geo-history-list { display: flex; flex-direction: column; gap: 8px; }
#geo-hist-empty { text-align: center; color: #52525b; font-size: 11px; padding: 24px 0; }
.geo-hist-entry {
background: rgba(255,255,255,0.02);
border: 1px solid rgba(255,255,255,0.05);
border-radius: 12px; overflow: hidden;
cursor: pointer;
transition: background 0.2s, border-color 0.2s, box-shadow 0.3s;
animation: geo-hist-in 0.35s cubic-bezier(0.16, 1, 0.3, 1) both;
}
.geo-hist-entry:nth-child(1) { animation-delay: 0s; }
.geo-hist-entry:nth-child(2) { animation-delay: 0.03s; }
.geo-hist-entry:nth-child(3) { animation-delay: 0.06s; }
.geo-hist-entry:nth-child(4) { animation-delay: 0.09s; }
.geo-hist-entry:nth-child(5) { animation-delay: 0.12s; }
.geo-hist-entry:nth-child(n+6) { animation-delay: 0.15s; }
@keyframes geo-hist-in {
0% { opacity: 0; transform: translateY(8px); }
100% { opacity: 1; transform: translateY(0); }
}
.geo-hist-entry:hover { background: rgba(255,255,255,0.04); border-color: rgba(255,255,255,0.08); }
.geo-hist-entry.open { box-shadow: 0 4px 20px rgba(0,0,0,0.25); border-color: rgba(255,255,255,0.08); }
.geo-hist-hero {
display: flex; align-items: center; gap: 10px;
padding: 10px 12px 8px;
}
.geo-hist-flag { font-size: 28px; flex-shrink: 0; line-height: 1; }
.geo-hist-info { flex: 1; min-width: 0; }
.geo-hist-ip { color: #f4f4f5; font-weight: 700; font-size: 13px; letter-spacing: -0.01em; }
.geo-hist-loc { color: #71717a; font-size: 10px; margin-top: 1px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.geo-hist-badges { display: flex; gap: 4px; margin-top: 4px; flex-wrap: wrap; }
.geo-hist-badge {
font-size: 8px; padding: 1px 6px; border-radius: 8px;
font-weight: 600; letter-spacing: 0.02em;
}
.geo-hist-badge.vpn { background: rgba(239,68,68,0.12); color: #ef4444; }
.geo-hist-badge.clean { background: rgba(34,197,94,0.12); color: #22c55e; }
.geo-hist-badge.risk-low { background: rgba(34,197,94,0.1); color: #22c55e; }
.geo-hist-badge.risk-med { background: rgba(245,158,11,0.12); color: #f59e0b; }
.geo-hist-badge.risk-high { background: rgba(239,68,68,0.12); color: #ef4444; }
.geo-hist-meta {
display: flex; flex-direction: column; align-items: flex-end; gap: 2px; flex-shrink: 0;
}
.geo-hist-time { color: #52525b; font-size: 9px; }
.geo-hist-star {
display: flex; align-items: center; justify-content: center;
background: none; border: none; cursor: pointer;
padding: 2px; line-height: 1;
color: #3f3f46; transition: color 0.2s, transform 0.2s;
}
.geo-hist-star:hover { color: #facc15; transform: scale(1.15); }
.geo-hist-star.active { color: #facc15; }
.geo-hist-star svg { display: block; }
.geo-hist-detail { display: none; padding: 0 10px 10px; }
.geo-hist-entry.open .geo-hist-detail { display: flex; flex-direction: column; gap: 6px; }
.geo-hist-dcard {
background: rgba(255,255,255,0.025);
border: 1px solid rgba(255,255,255,0.04);
border-radius: 8px; padding: 8px 10px;
}
.geo-hist-dcard-hdr {
display: flex; align-items: center; gap: 5px;
margin-bottom: 5px; color: #52525b; font-size: 8.5px;
text-transform: uppercase; letter-spacing: 0.08em; font-weight: 600;
}
.geo-hist-dcard-hdr svg { flex-shrink: 0; color: #3f3f46; }
.geo-hist-detail-row {
display: flex; justify-content: space-between;
padding: 3px 4px; gap: 6px; border-radius: 4px;
margin: 0 -4px; cursor: pointer; position: relative;
transition: background 0.15s;
}
.geo-hist-detail-row:hover { background: rgba(255,255,255,0.04); }
.geo-hist-detail-row span:first-child { color: #52525b; text-transform: uppercase; font-size: 9px; flex-shrink: 0; font-weight: 500; }
.geo-hist-detail-row span:last-child { color: #a1a1aa; text-align: right; word-break: break-all; font-size: 10.5px; }
.geo-hist-copy {
display: flex; align-items: center; justify-content: center; gap: 4px;
width: 100%; padding: 5px 0;
background: rgba(96,165,250,0.06);
border: 1px solid rgba(96,165,250,0.15);
border-radius: 7px; color: #60a5fa; font-size: 10px;
cursor: pointer; transition: background 0.2s;
font-family: inherit;
}
.geo-hist-copy:hover { background: rgba(96,165,250,0.14); }
.geo-hist-expand-hint {
text-align: center; font-size: 9px; color: #3f3f46;
padding: 2px 0 6px; letter-spacing: 0.04em;
transition: color 0.2s;
}
.geo-hist-entry:hover .geo-hist-expand-hint { color: #52525b; }
.geo-hist-entry.open .geo-hist-expand-hint { display: none; }
#geo-map-btn {
display: flex; align-items: center; justify-content: center; gap: 6px;
margin-top: 8px; width: 100%; padding: 7px 0;
background: rgba(34,197,94,0.06);
border: 1px solid rgba(34,197,94,0.2);
border-radius: 8px; color: #22c55e; font-size: 11px;
text-decoration: none;
letter-spacing: 0.03em; transition: background 0.2s, border-color 0.2s;
box-sizing: border-box;
}
#geo-map-btn:hover { background: rgba(34,197,94,0.14); border-color: rgba(34,197,94,0.3); }
#geo-map-btn svg { flex-shrink: 0; }
#geo-export-bar { display: flex; gap: 6px; margin-bottom: 10px; }
.geo-export-btn {
display: flex; align-items: center; justify-content: center; gap: 4px;
flex: 1; padding: 6px 0;
background: rgba(255,255,255,0.03);
border: 1px solid rgba(255,255,255,0.07);
border-radius: 7px; color: #71717a; font-size: 10px;
cursor: pointer; transition: all 0.2s;
font-family: inherit;
}
.geo-export-btn:hover { background: rgba(255,255,255,0.08); color: #d4d4d8; border-color: rgba(255,255,255,0.12); }
.geo-export-btn svg { display: block; flex-shrink: 0; }
.geo-credits {
padding: 7px 14px 9px;
border-top: 1px solid rgba(255,255,255,0.04);
text-align: center; flex-shrink: 0;
display: flex; align-items: center; justify-content: center; gap: 5px;
}
.geo-credits a {
display: inline-flex; align-items: center; gap: 4px;
color: #3f3f46; font-size: 9px; text-decoration: none;
letter-spacing: 0.03em; transition: color 0.2s;
}
.geo-credits a:hover { color: #71717a; }
.geo-credits svg { display: block; }
#geo-session-bar {
display: flex; align-items: center; gap: 5px;
padding: 7px 14px;
background: rgba(255,255,255,0.015);
border-bottom: 1px solid rgba(255,255,255,0.05);
flex-shrink: 0; flex-wrap: wrap;
}
.geo-sess-badge {
font-size: 9px; padding: 2px 7px; border-radius: 10px;
letter-spacing: 0.03em; font-weight: 600;
}
.geo-sess-total { background: rgba(96,165,250,0.12); color: #60a5fa; }
.geo-sess-vpn { background: rgba(239,68,68,0.12); color: #ef4444; }
.geo-sess-clean { background: rgba(34,197,94,0.12); color: #22c55e; }
#geo-dup-banner {
display: none; padding: 7px 14px;
background: rgba(251,191,36,0.08);
border-bottom: 1px solid rgba(251,191,36,0.15);
font-size: 10px; color: #fbbf24; flex-shrink: 0;
align-items: center; gap: 6px;
}
#geo-risk-bar {
height: 3px; border-radius: 2px; margin-top: 6px;
background: rgba(255,255,255,0.06); overflow: hidden;
}
#geo-risk-fill {
height: 100%; width: 0; border-radius: 2px;
transition: width 0.5s ease, background 0.4s;
}
#geo-autoskip-row {
display: flex; align-items: center; justify-content: space-between;
padding: 7px 14px;
background: rgba(255,255,255,0.015);
border-bottom: 1px solid rgba(255,255,255,0.05);
flex-shrink: 0;
}
#geo-autoskip-label { font-size: 10px; color: #71717a; font-weight: 500; }
#geo-autoskip-toggle {
width: 34px; height: 18px; border-radius: 9px;
background: #27272a; border: 1px solid rgba(255,255,255,0.08); cursor: pointer;
position: relative; transition: background 0.25s, border-color 0.25s; flex-shrink: 0;
}
#geo-autoskip-toggle::after {
content: ''; position: absolute;
top: 2px; left: 2px;
width: 12px; height: 12px; border-radius: 50%;
background: #52525b; transition: left 0.25s, background 0.25s;
}
#geo-autoskip-toggle.on { background: rgba(239,68,68,0.25); border-color: rgba(239,68,68,0.3); }
#geo-autoskip-toggle.on::after { left: 18px; background: #ef4444; }
#geo-hist-search {
width: 100%; box-sizing: border-box;
background: rgba(255,255,255,0.04);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 8px; color: #d4d4d8; font-size: 11px;
padding: 7px 10px 7px 30px; margin-bottom: 10px; outline: none;
font-family: inherit;
transition: border-color 0.2s, background 0.2s;
}
#geo-hist-search::placeholder { color: #52525b; }
#geo-hist-search:focus { border-color: rgba(96,165,250,0.35); background: rgba(255,255,255,0.06); }
.geo-search-wrap {
position: relative; margin-bottom: 0;
}
.geo-search-wrap svg {
position: absolute; left: 10px; top: 50%; transform: translateY(-50%);
color: #52525b; pointer-events: none;
}
.geo-search-wrap #geo-hist-search { margin-bottom: 10px; }
#geo-stats-btn { }
#geo-stats-panel { display: none; margin-bottom: 10px; padding: 8px; background: rgba(255,255,255,0.02); border-radius: 8px; border: 1px solid rgba(255,255,255,0.04); }
#geo-stats-panel.open { display: block; }
.geo-stat-row {
display: flex; align-items: center; gap: 6px;
margin-bottom: 5px;
}
.geo-stat-row:last-child { margin-bottom: 0; }
.geo-stat-label { font-size: 10px; color: #71717a; min-width: 80px; flex-shrink: 0; }
.geo-stat-barwrap {
flex: 1; height: 6px; background: rgba(255,255,255,0.05);
border-radius: 3px; overflow: hidden;
}
.geo-stat-bar { height: 100%; border-radius: 3px; background: linear-gradient(90deg, #3b82f6, #60a5fa); min-width: 4px; transition: width 0.3s; }
.geo-stat-count { font-size: 9px; color: #52525b; min-width: 20px; text-align: right; font-weight: 500; }
.geo-risk-indicator {
display: inline-flex; align-items: center; gap: 4px;
}
.geo-risk-dot {
width: 6px; height: 6px; border-radius: 50%;
display: inline-block; flex-shrink: 0;
}
.geo-info-section {
display: flex; flex-direction: column; gap: 8px;
}
.geo-card {
background: rgba(255,255,255,0.025);
border: 1px solid rgba(255,255,255,0.05);
border-radius: 10px; padding: 10px 12px;
animation: geo-card-in 0.35s cubic-bezier(0.16, 1, 0.3, 1) both;
}
.geo-card:nth-child(1) { animation-delay: 0s; }
.geo-card:nth-child(2) { animation-delay: 0.04s; }
.geo-card:nth-child(3) { animation-delay: 0.08s; }
.geo-card:nth-child(4) { animation-delay: 0.12s; }
.geo-card:nth-child(5) { animation-delay: 0.16s; }
@keyframes geo-card-in {
0% { opacity: 0; transform: translateY(8px); }
100% { opacity: 1; transform: translateY(0); }
}
.geo-card-hdr {
display: flex; align-items: center; gap: 6px;
margin-bottom: 6px; color: #71717a; font-size: 9px;
text-transform: uppercase; letter-spacing: 0.08em; font-weight: 600;
}
.geo-card-hdr svg { flex-shrink: 0; color: #52525b; }
.geo-card .geo-row { border-bottom-color: rgba(255,255,255,0.03); padding: 4px 0; }
.geo-card .geo-row:last-child { border-bottom: none; }
.geo-card .geo-label { font-size: 9.5px; color: #52525b; }
.geo-card .geo-value { font-size: 11px; }
.geo-row { cursor: pointer; position: relative; border-radius: 4px; padding: 5px 4px; margin: 0 -4px; transition: background 0.15s; }
.geo-row:hover { background: rgba(255,255,255,0.04); }
.geo-row-copied {
position: absolute; right: 4px; top: 50%; transform: translateY(-50%);
font-size: 9px; color: #22c55e; font-weight: 600;
pointer-events: none; letter-spacing: 0.02em;
animation: geo-copied-in 0.25s ease;
}
@keyframes geo-copied-in {
0% { opacity: 0; transform: translateY(-50%) translateX(4px); }
100% { opacity: 1; transform: translateY(-50%) translateX(0); }
}
.geo-tab-bar {
display: flex; border-bottom: 1px solid rgba(255,255,255,0.06);
background: rgba(255,255,255,0.015); flex-shrink: 0;
}
.geo-tab {
flex: 1; padding: 8px 0; text-align: center;
font-size: 11px; font-weight: 600; color: #52525b;
cursor: pointer; border: none; background: none;
border-bottom: 2px solid transparent;
transition: color 0.2s, border-color 0.2s, background 0.2s;
font-family: inherit; letter-spacing: 0.03em;
display: flex; align-items: center; justify-content: center; gap: 5px;
}
.geo-tab:hover { color: #a1a1aa; background: rgba(255,255,255,0.02); }
.geo-tab.active { color: #60a5fa; border-bottom-color: #3b82f6; }
.geo-tab svg { display: block; }
.geo-tab-content { display: none; }
.geo-tab-content.active { display: block; }
.geo-hero {
display: flex; align-items: center; gap: 12px;
padding: 8px 0 4px; margin-bottom: 4px;
animation: geo-hero-in 0.4s cubic-bezier(0.16, 1, 0.3, 1) both;
}
@keyframes geo-hero-in {
0% { opacity: 0; transform: scale(0.9); }
100% { opacity: 1; transform: scale(1); }
}
.geo-hero-flag { font-size: 36px; line-height: 1; flex-shrink: 0; }
.geo-hero-info { flex: 1; min-width: 0; }
.geo-hero-ip { font-size: 15px; font-weight: 700; color: #f4f4f5; letter-spacing: -0.01em; }
.geo-hero-loc { font-size: 11px; color: #71717a; margin-top: 2px; }
.geo-hero-badges { display: flex; gap: 4px; margin-top: 5px; flex-wrap: wrap; }
.geo-hero-badge {
font-size: 9px; padding: 2px 7px; border-radius: 10px;
font-weight: 600; letter-spacing: 0.02em;
animation: geo-badge-in 0.3s ease both;
}
.geo-hero-badge.vpn { background: rgba(239,68,68,0.12); color: #ef4444; animation-delay: 0.15s; }
.geo-hero-badge.clean { background: rgba(34,197,94,0.12); color: #22c55e; animation-delay: 0.15s; }
.geo-hero-badge.risk-low { background: rgba(34,197,94,0.1); color: #22c55e; animation-delay: 0.2s; }
.geo-hero-badge.risk-med { background: rgba(245,158,11,0.12); color: #f59e0b; animation-delay: 0.2s; }
.geo-hero-badge.risk-high { background: rgba(239,68,68,0.12); color: #ef4444; animation-delay: 0.2s; }
@keyframes geo-badge-in {
0% { opacity: 0; transform: scale(0.8); }
100% { opacity: 1; transform: scale(1); }
}
#geo-risk-bar {
height: 3px; border-radius: 2px; margin-top: 2px;
background: rgba(255,255,255,0.06); overflow: hidden;
}
.geo-modal-overlay {
position: fixed; inset: 0;
background: rgba(0,0,0,0.65);
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
z-index: 999999;
display: flex; align-items: center; justify-content: center;
animation: geo-overlay-in 0.25s ease;
}
@keyframes geo-overlay-in {
0% { opacity: 0; }
100% { opacity: 1; }
}
.geo-modal {
width: 380px; max-width: 92vw; max-height: 85vh;
background: rgba(18,18,22,0.98);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 16px;
box-shadow: 0 24px 80px rgba(0,0,0,0.6);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
color: #d4d4d8;
overflow: hidden;
animation: geo-modal-in 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
@keyframes geo-modal-in {
0% { opacity: 0; transform: scale(0.95) translateY(10px); }
100% { opacity: 1; transform: scale(1) translateY(0); }
}
.geo-modal-hdr {
display: flex; align-items: center; gap: 10px;
padding: 20px 24px 14px;
border-bottom: 1px solid rgba(255,255,255,0.06);
}
.geo-modal-hdr svg { color: #60a5fa; flex-shrink: 0; }
.geo-modal-hdr-text { flex: 1; }
.geo-modal-hdr h3 {
margin: 0; font-size: 15px; font-weight: 700; color: #f4f4f5;
letter-spacing: -0.01em;
}
.geo-modal-hdr p {
margin: 3px 0 0; font-size: 11px; color: #71717a;
}
.geo-modal-body {
padding: 16px 24px;
overflow-y: auto; max-height: 50vh;
font-size: 12px; line-height: 1.65; color: #a1a1aa;
}
.geo-modal-body h4 {
font-size: 11px; font-weight: 600; color: #d4d4d8;
margin: 14px 0 6px; text-transform: uppercase; letter-spacing: 0.05em;
}
.geo-modal-body h4:first-child { margin-top: 0; }
.geo-modal-body ul {
margin: 4px 0 0; padding-left: 16px;
}
.geo-modal-body ul li {
margin-bottom: 3px;
}
.geo-modal-body .geo-tos-highlight {
background: rgba(96,165,250,0.06);
border: 1px solid rgba(96,165,250,0.12);
border-radius: 8px; padding: 8px 10px;
margin: 8px 0; font-size: 11px; color: #93c5fd;
}
.geo-modal-footer {
display: flex; align-items: center; gap: 8px;
padding: 14px 24px 18px;
border-top: 1px solid rgba(255,255,255,0.06);
}
.geo-modal-footer .geo-m-btn {
flex: 1; padding: 9px 0;
border-radius: 8px; border: none;
font-size: 12px; font-weight: 600;
cursor: pointer; font-family: inherit;
transition: background 0.2s, color 0.2s;
letter-spacing: 0.01em;
}
.geo-m-btn-secondary {
background: rgba(255,255,255,0.05); color: #a1a1aa;
}
.geo-m-btn-secondary:hover { background: rgba(255,255,255,0.1); color: #d4d4d8; }
.geo-m-btn-primary {
background: #3b82f6; color: #fff;
}
.geo-m-btn-primary:hover { background: #2563eb; }
.geo-m-btn-danger {
background: rgba(239,68,68,0.12); color: #ef4444;
}
.geo-m-btn-danger:hover { background: rgba(239,68,68,0.2); }
`;
document.head.appendChild(style);
const livePanel = document.createElement('div');
livePanel.id = 'geo-live';
livePanel.className = 'geo-panel';
livePanel.innerHTML = `
<div class="geo-panel-header">
<div class="geo-panel-title">
<span id="geo-status-dot"></span>
IP Geolocation
</div>
<div class="geo-hdr-actions">
<button class="geo-icon-btn" id="geo-telemetry-btn" title="Anonymous usage analytics">${IC.activity()}</button>
<button class="geo-icon-btn" id="geo-api-btn" title="Set IPInfo API key">${IC.key()}</button>
<button class="geo-icon-btn" id="geo-notif-btn" title="Notifications">${IC.bell()}</button>
<button class="geo-icon-btn" id="geo-stats-btn" title="Country stats">${IC.chart()}</button>
<button class="geo-icon-btn geo-minimize-btn" title="Minimize">${IC.minus()}</button>
</div>
</div>
<div id="geo-session-bar">
<span class="geo-sess-badge geo-sess-total" id="geo-sess-total">0 total</span>
<span class="geo-sess-badge geo-sess-vpn" id="geo-sess-vpn">0 VPN</span>
<span class="geo-sess-badge geo-sess-clean" id="geo-sess-clean">0 clean</span>
</div>
<div id="geo-autoskip-row">
<span id="geo-autoskip-label">Auto-skip VPN/Proxy</span>
<button id="geo-autoskip-toggle" title="Toggle auto-skip"></button>
</div>
<div id="geo-own-bar">
<span id="geo-own-flag">${IC.globe()}</span>
<span id="geo-own-text">You: detecting…</span>
</div>
<div id="geo-dup-banner"></div>
<div class="geo-tab-bar">
<button class="geo-tab active" data-tab="info">${IC.globe(12)} Info</button>
<button class="geo-tab" data-tab="history">${IC.clock(12)} History</button>
</div>
<div class="geo-panel-body">
<div class="geo-tab-content active" id="geo-tab-info">
<div id="geo-flag"></div>
<div id="geo-waiting">Waiting for connection…</div>
<div id="geo-rows"></div>
<button id="geo-copy-btn" class="geo-btn" style="display:none">${IC.copy()} Copy Info</button>
<a id="geo-map-btn" target="_blank" rel="noopener" style="display:none">${IC.mapPin()} Open in Maps</a>
</div>
<div class="geo-tab-content" id="geo-tab-history">
<div id="geo-hist-header-row">
<span id="geo-hist-count">0 entries</span>
<button id="geo-hist-clear">${IC.trash(12)} Clear All</button>
</div>
<div id="geo-export-bar">
<button class="geo-export-btn" id="geo-export-json">${IC.download(11)} JSON</button>
<button class="geo-export-btn" id="geo-export-csv">${IC.download(11)} CSV</button>
</div>
<div id="geo-stats-panel"></div>
<div class="geo-search-wrap">
${IC.search(12)}
<input id="geo-hist-search" type="text" placeholder="Search IP, city, country..." />
</div>
<div id="geo-history-list"></div>
<div id="geo-hist-empty">No history yet</div>
</div>
</div>
<div class="geo-credits"><a href="https://github.com/w0wzahh" target="_blank" rel="noopener">${IC.github(11)} w0wzahh</a> <span style="color:#27272a;font-size:9px">·</span> <a href="https://gitlab.com/MysteryBlokHed" target="_blank" rel="noopener" style="color:#3f3f46;font-size:9px;text-decoration:none">CamSurf detection based on MysteryBlokHed</a></div>
<div class="geo-resize-handle"></div>
`;
document.body.appendChild(livePanel);
const makeDraggable = (panel) => {
const header = panel.querySelector('.geo-panel-header');
let dragging = false, ox = 0, oy = 0;
header.addEventListener('mousedown', (e) => {
if (e.target.tagName === 'BUTTON') return;
e.preventDefault();
const rect = panel.getBoundingClientRect();
panel.style.left = rect.left + 'px';
panel.style.top = rect.top + 'px';
panel.style.right = 'unset';
dragging = true;
ox = e.clientX - rect.left;
oy = e.clientY - rect.top;
});
document.addEventListener('mousemove', (e) => {
if (dragging) {
panel.style.left = (e.clientX - ox) + 'px';
panel.style.top = (e.clientY - oy) + 'px';
}
});
document.addEventListener('mouseup', () => { dragging = false; });
};
const makeResizable = (panel) => {
const handle = panel.querySelector('.geo-resize-handle');
let resizing = false, rsx = 0, rsy = 0, rsw = 0, rsh = 0;
handle.addEventListener('mousedown', (e) => {
e.preventDefault();
e.stopPropagation();
resizing = true;
rsx = e.clientX; rsy = e.clientY;
rsw = panel.offsetWidth; rsh = panel.offsetHeight;
});
document.addEventListener('mousemove', (e) => {
if (resizing) {
panel.style.width = Math.max(200, rsw + e.clientX - rsx) + 'px';
panel.style.height = Math.max(80, rsh + e.clientY - rsy) + 'px';
}
});
document.addEventListener('mouseup', () => { resizing = false; });
};
const makeMinimizable = (panel) => {
const btn = panel.querySelector('.geo-minimize-btn');
btn.addEventListener('click', () => {
panel.classList.toggle('minimized');
btn.innerHTML = panel.classList.contains('minimized') ? IC.plus() : IC.minus();
});
};
makeDraggable(livePanel);
makeResizable(livePanel);
makeMinimizable(livePanel);
livePanel.querySelectorAll('.geo-tab').forEach(tab => {
tab.addEventListener('click', () => {
livePanel.querySelectorAll('.geo-tab').forEach(t => t.classList.remove('active'));
livePanel.querySelectorAll('.geo-tab-content').forEach(c => c.classList.remove('active'));
tab.classList.add('active');
document.getElementById('geo-tab-' + tab.dataset.tab).classList.add('active');
});
});
const dot = document.getElementById('geo-status-dot');
const flagEl = document.getElementById('geo-flag');
const waitingEl = document.getElementById('geo-waiting');
const rowsEl = document.getElementById('geo-rows');
const copyBtn = document.getElementById('geo-copy-btn');
const dupBanner = document.getElementById('geo-dup-banner');
let currentEntry = null;
let relayHiddenShown = false;
const setStatus = (state) => { dot.className = state; };
const updateSessionBar = () => {
document.getElementById('geo-sess-total').textContent = sessionTotal + ' total';
document.getElementById('geo-sess-vpn').textContent = sessionVPN + ' VPN';
document.getElementById('geo-sess-clean').textContent = sessionClean + ' clean';
};
const buildRows = (fields, extra) => {
const groups = [
{ title: 'Identity', icon: IC.user(11), keys: ['Name (PTR)', 'IP', 'Hostname'] },
{ title: 'Security', icon: IC.shield(11), keys: ['Proxy / VPN', 'Risk Score', 'Connection Type', 'Last Seen'] },
{ title: 'Location', icon: IC.mapPin(11), keys: ['Address', 'Country', 'Region', 'City', 'Postal', 'Continent', 'Latitude', 'Longitude'] },
{ title: 'Network', icon: IC.activity(11), keys: ['Timezone', 'ISP / Provider', 'Organisation', 'ASN'] },
];
const fieldMap = {};
fields.forEach(([label, value, hi]) => { fieldMap[label] = { value, hi }; });
let html = '';
if (extra && extra.hero) html += extra.hero;
html += '<div class="geo-info-section">';
groups.forEach(g => {
const rows = g.keys.filter(k => fieldMap[k]).map(k => {
const f = fieldMap[k];
return '<div class="geo-row" data-copy-value="' + esc(f.value) + '">' +
'<span class="geo-label">' + esc(k) + '</span>' +
'<span class="geo-value' + (f.hi ? ' highlight' : '') + '">' + esc(f.value) + '</span>' +
'</div>';
});
if (!rows.length) return;
html += '<div class="geo-card">' +
'<div class="geo-card-hdr">' + g.icon + ' ' + g.title + '</div>' +
rows.join('') +
'</div>';
});
html += '</div>';
rowsEl.innerHTML = html;
rowsEl.querySelectorAll('.geo-row[data-copy-value]').forEach(row => {
row.addEventListener('click', () => {
const val = row.getAttribute('data-copy-value');
if (!val || val === 'N/A') return;
navigator.clipboard.writeText(val).then(() => {
const existing = row.querySelector('.geo-row-copied');
if (existing) existing.remove();
const badge = document.createElement('span');
badge.className = 'geo-row-copied';
badge.textContent = 'Copied!';
row.appendChild(badge);
setTimeout(() => badge.remove(), 1500);
});
});
});
};
const clearPanelFn = () => {
currentEntry = null;
relayHiddenShown = false;
flagEl.innerHTML = '';
rowsEl.innerHTML = '';
copyBtn.style.display = 'none';
dupBanner.style.display = 'none';
dupBanner.innerHTML = '';
waitingEl.textContent = 'Waiting for connection\u2026';
waitingEl.style.display = '';
setStatus('');
};
const playPing = () => {
if (!soundEnabled) return;
try {
const ctx = new (win.AudioContext || win.webkitAudioContext)();
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.connect(gain);
gain.connect(ctx.destination);
osc.type = 'sine';
osc.frequency.setValueAtTime(880, ctx.currentTime);
osc.frequency.exponentialRampToValueAtTime(440, ctx.currentTime + 0.2);
gain.gain.setValueAtTime(0.12, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.4);
osc.start(ctx.currentTime);
osc.stop(ctx.currentTime + 0.4);
osc.onended = () => ctx.close();
} catch (_) {}
};
const extractNameFromHostname = (hostname) => {
if (!hostname || hostname === 'N/A') return null;
const SKIP = new Set([
'dynamic','static','broadband','cable','dsl','fiber','fibre','pool','ip',
'ptr','rdns','rev','reverse','customer','residential','home','host',
'node','dhcp','dhcppc','lease','assigned','allocated','client','user',
'subscriber','access','internet','service','provider','network','net',
'com','org','edu','gov','info','biz','int','arpa','local','in','addr',
'cpe','cust','bredband','adsl','vdsl','fttp','ftth','ppp','dial',
'mobile','cellular','lte','nas','gpon','olt','bbnet',
]);
const parts = hostname.toLowerCase().split(/[\.\-_]/);
const candidates = parts.filter(p =>
/^[a-z]{3,16}$/.test(p) && !SKIP.has(p) && !/^(\d)/.test(p)
);
if (candidates.length >= 2) {
return candidates.slice(0, 2)
.map(s => s.charAt(0).toUpperCase() + s.slice(1))
.join(' ');
} else if (candidates.length === 1) {
const c = candidates[0];
return c.charAt(0).toUpperCase() + c.slice(1);
}
return null;
};
const entryToText = (e) => [
'Name : ' + (e.name || 'N/A') + (e.name ? ' (from PTR)' : ''),
'IP : ' + e.ip,
'Proxy/VPN : ' + (e.proxy || 'N/A'),
'Risk Score: ' + (e.risk != null ? e.risk + '/100' : 'N/A'),
'Conn Type : ' + (e.connType || 'N/A'),
'Last Seen : ' + (e.lastSeen || 'N/A'),
'Address : ' + (e.address || 'N/A'),
'Hostname : ' + e.hostname,
'Country : ' + e.country,
'Region : ' + e.region,
'City : ' + e.city,
'Postal : ' + e.postal,
'Continent : ' + (e.continent || 'N/A'),
'Latitude : ' + (e.lat != null ? Math.abs(e.lat).toFixed(4) + '\u00b0 ' + (e.lat >= 0 ? 'N' : 'S') : 'N/A'),
'Longitude : ' + (e.lon != null ? Math.abs(e.lon).toFixed(4) + '\u00b0 ' + (e.lon >= 0 ? 'E' : 'W') : 'N/A'),
'Timezone : ' + e.timezone,
'ISP : ' + e.isp,
'Organisatn: ' + (e.organisation || 'N/A'),
'ASN : ' + e.asn,
'Time : ' + e.timestamp,
].join('\n');
copyBtn.addEventListener('click', () => {
if (!currentEntry) return;
navigator.clipboard.writeText(entryToText(currentEntry)).then(() => {
copyBtn.innerHTML = IC.check() + ' Copied!';
copyBtn.classList.add('copied');
setTimeout(() => {
copyBtn.innerHTML = IC.copy() + ' Copy Info';
copyBtn.classList.remove('copied');
}, 2000);
});
});
const renderStats = () => {
const entries = loadHistory();
const statsPanel = document.getElementById('geo-stats-panel');
if (!statsPanel.classList.contains('open')) return;
const map = {};
entries.forEach(e => {
const k = (e.flag || '') + ' ' + e.country;
map[k] = (map[k] || 0) + 1;
});
const sorted = Object.entries(map).sort((a, b) => b[1] - a[1]).slice(0, 8);
const max = sorted.length ? sorted[0][1] : 1;
statsPanel.innerHTML = sorted.length ? sorted.map(([country, count]) =>
'<div class="geo-stat-row">' +
'<span class="geo-stat-label">' + esc(country.slice(0, 18)) + '</span>' +
'<div class="geo-stat-barwrap"><div class="geo-stat-bar" style="width:' + Math.round(count / max * 100) + '%"></div></div>' +
'<span class="geo-stat-count">' + count + '</span>' +
'</div>'
).join('') : '<div style="color:#444;font-size:10px;padding:4px 0">No data yet</div>';
};
const renderHistory = (filter) => {
const list = document.getElementById('geo-history-list');
const empty = document.getElementById('geo-hist-empty');
const countEl = document.getElementById('geo-hist-count');
let entries = loadHistory();
countEl.textContent = entries.length + ' entr' + (entries.length === 1 ? 'y' : 'ies');
const stars = loadStars();
const q = (filter || '').toLowerCase().trim();
if (q) {
entries = entries.filter(e =>
(e.ip || '').includes(q) ||
(e.city || '').toLowerCase().includes(q) ||
(e.country || '').toLowerCase().includes(q) ||
(e.isp || '').toLowerCase().includes(q) ||
(e.hostname || '').toLowerCase().includes(q)
);
}
if (!entries.length) {
list.innerHTML = '';
empty.style.display = '';
return;
}
empty.style.display = 'none';
list.innerHTML = entries.map((e, i) => {
const starred = !!stars[e.ip];
const loc = (e.city !== 'N/A' ? e.city + ', ' : '') + e.country;
let badges = '';
if (e.proxy && e.proxy.toLowerCase() === 'yes') {
badges += '<span class="geo-hist-badge vpn">VPN</span>';
} else {
badges += '<span class="geo-hist-badge clean">Clean</span>';
}
if (e.risk != null) {
const r = +e.risk;
const rc = r >= 66 ? 'risk-high' : r >= 33 ? 'risk-med' : 'risk-low';
badges += '<span class="geo-hist-badge ' + rc + '">Risk ' + r + '</span>';
}
const detailGroups = [
{ title: 'Identity', icon: IC.user(10), rows: [
['Name (PTR)', e.name || 'N/A'], ['IP', e.ip], ['Hostname', e.hostname]
]},
{ title: 'Security', icon: IC.shield(10), rows: [
['Proxy / VPN', e.proxy || 'N/A'],
['Risk Score', e.risk != null ? e.risk + '/100' : 'N/A'],
['Connection Type', e.connType || 'N/A'],
['Last Seen', e.lastSeen || 'N/A']
]},
{ title: 'Location', icon: IC.mapPin(10), rows: [
['Address', e.address || 'N/A'], ['Region', e.region], ['Postal', e.postal],
['Continent', e.continent || 'N/A'],
['Latitude', e.lat != null ? Math.abs(e.lat).toFixed(4) + '\u00b0 ' + (e.lat >= 0 ? 'N' : 'S') : 'N/A'],
['Longitude', e.lon != null ? Math.abs(e.lon).toFixed(4) + '\u00b0 ' + (e.lon >= 0 ? 'E' : 'W') : 'N/A']
]},
{ title: 'Network', icon: IC.activity(10), rows: [
['Timezone', e.timezone], ['ISP / Provider', e.isp],
['Organisation', e.organisation || 'N/A'], ['ASN', e.asn]
]}
];
const detailHtml = detailGroups.map(g => {
const rows = g.rows.map(([l, v]) =>
'<div class="geo-hist-detail-row" data-copy-value="' + esc(v) + '"><span>' + esc(l) + '</span><span>' + esc(v) + '</span></div>'
).join('');
return '<div class="geo-hist-dcard"><div class="geo-hist-dcard-hdr">' + g.icon + ' ' + g.title + '</div>' + rows + '</div>';
}).join('');
return '<div class="geo-hist-entry" data-idx="' + i + '" data-ip="' + esc(e.ip) + '">' +
'<div class="geo-hist-hero">' +
'<span class="geo-hist-flag">' + esc(e.flag || '') + '</span>' +
'<div class="geo-hist-info">' +
'<div class="geo-hist-ip">' + esc(e.ip) + '</div>' +
'<div class="geo-hist-loc">' + esc(loc) + '</div>' +
'<div class="geo-hist-badges">' + badges + '</div>' +
'</div>' +
'<div class="geo-hist-meta">' +
'<span class="geo-hist-time">' + esc(e.timestamp) + '</span>' +
'<button class="geo-hist-star' + (starred ? ' active' : '') + '" data-ip="' + esc(e.ip) + '" title="Star">' + (starred ? IC.starFill() : IC.star()) + '</button>' +
'</div>' +
'</div>' +
'<div class="geo-hist-expand-hint">' + IC.chevDown(10) + '</div>' +
'<div class="geo-hist-detail">' + detailHtml +
'<button class="geo-hist-copy" data-idx="' + i + '">' + IC.copy(11) + ' Copy All</button>' +
'</div>' +
'</div>';
}).join('');
list.querySelectorAll('.geo-hist-entry').forEach(el => {
el.addEventListener('click', (e) => {
if (e.target.closest('.geo-hist-copy') || e.target.closest('.geo-hist-star') || e.target.closest('.geo-hist-detail-row')) return;
el.classList.toggle('open');
});
});
list.querySelectorAll('.geo-hist-detail-row[data-copy-value]').forEach(row => {
row.addEventListener('click', (ev) => {
ev.stopPropagation();
const val = row.getAttribute('data-copy-value');
if (!val || val === 'N/A') return;
navigator.clipboard.writeText(val).then(() => {
const existing = row.querySelector('.geo-row-copied');
if (existing) existing.remove();
const badge = document.createElement('span');
badge.className = 'geo-row-copied';
badge.textContent = 'Copied!';
row.appendChild(badge);
setTimeout(() => badge.remove(), 1500);
});
});
});
list.querySelectorAll('.geo-hist-copy').forEach(btn => {
btn.addEventListener('click', (ev) => {
ev.stopPropagation();
const allEntries = loadHistory();
const entry = allEntries[+btn.dataset.idx];
if (!entry) return;
navigator.clipboard.writeText(entryToText(entry)).then(() => {
btn.innerHTML = IC.check(11) + ' Copied!';
setTimeout(() => { btn.innerHTML = IC.copy(11) + ' Copy All'; }, 2000);
});
});
});
list.querySelectorAll('.geo-hist-star').forEach(btn => {
btn.addEventListener('click', (ev) => {
ev.stopPropagation();
const ip = btn.dataset.ip;
toggleStar(ip);
const nowStarred = isStarred(ip);
btn.innerHTML = nowStarred ? IC.starFill() : IC.star();
btn.classList.toggle('active', nowStarred);
});
});
};
document.getElementById('geo-hist-clear').addEventListener('click', () => {
localStorage.removeItem(HISTORY_KEY);
renderHistory();
renderStats();
});
document.getElementById('geo-stats-btn').addEventListener('click', () => {
const p = document.getElementById('geo-stats-panel');
p.classList.toggle('open');
renderStats();
});
document.getElementById('geo-hist-search').addEventListener('input', (e) => {
renderHistory(e.target.value);
});
const exportEntries = (format) => {
const entries = loadHistory();
if (!entries.length) return;
let content, mime, filename;
if (format === 'json') {
content = JSON.stringify(entries, null, 2);
mime = 'application/json';
filename = 'geo_history.json';
} else {
const headers = ['IP','Country','City','Region','Postal','Address','Proxy','Risk','Connection Type','Latitude','Longitude','Continent','Timezone','ISP','Organisation','ASN','Hostname','Name','Last Seen','Timestamp'];
const rows = entries.map(e => [
e.ip, e.country, e.city, e.region, e.postal,
e.address || 'N/A', e.proxy || 'N/A',
e.risk != null ? e.risk : 'N/A',
e.connType || 'N/A',
e.lat != null ? e.lat.toFixed(6) : 'N/A',
e.lon != null ? e.lon.toFixed(6) : 'N/A',
e.continent || 'N/A',
e.timezone, e.isp, e.organisation || 'N/A', e.asn, e.hostname,
e.name || 'N/A', e.lastSeen || 'N/A', e.timestamp
].map(v => '"' + String(v || '').replace(/"/g, '""') + '"').join(','));
content = [headers.join(','), ...rows].join('\n');
mime = 'text/csv';
filename = 'geo_history.csv';
}
const blob = new Blob([content], { type: mime });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = filename; a.click();
setTimeout(() => URL.revokeObjectURL(url), 1000);
};
document.getElementById('geo-export-json').addEventListener('click', () => exportEntries('json'));
document.getElementById('geo-export-csv').addEventListener('click', () => exportEntries('csv'));
document.getElementById('geo-autoskip-toggle').addEventListener('click', () => {
autoSkipVPN = !autoSkipVPN;
document.getElementById('geo-autoskip-toggle').classList.toggle('on', autoSkipVPN);
});
document.getElementById('geo-notif-btn').addEventListener('click', () => {
if (typeof Notification === 'undefined') return;
if (Notification.permission === 'default') {
Notification.requestPermission();
} else if (Notification.permission === 'granted') {
notifEnabled = !notifEnabled;
const nb = document.getElementById('geo-notif-btn');
nb.innerHTML = notifEnabled ? IC.bell() : IC.bellOff();
nb.classList.toggle('active', notifEnabled);
}
});
const telemetryBtn = document.getElementById('geo-telemetry-btn');
const updateTelemetryButton = () => {
const consent = getTelemetryConsent() === 'yes';
telemetryBtn.innerHTML = consent ? IC.chart() : IC.activity();
telemetryBtn.classList.toggle('active', consent);
telemetryBtn.title = consent
? 'Anonymous analytics: ON (click to disable)'
: 'Anonymous analytics: OFF (click to enable)';
};
const showModal = (opts) => {
return new Promise((resolve) => {
const overlay = document.createElement('div');
overlay.className = 'geo-modal-overlay';
const modal = document.createElement('div');
modal.className = 'geo-modal';
let bodyHtml = '';
if (opts.body) bodyHtml = opts.body;
let footerHtml = '';
(opts.buttons || []).forEach((b, i) => {
footerHtml += '<button class="geo-m-btn ' + (b.cls || 'geo-m-btn-secondary') + '" data-idx="' + i + '">' + b.label + '</button>';
});
modal.innerHTML =
'<div class="geo-modal-hdr">' +
(opts.icon || IC.shield(22)) +
'<div class="geo-modal-hdr-text">' +
'<h3>' + (opts.title || '') + '</h3>' +
(opts.subtitle ? '<p>' + opts.subtitle + '</p>' : '') +
'</div>' +
'</div>' +
'<div class="geo-modal-body">' + bodyHtml + '</div>' +
'<div class="geo-modal-footer">' + footerHtml + '</div>';
overlay.appendChild(modal);
document.body.appendChild(overlay);
const close = (val) => {
overlay.style.animation = 'none';
overlay.style.opacity = '0';
overlay.style.transition = 'opacity 0.15s';
setTimeout(() => { overlay.remove(); resolve(val); }, 160);
};
modal.querySelectorAll('.geo-m-btn').forEach(btn => {
btn.addEventListener('click', () => {
const b = opts.buttons[+btn.dataset.idx];
close(b.value);
});
});
overlay.addEventListener('click', (e) => {
if (e.target === overlay) close(null);
});
});
};
const TOS_BODY =
'<h4>Terms of Service</h4>' +
'<p>By using Ome.tv IP Geolocation ("the Script"), you agree to the following terms:</p>' +
'<ul>' +
'<li><strong>Personal use only.</strong> The Script is provided for informational and educational purposes. Do not use it to harass, stalk, or harm others.</li>' +
'<li><strong>No warranty.</strong> The Script is provided "as is" without warranty of any kind. Geolocation data is approximate and may be inaccurate.</li>' +
'<li><strong>Third-party APIs.</strong> The Script queries ipinfo.io and proxycheck.io. Your use is subject to their respective terms of service and rate limits.</li>' +
'<li><strong>API keys.</strong> You are responsible for your own API keys. Do not share them publicly.</li>' +
'<li><strong>Privacy.</strong> The Script does not collect personal data by default. If you opt in to anonymous analytics, only a random install ID, script version, date, and coarse locale country are sent to our telemetry endpoint.</li>' +
'<li><strong>Liability.</strong> The author is not liable for any misuse, data inaccuracy, or damages arising from use of the Script.</li>' +
'</ul>' +
'<h4>Anonymous Usage Analytics</h4>' +
'<p>To help improve the Script, you may opt in to anonymous analytics.</p>' +
'<div class="geo-tos-highlight">' +
'<strong>Data sent:</strong> Random install ID, script version, date, and coarse locale country.<br>' +
'<strong>Not sent:</strong> IP addresses, chat content, geolocation lookups, or any personal information.' +
'</div>' +
'<p>You can change your analytics preference at any time by clicking the analytics icon in the panel header.</p>';
const askTelemetryConsentIfNeeded = async () => {
const existing = getTelemetryConsent();
if (existing === 'yes' || existing === 'no') {
updateTelemetryButton();
return;
}
const result = await showModal({
icon: IC.shield(22),
title: 'Terms of Service & Privacy',
subtitle: 'Please review before continuing',
body: TOS_BODY,
buttons: [
{ label: 'Decline Analytics', cls: 'geo-m-btn-secondary', value: 'decline' },
{ label: 'Accept & Enable Analytics', cls: 'geo-m-btn-primary', value: 'accept' }
]
});
const accepted = result === 'accept';
setTelemetryConsent(accepted);
updateTelemetryButton();
if (accepted) {
pingTelemetryInstallOnce('first_run_prompt');
pingTelemetrySessionDaily();
pingTelemetryVersion();
}
};
telemetryBtn.addEventListener('click', async () => {
const enabled = getTelemetryConsent() === 'yes';
if (enabled) {
const result = await showModal({
icon: IC.activity(22),
title: 'Disable Analytics',
subtitle: 'Anonymous usage analytics',
body: '<p>Are you sure you want to disable anonymous usage analytics? No data will be sent after disabling.</p>',
buttons: [
{ label: 'Keep Enabled', cls: 'geo-m-btn-secondary', value: 'keep' },
{ label: 'Disable', cls: 'geo-m-btn-danger', value: 'disable' }
]
});
if (result !== 'disable') return;
setTelemetryConsent(false);
updateTelemetryButton();
return;
}
if (!telemetryEndpointConfigured()) {
await showModal({
icon: IC.warn(22),
title: 'Not Configured',
body: '<p>Analytics endpoint is not configured in the script source yet.</p>',
buttons: [{ label: 'OK', cls: 'geo-m-btn-secondary', value: 'ok' }]
});
updateTelemetryButton();
return;
}
const result = await showModal({
icon: IC.shield(22),
title: 'Enable Analytics',
subtitle: 'Anonymous usage analytics',
body: '<p>Enable anonymous usage analytics to help improve the Script?</p>' +
'<div class="geo-tos-highlight">' +
'<strong>Data sent:</strong> Random install ID, script version, date, and coarse locale country only.<br>' +
'<strong>Not sent:</strong> IP addresses, chat content, or geolocation lookups.' +
'</div>',
buttons: [
{ label: 'Not Now', cls: 'geo-m-btn-secondary', value: 'no' },
{ label: 'Enable Analytics', cls: 'geo-m-btn-primary', value: 'yes' }
]
});
if (result !== 'yes') return;
setTelemetryConsent(true);
updateTelemetryButton();
pingTelemetryInstallOnce('settings_toggle');
pingTelemetrySessionDaily();
pingTelemetryVersion();
});
const fetchOwnInfo = () => {
const apiKey = loadApiKey();
if (!apiKey) {
ownIP = null;
document.getElementById('geo-own-flag').innerHTML = IC.globe();
document.getElementById('geo-own-text').textContent = 'You: add IPInfo API key (key icon)';
return;
}
gmFetch('https://ipinfo.io/json?token=' + encodeURIComponent(apiKey))
.then(r => r.json())
.then(d => {
ownIP = d.ip;
const flag = countryFlag(d.country);
const country = d.country ? regionNames.of(d.country) : 'Unknown';
document.getElementById('geo-own-flag').innerHTML = flag || IC.globe();
document.getElementById('geo-own-text').textContent =
'You: ' + d.ip + ' \u2014 ' + country;
})
.catch(() => {
ownIP = null;
document.getElementById('geo-own-text').textContent = 'You: invalid API key';
});
};
const configureApiKey = () => {
const current = loadApiKey();
const entered = window.prompt('Enter your IPInfo API token (saved locally in your browser):', current);
if (entered == null) return;
const clean = entered.trim();
if (!clean) {
localStorage.removeItem(API_KEY_STORAGE);
ownIP = null;
setStatus('error');
waitingEl.textContent = 'IPInfo API key removed. Click the key icon to add your key.';
waitingEl.style.display = '';
document.getElementById('geo-own-flag').innerHTML = IC.globe();
document.getElementById('geo-own-text').textContent = 'You: add IPInfo API key (key icon)';
return;
}
saveApiKey(clean);
setStatus('loading');
waitingEl.textContent = 'API key saved. Waiting for connection\u2026';
waitingEl.style.display = '';
fetchOwnInfo();
};
document.getElementById('geo-api-btn').addEventListener('click', configureApiKey);
document.addEventListener('keydown', (e) => {
if (e.key === 'c' || e.key === 'C') {
const active = document.activeElement;
if (active && (active.tagName === 'INPUT' || active.tagName === 'TEXTAREA')) return;
if (!currentEntry) return;
navigator.clipboard.writeText(entryToText(currentEntry)).then(() => {
copyBtn.innerHTML = IC.check() + ' Copied!';
copyBtn.classList.add('copied');
setTimeout(() => {
copyBtn.innerHTML = IC.copy() + ' Copy Info';
copyBtn.classList.remove('copied');
}, 2000);
});
}
});
const getLocationFn = async (ip) => {
try {
const apiKey = loadApiKey();
if (!apiKey) {
setStatus('error');
flagEl.innerHTML = IC.key(28);
waitingEl.textContent = 'No IPInfo API key set. Click the key icon in the header.';
waitingEl.style.display = '';
return;
}
const [ipResp, proxyResp] = await Promise.all([
gmFetch('https://ipinfo.io/' + ip + '?token=' + encodeURIComponent(apiKey)),
gmFetch('https://proxycheck.io/v2/' + ip + '?key=' + _pk + '&vpn=1&asn=1&seen=1').catch(() => null)
]);
if (!ipResp.ok) throw new Error('HTTP ' + ipResp.status);
const d = await ipResp.json();
if (d.bogon) {
setStatus('error');
flagEl.innerHTML = IC.lock(28);
waitingEl.textContent = 'Private / bogon address';
waitingEl.style.display = '';
return;
}
const TYPE_LABELS = { DCH: 'Datacenter', VPN: 'VPN', TOR: 'Tor Exit', PUB: 'Public Proxy', WEB: 'Web Proxy', SES: 'Search Engine' };
let proxyType = 'N/A';
let riskScore = null;
let pcLat = null, pcLon = null, pcCity = null, pcRegion = null, pcCountry = null;
let pcProvider = null, pcOrg = null, pcContinent = null, pcConnType = null, pcLastSeen = null;
try {
if (proxyResp && proxyResp.ok) {
const pd = await proxyResp.json();
const ipData = pd[ip];
if (ipData) {
proxyType = ipData.proxy === 'yes'
? (TYPE_LABELS[ipData.type] || ipData.type || 'Proxy')
: 'Clean';
if (ipData.risk != null) riskScore = parseInt(ipData.risk, 10);
if (ipData.latitude != null) pcLat = parseFloat(ipData.latitude);
if (ipData.longitude != null) pcLon = parseFloat(ipData.longitude);
if (ipData.city) pcCity = ipData.city;
if (ipData.region) pcRegion = ipData.region;
if (ipData.country) pcCountry = ipData.country;
if (ipData.provider) pcProvider = ipData.provider;
if (ipData.organisation) pcOrg = ipData.organisation;
if (ipData.continent) pcContinent = ipData.continent;
if (ipData.type) pcConnType = TYPE_LABELS[ipData.type] || ipData.type;
if (ipData['last seen human']) pcLastSeen = ipData['last seen human'];
else if (ipData.lastseen) pcLastSeen = ipData.lastseen;
}
}
} catch (_) {}
const isVPN = proxyType !== 'Clean' && proxyType !== 'N/A';
if (autoSkipVPN && isVPN) {
const skipBtn = document.querySelector(
SITE === 'camsurf'
? '.rv_buttons .btn-next, .next-btn, button[class*="next"], [data-action="next"]'
: SITE === 'umingle'
? 'button[class*="next"], button[class*="skip"], [data-action="next"], [data-action="skip"], button.next-btn'
: '.controls__btn-decline, [data-action="decline"], button[class*="skip"], button[class*="next"], button[class*="decline"]'
);
if (skipBtn) skipBtn.click();
}
const flag = countryFlag(d.country);
const countryLabel = d.country
? regionNames.of(d.country) + ' (' + d.country + ')'
: 'Unknown';
const org = d.org || '';
const orgParts = org.match(/^(AS\d+)\s(.+)$/);
const asn = orgParts ? orgParts[1] : 'N/A';
const isp = orgParts ? orgParts[2] : (org || 'N/A');
const provider = pcProvider || isp;
const organisation = pcOrg || (orgParts ? orgParts[2] : '') || 'N/A';
const continent = pcContinent || 'N/A';
const connType = pcConnType || 'N/A';
const lastSeen = pcLastSeen || null;
const hostname = d.hostname || 'N/A';
let rdnsHostname = hostname;
if (hostname === 'N/A') {
try {
const octets = ip.split('.').reverse().join('.');
const dnsResp = await gmFetch('https://dns.google/resolve?name=' + octets + '.in-addr.arpa&type=PTR');
if (dnsResp.ok) {
const dnsData = await dnsResp.json();
if (dnsData.Answer && dnsData.Answer.length > 0) {
let ptr = dnsData.Answer[dnsData.Answer.length - 1].data;
if (ptr && ptr.endsWith('.')) ptr = ptr.slice(0, -1);
if (ptr) rdnsHostname = ptr;
}
}
} catch (_) {}
}
const name = extractNameFromHostname(rdnsHostname);
let lat = null, lon = null;
if (d.loc && d.loc.includes(',')) {
const parts = d.loc.split(',');
lat = parseFloat(parts[0]);
lon = parseFloat(parts[1]);
}
if (pcLat != null && pcLon != null) {
if (lat == null || lon == null) {
lat = pcLat; lon = pcLon;
} else {
const dLat = Math.abs(lat - pcLat), dLon = Math.abs(lon - pcLon);
if (dLat > 0.5 || dLon > 0.5) {
lat = pcLat; lon = pcLon;
}
}
}
const fmtCoord = (v, pos, neg) => {
if (v == null || isNaN(v)) return 'N/A';
const dir = v >= 0 ? pos : neg;
return Math.abs(v).toFixed(4) + '° ' + dir;
};
const latStr = fmtCoord(lat, 'N', 'S');
const lonStr = fmtCoord(lon, 'E', 'W');
const locRaw = (lat != null && lon != null) ? lat.toFixed(6) + ',' + lon.toFixed(6) : 'N/A';
const city = pcCity || d.city || 'N/A';
const region = pcRegion || d.region || 'N/A';
const addrLine = [city !== 'N/A' ? city : d.city, region !== 'N/A' ? region : d.region].filter(Boolean).join(', ');
const address = [addrLine, d.postal, d.country ? regionNames.of(d.country) : ''].filter(Boolean).join(' ') || 'N/A';
const now = new Date();
const timestamp = now.toLocaleDateString('en-GB') + ' '
+ now.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' });
const prevHistory = loadHistory();
const prevEntry = prevHistory.find(e => e.ip === ip);
sessionTotal++;
if (isVPN) sessionVPN++;
else if (proxyType === 'Clean') sessionClean++;
updateSessionBar();
currentEntry = {
ip: d.ip, flag, country: countryLabel,
name: name || null,
hostname: rdnsHostname, region: region,
city: city, postal: d.postal || 'N/A',
lat: lat, lon: lon, loc: locRaw,
timezone: d.timezone || 'N/A',
isp: provider, asn, timestamp, address, proxy: proxyType,
risk: riskScore,
organisation, continent, connType,
lastSeen
};
if (prevEntry) {
dupBanner.innerHTML = IC.warn() + ' Seen before \u2014 last seen ' + esc(prevEntry.timestamp);
dupBanner.style.display = 'flex';
}
playPing();
pushHistory(currentEntry);
renderHistory();
renderStats();
flagEl.innerHTML = '';
waitingEl.style.display = 'none';
setStatus('ok');
copyBtn.style.display = '';
const mapBtn = document.getElementById('geo-map-btn');
if (lat != null && lon != null) {
mapBtn.href = 'https://maps.google.com/?q=' + lat.toFixed(6) + ',' + lon.toFixed(6);
mapBtn.style.display = 'flex';
} else {
mapBtn.style.display = 'none';
}
const riskColor = riskScore == null ? '#3f3f46' : riskScore >= 76 ? '#ef4444' : riskScore >= 51 ? '#f97316' : riskScore >= 26 ? '#f59e0b' : '#22c55e';
const riskLabel = riskScore != null ? riskScore + '/100' : 'N/A';
const riskClass = riskScore == null ? '' : riskScore >= 51 ? 'risk-high' : riskScore >= 26 ? 'risk-med' : 'risk-low';
const locStr = [city !== 'N/A' ? city : null, region !== 'N/A' ? region : null, d.country ? regionNames.of(d.country) : ''].filter(Boolean).join(', ');
let heroHtml = '<div class="geo-hero">';
heroHtml += '<div class="geo-hero-flag">' + (flag || IC.globe(32)) + '</div>';
heroHtml += '<div class="geo-hero-info">';
heroHtml += '<div class="geo-hero-ip">' + esc(d.ip) + '</div>';
heroHtml += '<div class="geo-hero-loc">' + esc(locStr || 'Unknown location') + '</div>';
heroHtml += '<div class="geo-hero-badges">';
if (isVPN) heroHtml += '<span class="geo-hero-badge vpn">' + esc(proxyType) + '</span>';
else if (proxyType === 'Clean') heroHtml += '<span class="geo-hero-badge clean">Clean</span>';
if (riskScore != null) heroHtml += '<span class="geo-hero-badge ' + riskClass + '">Risk ' + riskScore + '/100</span>';
heroHtml += '</div></div></div>';
heroHtml += '<div id="geo-risk-bar"><div id="geo-risk-fill" style="width:' + (riskScore != null ? riskScore : 0) + '%;background:' + riskColor + '"></div></div>';
buildRows([
['Name (PTR)', name || 'N/A', !!name],
['IP', d.ip, true],
['Proxy / VPN', proxyType, isVPN],
['Risk Score', riskLabel, riskScore != null && riskScore > 50],
['Connection Type', connType, false],
['Last Seen', lastSeen || 'N/A', false],
['Address', address, false],
['Hostname', rdnsHostname, rdnsHostname !== 'N/A'],
['Country', countryLabel, false],
['Region', region, false],
['City', city, false],
['Postal', d.postal || 'N/A', false],
['Continent', continent, false],
['Latitude', latStr, false],
['Longitude', lonStr, false],
['Timezone', d.timezone || 'N/A', false],
['ISP / Provider', provider, false],
['Organisation', organisation, false],
['ASN', asn, false],
], { hero: heroHtml });
if (notifEnabled && typeof Notification !== 'undefined' && Notification.permission === 'granted') {
try {
const _siteLabel = SITE === 'camsurf' ? 'CamSurf' : SITE === 'umingle' ? 'Umingle' : 'OmeTV';
new Notification(_siteLabel + ' - New Connection', {
body: (d.ip || ip) + ' - ' + (city !== 'N/A' ? city : '') + ', ' + (d.country || '') + (isVPN ? ' [' + proxyType + ']' : ''),
icon: 'https://www.google.com/s2/favicons?domain=' + location.hostname
});
} catch (_) {}
}
} catch (err) {
setStatus('error');
flagEl.innerHTML = IC.warn(28);
waitingEl.textContent = 'Error: ' + err.message;
waitingEl.style.display = '';
}
};
fetchOwnInfo();
updateTelemetryButton();
askTelemetryConsentIfNeeded();
pingTelemetryInstallOnce('startup');
pingTelemetrySessionDaily();
pingTelemetryVersion();
if (!loadApiKey()) {
setStatus('error');
flagEl.innerHTML = IC.key(28);
waitingEl.textContent = 'Set your IPInfo API key with the key button to enable lookups.';
waitingEl.style.display = '';
}
onRelayHidden = () => {
if (relayHiddenShown) return;
relayHiddenShown = true;
setStatus('error');
flagEl.innerHTML = IC.lock(28);
waitingEl.textContent = 'Connected - IP hidden by relay';
waitingEl.style.display = '';
};
onIPDetected = (ip) => {
if (ip === ownIP) return;
clearPanelFn();
setStatus('loading');
flagEl.innerHTML = IC.globe(28);
waitingEl.textContent = 'Looking up ' + ip + '...';
waitingEl.style.display = '';
getLocationFn(ip);
};
uiReady = true;
updateSessionBar();
renderHistory();
renderStats();
if (typeof Notification !== 'undefined' && Notification.permission === 'default') {
Notification.requestPermission();
}
pendingIPs.forEach(ip => onIPDetected(ip));
pendingIPs.length = 0;
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initUI);
} else {
initUI();
}
})();