// ==UserScript== // @name X Account Location & Device Info // @namespace http://tampermonkey.net/ // @version 1.2.0 // @description Shows country flag emojis and device/platform emojis next to X usernames with hover tooltips // @author Alexander Hagenah (@xaitax) // @homepage https://github.com/xaitax/x-account-location-device // @supportURL https://primepage.de // @supportURL https://www.linkedin.com/in/alexhagenah/ // @license MIT // @match *://*x.com/* // @match *://*twitter.com/* // @grant unsafeWindow // @run-at document-start // @downloadURL none // ==/UserScript== (function() { 'use strict'; /** * Configuration & Constants */ const CONFIG = { VERSION: '1.2.0', CACHE_KEY: 'x_location_cache_v2', CACHE_EXPIRY: 24 * 60 * 60 * 1000, // 24 hours API: { QUERY_ID: 'XRqGa7EeokUU5kppkh13EA', // AboutAccountQuery MIN_INTERVAL: 2000, MAX_CONCURRENT: 2, RETRY_DELAY: 5000 }, SELECTORS: { USERNAME: '[data-testid="UserName"], [data-testid="User-Name"]', TWEET: 'article[data-testid="tweet"]', USER_CELL: '[data-testid="UserCell"]', LINKS: 'a[href^="/"]' }, STYLES: { SHIMMER_ID: 'x-flag-shimmer-style', FLAG_CLASS: 'x-location-flag', DEVICE_CLASS: 'x-device-indicator' } }; /** * Country & Flag Data * Optimized for O(1) lookup */ const COUNTRY_FLAGS = { "afghanistan": "๐Ÿ‡ฆ๐Ÿ‡ซ", "albania": "๐Ÿ‡ฆ๐Ÿ‡ฑ", "algeria": "๐Ÿ‡ฉ๐Ÿ‡ฟ", "andorra": "๐Ÿ‡ฆ๐Ÿ‡ฉ", "angola": "๐Ÿ‡ฆ๐Ÿ‡ด", "antigua and barbuda": "๐Ÿ‡ฆ๐Ÿ‡ฌ", "argentina": "๐Ÿ‡ฆ๐Ÿ‡ท", "armenia": "๐Ÿ‡ฆ๐Ÿ‡ฒ", "australia": "๐Ÿ‡ฆ๐Ÿ‡บ", "austria": "๐Ÿ‡ฆ๐Ÿ‡น", "azerbaijan": "๐Ÿ‡ฆ๐Ÿ‡ฟ", "bahamas": "๐Ÿ‡ง๐Ÿ‡ธ", "bahrain": "๐Ÿ‡ง๐Ÿ‡ญ", "bangladesh": "๐Ÿ‡ง๐Ÿ‡ฉ", "barbados": "๐Ÿ‡ง๐Ÿ‡ง", "belarus": "๐Ÿ‡ง๐Ÿ‡พ", "belgium": "๐Ÿ‡ง๐Ÿ‡ช", "belize": "๐Ÿ‡ง๐Ÿ‡ฟ", "benin": "๐Ÿ‡ง๐Ÿ‡ฏ", "bhutan": "๐Ÿ‡ง๐Ÿ‡น", "bolivia": "๐Ÿ‡ง๐Ÿ‡ด", "bosnia and herzegovina": "๐Ÿ‡ง๐Ÿ‡ฆ", "bosnia": "๐Ÿ‡ง๐Ÿ‡ฆ", "botswana": "๐Ÿ‡ง๐Ÿ‡ผ", "brazil": "๐Ÿ‡ง๐Ÿ‡ท", "brunei": "๐Ÿ‡ง๐Ÿ‡ณ", "bulgaria": "๐Ÿ‡ง๐Ÿ‡ฌ", "burkina faso": "๐Ÿ‡ง๐Ÿ‡ซ", "burundi": "๐Ÿ‡ง๐Ÿ‡ฎ", "cambodia": "๐Ÿ‡ฐ๐Ÿ‡ญ", "cameroon": "๐Ÿ‡จ๐Ÿ‡ฒ", "canada": "๐Ÿ‡จ๐Ÿ‡ฆ", "cape verde": "๐Ÿ‡จ๐Ÿ‡ป", "central african republic": "๐Ÿ‡จ๐Ÿ‡ซ", "chad": "๐Ÿ‡น๐Ÿ‡ฉ", "chile": "๐Ÿ‡จ๐Ÿ‡ฑ", "china": "๐Ÿ‡จ๐Ÿ‡ณ", "colombia": "๐Ÿ‡จ๐Ÿ‡ด", "comoros": "๐Ÿ‡ฐ๐Ÿ‡ฒ", "congo": "๐Ÿ‡จ๐Ÿ‡ฌ", "costa rica": "๐Ÿ‡จ๐Ÿ‡ท", "croatia": "๐Ÿ‡ญ๐Ÿ‡ท", "cuba": "๐Ÿ‡จ๐Ÿ‡บ", "cyprus": "๐Ÿ‡จ๐Ÿ‡พ", "czech republic": "๐Ÿ‡จ๐Ÿ‡ฟ", "czechia": "๐Ÿ‡จ๐Ÿ‡ฟ", "democratic republic of the congo": "๐Ÿ‡จ๐Ÿ‡ฉ", "denmark": "๐Ÿ‡ฉ๐Ÿ‡ฐ", "djibouti": "๐Ÿ‡ฉ๐Ÿ‡ฏ", "dominica": "๐Ÿ‡ฉ๐Ÿ‡ฒ", "dominican republic": "๐Ÿ‡ฉ๐Ÿ‡ด", "east timor": "๐Ÿ‡น๐Ÿ‡ฑ", "ecuador": "๐Ÿ‡ช๐Ÿ‡จ", "egypt": "๐Ÿ‡ช๐Ÿ‡ฌ", "el salvador": "๐Ÿ‡ธ๐Ÿ‡ป", "england": "๐Ÿด๓ ง๓ ข๓ ฅ๓ ฎ๓ ง๓ ฟ", "equatorial guinea": "๐Ÿ‡ฌ๐Ÿ‡ถ", "eritrea": "๐Ÿ‡ช๐Ÿ‡ท", "estonia": "๐Ÿ‡ช๐Ÿ‡ช", "eswatini": "๐Ÿ‡ธ๐Ÿ‡ฟ", "ethiopia": "๐Ÿ‡ช๐Ÿ‡น", "europe": "๐Ÿ‡ช๐Ÿ‡บ", "european union": "๐Ÿ‡ช๐Ÿ‡บ", "fiji": "๐Ÿ‡ซ๐Ÿ‡ฏ", "finland": "๐Ÿ‡ซ๐Ÿ‡ฎ", "france": "๐Ÿ‡ซ๐Ÿ‡ท", "gabon": "๐Ÿ‡ฌ๐Ÿ‡ฆ", "gambia": "๐Ÿ‡ฌ๐Ÿ‡ฒ", "georgia": "๐Ÿ‡ฌ๐Ÿ‡ช", "germany": "๐Ÿ‡ฉ๐Ÿ‡ช", "ghana": "๐Ÿ‡ฌ๐Ÿ‡ญ", "greece": "๐Ÿ‡ฌ๐Ÿ‡ท", "grenada": "๐Ÿ‡ฌ๐Ÿ‡ฉ", "guatemala": "๐Ÿ‡ฌ๐Ÿ‡น", "guinea": "๐Ÿ‡ฌ๐Ÿ‡ณ", "guinea-bissau": "๐Ÿ‡ฌ๐Ÿ‡ผ", "guyana": "๐Ÿ‡ฌ๐Ÿ‡พ", "haiti": "๐Ÿ‡ญ๐Ÿ‡น", "honduras": "๐Ÿ‡ญ๐Ÿ‡ณ", "hong kong": "๐Ÿ‡ญ๐Ÿ‡ฐ", "hungary": "๐Ÿ‡ญ๐Ÿ‡บ", "iceland": "๐Ÿ‡ฎ๐Ÿ‡ธ", "india": "๐Ÿ‡ฎ๐Ÿ‡ณ", "indonesia": "๐Ÿ‡ฎ๐Ÿ‡ฉ", "iran": "๐Ÿ‡ฎ๐Ÿ‡ท", "iraq": "๐Ÿ‡ฎ๐Ÿ‡ถ", "ireland": "๐Ÿ‡ฎ๐Ÿ‡ช", "israel": "๐Ÿ‡ฎ๐Ÿ‡ฑ", "italy": "๐Ÿ‡ฎ๐Ÿ‡น", "ivory coast": "๐Ÿ‡จ๐Ÿ‡ฎ", "jamaica": "๐Ÿ‡ฏ๐Ÿ‡ฒ", "japan": "๐Ÿ‡ฏ๐Ÿ‡ต", "jordan": "๐Ÿ‡ฏ๐Ÿ‡ด", "kazakhstan": "๐Ÿ‡ฐ๐Ÿ‡ฟ", "kenya": "๐Ÿ‡ฐ๐Ÿ‡ช", "kiribati": "๐Ÿ‡ฐ๐Ÿ‡ฎ", "korea": "๐Ÿ‡ฐ๐Ÿ‡ท", "kosovo": "๐Ÿ‡ฝ๐Ÿ‡ฐ", "kuwait": "๐Ÿ‡ฐ๐Ÿ‡ผ", "kyrgyzstan": "๐Ÿ‡ฐ๐Ÿ‡ฌ", "laos": "๐Ÿ‡ฑ๐Ÿ‡ฆ", "latvia": "๐Ÿ‡ฑ๐Ÿ‡ป", "lebanon": "๐Ÿ‡ฑ๐Ÿ‡ง", "lesotho": "๐Ÿ‡ฑ๐Ÿ‡ธ", "liberia": "๐Ÿ‡ฑ๐Ÿ‡ท", "libya": "๐Ÿ‡ฑ๐Ÿ‡พ", "liechtenstein": "๐Ÿ‡ฑ๐Ÿ‡ฎ", "lithuania": "๐Ÿ‡ฑ๐Ÿ‡น", "luxembourg": "๐Ÿ‡ฑ๐Ÿ‡บ", "macao": "๐Ÿ‡ฒ๐Ÿ‡ด", "macau": "๐Ÿ‡ฒ๐Ÿ‡ด", "madagascar": "๐Ÿ‡ฒ๐Ÿ‡ฌ", "malawi": "๐Ÿ‡ฒ๐Ÿ‡ผ", "malaysia": "๐Ÿ‡ฒ๐Ÿ‡พ", "maldives": "๐Ÿ‡ฒ๐Ÿ‡ป", "mali": "๐Ÿ‡ฒ๐Ÿ‡ฑ", "malta": "๐Ÿ‡ฒ๐Ÿ‡น", "marshall islands": "๐Ÿ‡ฒ๐Ÿ‡ญ", "mauritania": "๐Ÿ‡ฒ๐Ÿ‡ท", "mauritius": "๐Ÿ‡ฒ๐Ÿ‡บ", "mexico": "๐Ÿ‡ฒ๐Ÿ‡ฝ", "micronesia": "๐Ÿ‡ซ๐Ÿ‡ฒ", "moldova": "๐Ÿ‡ฒ๐Ÿ‡ฉ", "monaco": "๐Ÿ‡ฒ๐Ÿ‡จ", "mongolia": "๐Ÿ‡ฒ๐Ÿ‡ณ", "montenegro": "๐Ÿ‡ฒ๐Ÿ‡ช", "morocco": "๐Ÿ‡ฒ๐Ÿ‡ฆ", "mozambique": "๐Ÿ‡ฒ๐Ÿ‡ฟ", "myanmar": "๐Ÿ‡ฒ๐Ÿ‡ฒ", "burma": "๐Ÿ‡ฒ๐Ÿ‡ฒ", "namibia": "๐Ÿ‡ณ๐Ÿ‡ฆ", "nauru": "๐Ÿ‡ณ๐Ÿ‡ท", "nepal": "๐Ÿ‡ณ๐Ÿ‡ต", "netherlands": "๐Ÿ‡ณ๐Ÿ‡ฑ", "new zealand": "๐Ÿ‡ณ๐Ÿ‡ฟ", "nicaragua": "๐Ÿ‡ณ๐Ÿ‡ฎ", "niger": "๐Ÿ‡ณ๐Ÿ‡ช", "nigeria": "๐Ÿ‡ณ๐Ÿ‡ฌ", "north korea": "๐Ÿ‡ฐ๐Ÿ‡ต", "north macedonia": "๐Ÿ‡ฒ๐Ÿ‡ฐ", "macedonia": "๐Ÿ‡ฒ๐Ÿ‡ฐ", "norway": "๐Ÿ‡ณ๐Ÿ‡ด", "oman": "๐Ÿ‡ด๐Ÿ‡ฒ", "pakistan": "๐Ÿ‡ต๐Ÿ‡ฐ", "palau": "๐Ÿ‡ต๐Ÿ‡ผ", "palestine": "๐Ÿ‡ต๐Ÿ‡ธ", "panama": "๐Ÿ‡ต๐Ÿ‡ฆ", "papua new guinea": "๐Ÿ‡ต๐Ÿ‡ฌ", "paraguay": "๐Ÿ‡ต๐Ÿ‡พ", "peru": "๐Ÿ‡ต๐Ÿ‡ช", "philippines": "๐Ÿ‡ต๐Ÿ‡ญ", "poland": "๐Ÿ‡ต๐Ÿ‡ฑ", "portugal": "๐Ÿ‡ต๐Ÿ‡น", "puerto rico": "๐Ÿ‡ต๐Ÿ‡ท", "qatar": "๐Ÿ‡ถ๐Ÿ‡ฆ", "romania": "๐Ÿ‡ท๐Ÿ‡ด", "russia": "๐Ÿ‡ท๐Ÿ‡บ", "russian federation": "๐Ÿ‡ท๐Ÿ‡บ", "rwanda": "๐Ÿ‡ท๐Ÿ‡ผ", "saint kitts and nevis": "๐Ÿ‡ฐ๐Ÿ‡ณ", "saint lucia": "๐Ÿ‡ฑ๐Ÿ‡จ", "saint vincent and the grenadines": "๐Ÿ‡ป๐Ÿ‡จ", "samoa": "๐Ÿ‡ผ๐Ÿ‡ธ", "san marino": "๐Ÿ‡ธ๐Ÿ‡ฒ", "sao tome and principe": "๐Ÿ‡ธ๐Ÿ‡น", "saudi arabia": "๐Ÿ‡ธ๐Ÿ‡ฆ", "scotland": "๐Ÿด๓ ง๓ ข๓ ณ๓ ฃ๓ ด๓ ฟ", "senegal": "๐Ÿ‡ธ๐Ÿ‡ณ", "serbia": "๐Ÿ‡ท๐Ÿ‡ธ", "seychelles": "๐Ÿ‡ธ๐Ÿ‡จ", "sierra leone": "๐Ÿ‡ธ๐Ÿ‡ฑ", "singapore": "๐Ÿ‡ธ๐Ÿ‡ฌ", "slovakia": "๐Ÿ‡ธ๐Ÿ‡ฐ", "slovenia": "๐Ÿ‡ธ๐Ÿ‡ฎ", "solomon islands": "๐Ÿ‡ธ๐Ÿ‡ง", "somalia": "๐Ÿ‡ธ๐Ÿ‡ด", "south africa": "๐Ÿ‡ฟ๐Ÿ‡ฆ", "south korea": "๐Ÿ‡ฐ๐Ÿ‡ท", "south sudan": "๐Ÿ‡ธ๐Ÿ‡ธ", "spain": "๐Ÿ‡ช๐Ÿ‡ธ", "sri lanka": "๐Ÿ‡ฑ๐Ÿ‡ฐ", "sudan": "๐Ÿ‡ธ๐Ÿ‡ฉ", "suriname": "๐Ÿ‡ธ๐Ÿ‡ท", "sweden": "๐Ÿ‡ธ๐Ÿ‡ช", "switzerland": "๐Ÿ‡จ๐Ÿ‡ญ", "syria": "๐Ÿ‡ธ๐Ÿ‡พ", "taiwan": "๐Ÿ‡น๐Ÿ‡ผ", "tajikistan": "๐Ÿ‡น๐Ÿ‡ฏ", "tanzania": "๐Ÿ‡น๐Ÿ‡ฟ", "thailand": "๐Ÿ‡น๐Ÿ‡ญ", "timor-leste": "๐Ÿ‡น๐Ÿ‡ฑ", "togo": "๐Ÿ‡น๐Ÿ‡ฌ", "tonga": "๐Ÿ‡น๐Ÿ‡ด", "trinidad and tobago": "๐Ÿ‡น๐Ÿ‡น", "tunisia": "๐Ÿ‡น๐Ÿ‡ณ", "turkey": "๐Ÿ‡น๐Ÿ‡ท", "tรผrkiye": "๐Ÿ‡น๐Ÿ‡ท", "turkmenistan": "๐Ÿ‡น๐Ÿ‡ฒ", "tuvalu": "๐Ÿ‡น๐Ÿ‡ป", "uganda": "๐Ÿ‡บ๐Ÿ‡ฌ", "ukraine": "๐Ÿ‡บ๐Ÿ‡ฆ", "united arab emirates": "๐Ÿ‡ฆ๐Ÿ‡ช", "uae": "๐Ÿ‡ฆ๐Ÿ‡ช", "united kingdom": "๐Ÿ‡ฌ๐Ÿ‡ง", "uk": "๐Ÿ‡ฌ๐Ÿ‡ง", "great britain": "๐Ÿ‡ฌ๐Ÿ‡ง", "britain": "๐Ÿ‡ฌ๐Ÿ‡ง", "united states": "๐Ÿ‡บ๐Ÿ‡ธ", "usa": "๐Ÿ‡บ๐Ÿ‡ธ", "us": "๐Ÿ‡บ๐Ÿ‡ธ", "uruguay": "๐Ÿ‡บ๐Ÿ‡พ", "uzbekistan": "๐Ÿ‡บ๐Ÿ‡ฟ", "vanuatu": "๐Ÿ‡ป๐Ÿ‡บ", "vatican city": "๐Ÿ‡ป๐Ÿ‡ฆ", "venezuela": "๐Ÿ‡ป๐Ÿ‡ช", "vietnam": "๐Ÿ‡ป๐Ÿ‡ณ", "wales": "๐Ÿด๓ ง๓ ข๓ ท๓ ฌ๓ ณ๓ ฟ", "yemen": "๐Ÿ‡พ๐Ÿ‡ช", "zambia": "๐Ÿ‡ฟ๐Ÿ‡ฒ", "zimbabwe": "๐Ÿ‡ฟ๐Ÿ‡ผ" }; /** * Core Application Class */ class XLocationPatcher { constructor() { this.cache = new Map(); this.requestQueue = []; this.activeRequests = 0; this.lastRequestTime = 0; this.rateLimitReset = 0; this.headers = null; this.processingSet = new Set(); this.observer = null; this.isEnabled = true; this.init(); } init() { console.log(`๐Ÿš€ X Account Location v${CONFIG.VERSION} initializing...`); this.loadSettings(); this.loadCache(); this.setupInterceptors(); this.exposeAPI(); // Inject styles and start observing when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { this.injectStyles(); this.startObserver(); }); } else { this.injectStyles(); this.startObserver(); } // Periodic cache save setInterval(() => this.saveCache(), 30000); } /** * Network Interception & Header Capture */ setupInterceptors() { const self = this; // Intercept Fetch const originalFetch = window.fetch; window.fetch = function(url, options) { if (typeof url === 'string' && url.includes('x.com/i/api/graphql') && options?.headers) { self.captureHeaders(options.headers); } return originalFetch.apply(this, arguments); }; // Intercept XHR const originalOpen = XMLHttpRequest.prototype.open; const originalSend = XMLHttpRequest.prototype.send; const originalSetHeader = XMLHttpRequest.prototype.setRequestHeader; XMLHttpRequest.prototype.open = function(method, url) { this._url = url; return originalOpen.apply(this, arguments); }; XMLHttpRequest.prototype.setRequestHeader = function(header, value) { if (!this._headers) this._headers = {}; this._headers[header] = value; return originalSetHeader.apply(this, arguments); }; XMLHttpRequest.prototype.send = function() { if (this._url?.includes('x.com/i/api/graphql') && this._headers) { self.captureHeaders(this._headers); } return originalSend.apply(this, arguments); }; } captureHeaders(headers) { if (this.headers) return; // Already captured const headerObj = headers instanceof Headers ? Object.fromEntries(headers.entries()) : headers; // Validate we have auth headers if (headerObj.authorization || headerObj['authorization']) { this.headers = headerObj; console.log('โœ… X API Headers captured successfully'); } } getFallbackHeaders() { const cookies = document.cookie.split('; ').reduce((acc, cookie) => { const [key, value] = cookie.split('='); acc[key] = value; return acc; }, {}); if (!cookies.ct0) return null; return { 'authorization': 'Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA', 'x-csrf-token': cookies.ct0, 'x-twitter-active-user': 'yes', 'x-twitter-auth-type': 'OAuthSession', 'content-type': 'application/json' }; } /** * Data Management */ loadSettings() { try { const stored = localStorage.getItem('x_location_enabled'); this.isEnabled = stored !== null ? JSON.parse(stored) : true; } catch (e) { console.error('Failed to load settings', e); } } loadCache() { try { const raw = localStorage.getItem(CONFIG.CACHE_KEY); if (!raw) return; const parsed = JSON.parse(raw); const now = Date.now(); let count = 0; Object.entries(parsed).forEach(([key, data]) => { if (data.expiry > now) { this.cache.set(key, data.value); count++; } }); console.log(`๐Ÿ“ฆ Loaded ${count} cached entries`); } catch (e) { console.error('Cache load failed', e); localStorage.removeItem(CONFIG.CACHE_KEY); } } saveCache() { try { const now = Date.now(); const expiry = now + CONFIG.CACHE_EXPIRY; const exportData = {}; this.cache.forEach((value, key) => { exportData[key] = { value, expiry }; }); localStorage.setItem(CONFIG.CACHE_KEY, JSON.stringify(exportData)); } catch (e) { console.error('Cache save failed', e); } } /** * API Interaction */ async fetchUserInfo(screenName) { // Check cache first if (this.cache.has(screenName)) { return this.cache.get(screenName); } // Queue request return new Promise((resolve, reject) => { this.requestQueue.push({ screenName, resolve, reject }); this.processQueue(); }); } async processQueue() { if (this.activeRequests >= CONFIG.API.MAX_CONCURRENT || this.requestQueue.length === 0) return; // Rate limit check const now = Date.now(); if (this.rateLimitReset > now) { const wait = this.rateLimitReset - now; setTimeout(() => this.processQueue(), Math.min(wait, 60000)); return; } const timeSinceLast = now - this.lastRequestTime; if (timeSinceLast < CONFIG.API.MIN_INTERVAL) { setTimeout(() => this.processQueue(), CONFIG.API.MIN_INTERVAL - timeSinceLast); return; } // Execute request const request = this.requestQueue.shift(); this.activeRequests++; this.lastRequestTime = Date.now(); try { const result = await this.executeApiCall(request.screenName); this.cache.set(request.screenName, result); request.resolve(result); } catch (error) { request.reject(error); } finally { this.activeRequests--; this.processQueue(); } } async executeApiCall(screenName) { let headers = this.headers; if (!headers) { // Try fallback headers = this.getFallbackHeaders(); if (!headers) { // Wait for headers await new Promise(r => setTimeout(r, 2000)); headers = this.headers || this.getFallbackHeaders(); if (!headers) throw new Error('No API headers captured'); } else { console.log('โš ๏ธ Using fallback headers'); } } const variables = encodeURIComponent(JSON.stringify({ screenName })); const url = `https://x.com/i/api/graphql/${CONFIG.API.QUERY_ID}/AboutAccountQuery?variables=${variables}`; const requestHeaders = { ...headers }; // Force English for consistent country names requestHeaders['accept-language'] = 'en-US,en;q=0.9'; const response = await fetch(url, { headers: requestHeaders, method: 'GET', mode: 'cors', credentials: 'include' }); if (!response.ok) { if (response.status === 429) { const reset = response.headers.get('x-rate-limit-reset'); this.rateLimitReset = reset ? parseInt(reset) * 1000 : Date.now() + 60000; throw new Error('Rate limited'); } throw new Error(`API Error: ${response.status}`); } const data = await response.json(); const profile = data?.data?.user_result_by_screen_name?.result?.about_profile; return { location: profile?.account_based_in || null, device: profile?.source || null }; } /** * UI & DOM Manipulation */ injectStyles() { if (document.getElementById(CONFIG.STYLES.SHIMMER_ID)) return; const style = document.createElement('style'); style.id = CONFIG.STYLES.SHIMMER_ID; style.textContent = ` @keyframes x-shimmer { 0% { background-position: -200% 0; } 100% { background-position: 200% 0; } } .x-flag-shimmer { display: inline-block; width: 20px; height: 16px; margin: 0 4px; vertical-align: middle; border-radius: 2px; background: linear-gradient(90deg, rgba(113,118,123,0.2) 25%, rgba(113,118,123,0.4) 50%, rgba(113,118,123,0.2) 75%); background-size: 200% 100%; animation: x-shimmer 1.5s infinite; } .x-info-badge { margin: 0 4px; display: inline-flex; align-items: center; vertical-align: middle; gap: 4px; font-family: "Twemoji Mozilla", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji", "EmojiOne Color", "Android Emoji", sans-serif; font-size: 14px; opacity: 0.8; transition: all 0.2s; cursor: help; } .x-info-badge:hover { opacity: 1; transform: scale(1.1); } `; document.head.appendChild(style); } getFlagEmoji(countryName) { if (!countryName) return null; return COUNTRY_FLAGS[countryName.trim().toLowerCase()] || '๐ŸŒ'; } getDeviceEmoji(deviceString) { if (!deviceString) return null; const d = deviceString.toLowerCase(); if (d.includes('android') || d.includes('iphone') || d.includes('mobile')) return '๐Ÿ“ฑ'; if (d.includes('mac') || d.includes('linux') || d.includes('windows')) return '๐Ÿ’ป'; if (d.includes('web')) return '๐ŸŒ'; return '๐Ÿ“ฑ'; } async processElement(element) { if (element.dataset.xProcessed) return; const screenName = this.extractUsername(element); if (!screenName || this.processingSet.has(screenName)) return; element.dataset.xProcessed = 'true'; this.processingSet.add(screenName); // Insert shimmer const shimmer = document.createElement('span'); shimmer.className = 'x-flag-shimmer'; const insertionPoint = this.findInsertionPoint(element, screenName); if (insertionPoint) insertionPoint.target.insertBefore(shimmer, insertionPoint.ref); try { const info = await this.fetchUserInfo(screenName); shimmer.remove(); if (info.location || info.device) { const badge = document.createElement('span'); badge.className = 'x-info-badge'; let content = ''; if (info.location) { const flag = this.getFlagEmoji(info.location); if (flag) content += `${flag}`; } // Fallback device detection if API returns null (common for some accounts) let device = info.device; if (!device) { const ua = navigator.userAgent.toLowerCase(); if (ua.includes('android')) device = 'Android'; else if (ua.includes('iphone')) device = 'iOS'; else if (ua.includes('windows')) device = 'Windows'; else device = 'Web'; } if (device) { const emoji = this.getDeviceEmoji(device); content += `${emoji}`; } badge.innerHTML = content; // Re-find insertion point as DOM might have changed const finalPoint = this.findInsertionPoint(element, screenName); if (finalPoint) finalPoint.target.insertBefore(badge, finalPoint.ref); } } catch (e) { console.debug(`Failed to process ${screenName}`, e); shimmer.remove(); } finally { this.processingSet.delete(screenName); } } extractUsername(element) { // Try to find the username link const link = element.querySelector('a[href^="/"]'); if (!link) return null; const href = link.getAttribute('href'); const match = href.match(/^\/([^/]+)$/); if (!match) return null; const username = match[1]; const invalid = ['home', 'explore', 'notifications', 'messages', 'search', 'settings']; if (invalid.includes(username)) return null; return username; } findInsertionPoint(container, screenName) { // Look for the handle (@username) const links = Array.from(container.querySelectorAll('a')); const handleLink = links.find(l => l.textContent.trim().toLowerCase() === `@${screenName.toLowerCase()}`); if (handleLink) { // Insert after the handle return { target: handleLink.parentNode.parentNode, ref: handleLink.parentNode.nextSibling }; } // Fallback: Try to find the name container const nameLink = container.querySelector(`a[href="/${screenName}"]`); if (nameLink) { return { target: nameLink.parentNode, ref: nameLink.nextSibling }; } return null; } startObserver() { this.observer = new MutationObserver((mutations) => { if (!this.isEnabled) return; let shouldProcess = false; for (const m of mutations) { if (m.addedNodes.length) { shouldProcess = true; break; } } if (shouldProcess) { this.scanPage(); } }); this.observer.observe(document.body, { childList: true, subtree: true }); this.scanPage(); // Initial scan } scanPage() { const elements = document.querySelectorAll(CONFIG.SELECTORS.USERNAME); elements.forEach(el => this.processElement(el)); } /** * Public API */ getCacheInfo() { const entries = Array.from(this.cache.entries()).map(([key, value]) => ({ key, value })); return { size: this.cache.size, entries }; } exposeAPI() { const api = { clearCache: () => { this.cache.clear(); localStorage.removeItem(CONFIG.CACHE_KEY); console.log('๐Ÿงน Cache cleared'); }, getCacheInfo: () => { const info = this.getCacheInfo(); console.log('Cache info:', info); return info; }, toggle: () => { this.isEnabled = !this.isEnabled; localStorage.setItem('x_location_enabled', this.isEnabled); console.log(`Extension ${this.isEnabled ? 'enabled' : 'disabled'}`); }, debug: () => { console.log('Cache Size:', this.cache.size); console.log('Queue Length:', this.requestQueue.length); console.log('Active Requests:', this.activeRequests); } }; if (typeof cloneInto === 'function') { unsafeWindow.XFlagScript = cloneInto(api, unsafeWindow, { cloneFunctions: true }); } else { unsafeWindow.XFlagScript = api; } } } // Instantiate new XLocationPatcher(); })();