// ==UserScript== // @name X-Posed: Account Location & Device Info // @namespace http://tampermonkey.net/ // @version 1.5.1 // @description See where X users are located and what devices they use. Country flags & device icons next to every username. Optional geo-blocking. // @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 https://update.greasyfork.icu/scripts/556794/X-Posed%3A%20Account%20Location%20%20Device%20Info.user.js // @updateURL https://update.greasyfork.icu/scripts/556794/X-Posed%3A%20Account%20Location%20%20Device%20Info.meta.js // ==/UserScript== (function() { 'use strict'; /** * Configuration & Constants */ const CONFIG = { VERSION: '1.5.1', CACHE_KEY: 'x_location_cache_v3', // v3 includes locationAccurate field BLOCKED_COUNTRIES_KEY: 'x_blocked_countries', CACHE_EXPIRY: 48 * 60 * 60 * 1000, // 48 hours (extended from 24) 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.fetchPromises = new Map(); // Track active promises this.observer = null; this.isEnabled = true; this.blockedCountries = new Set(); this.init(); } init() { console.log(`๐Ÿš€ X Account Location v${CONFIG.VERSION} initializing...`); console.log(`๐Ÿ“ฆ Cache expiry: ${CONFIG.CACHE_EXPIRY / 1000 / 60 / 60} hours`); this.loadSettings(); this.loadCache(); this.loadBlockedCountries(); this.setupInterceptors(); this.exposeAPI(); // Inject styles and start observing when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { this.injectStyles(); this.injectSidebarLink(); this.startObserver(); }); } else { this.injectStyles(); this.injectSidebarLink(); 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); } } loadBlockedCountries() { try { const stored = localStorage.getItem(CONFIG.BLOCKED_COUNTRIES_KEY); if (stored) { this.blockedCountries = new Set(JSON.parse(stored)); console.log(`๐Ÿšซ Loaded ${this.blockedCountries.size} blocked countries`); } } catch (e) { console.error('Failed to load blocked countries', e); this.blockedCountries = new Set(); } } saveBlockedCountries() { try { const array = Array.from(this.blockedCountries); localStorage.setItem(CONFIG.BLOCKED_COUNTRIES_KEY, JSON.stringify(array)); console.log(`๐Ÿ’พ Saved ${array.length} blocked countries`); } catch (e) { console.error('Failed to save blocked countries', 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) { // 1. Check cache if (this.cache.has(screenName)) { return this.cache.get(screenName); } // 2. Check active promises (deduplication) if (this.fetchPromises.has(screenName)) { return this.fetchPromises.get(screenName); } // 3. Create new promise and queue request const promise = new Promise((resolve, reject) => { this.requestQueue.push({ screenName, resolve, reject }); this.processQueue(); }).then(result => { this.fetchPromises.delete(screenName); return result; }).catch(error => { this.fetchPromises.delete(screenName); throw error; }); this.fetchPromises.set(screenName, promise); return promise; } 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 { console.debug(`๐Ÿ“ก API Request for: ${request.screenName}`); const result = await this.executeApiCall(request.screenName); this.cache.set(request.screenName, result); request.resolve(result); } catch (error) { console.warn(`โŒ API Error for ${request.screenName}:`, error.message); 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; const waitMinutes = Math.ceil((this.rateLimitReset - Date.now()) / 60000); console.warn(`โš ๏ธ X API rate limit reached. Waiting ${waitMinutes} minute(s) before retrying...`); 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, locationAccurate: profile?.location_accurate !== false // Default to true if not present }; } /** * 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 0 8px; 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); } /* Country Blocker Modal Styles */ .x-blocker-modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.4); z-index: 999999; display: flex; align-items: center; justify-content: center; } .x-blocker-modal { background: rgb(0, 0, 0); border-radius: 16px; max-width: 600px; width: 90%; max-height: 90vh; overflow: hidden; display: flex; flex-direction: column; box-shadow: 0 0 40px rgba(255,255,255,0.1); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; } .x-blocker-header { padding: 16px 20px; border-bottom: 1px solid rgb(47, 51, 54); display: flex; align-items: center; justify-content: space-between; } .x-blocker-title { font-size: 20px; font-weight: 700; color: rgb(231, 233, 234); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; } .x-blocker-close { background: none; border: none; color: rgb(231, 233, 234); cursor: pointer; padding: 8px; border-radius: 50%; display: flex; align-items: center; justify-content: center; transition: background 0.2s; } .x-blocker-close:hover { background: rgba(231, 233, 234, 0.1); } .x-blocker-body { padding: 20px; overflow-y: auto; flex: 1; } .x-blocker-info { color: rgb(113, 118, 123); font-size: 14px; margin-bottom: 16px; line-height: 1.5; } .x-blocker-search { width: 100%; padding: 12px 16px; border-radius: 24px; background: rgb(32, 35, 39); border: 1px solid rgb(47, 51, 54); color: rgb(231, 233, 234); font-size: 15px; margin-bottom: 16px; outline: none; box-sizing: border-box; } .x-blocker-search:focus { border-color: rgb(29, 155, 240); } .x-blocker-countries { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 8px; } .x-country-item { padding: 12px 16px; border-radius: 8px; background: rgb(22, 24, 28); border: 1px solid rgb(47, 51, 54); cursor: pointer; display: flex; align-items: center; gap: 12px; transition: all 0.2s; } .x-country-item:hover { background: rgb(32, 35, 39); border-color: rgb(113, 118, 123); } .x-country-item.blocked { background: rgba(244, 33, 46, 0.1); border-color: rgb(244, 33, 46); } .x-country-flag { font-size: 24px; line-height: 1; font-family: "Twemoji Mozilla", "Apple Color Emoji", "Segoe UI Emoji", sans-serif; } .x-country-name { flex: 1; color: rgb(231, 233, 234); font-size: 15px; } .x-country-status { font-size: 12px; color: rgb(244, 33, 46); font-weight: 600; } .x-blocker-footer { padding: 16px 20px; border-top: 1px solid rgb(47, 51, 54); display: flex; gap: 12px; justify-content: space-between; align-items: center; } .x-blocker-stats { color: rgb(113, 118, 123); font-size: 14px; } .x-blocker-btn { padding: 10px 20px; border-radius: 24px; font-size: 15px; font-weight: 600; cursor: pointer; transition: all 0.2s; border: none; } .x-blocker-btn-primary { background: rgb(29, 155, 240); color: white; } .x-blocker-btn-primary:hover { background: rgb(26, 140, 216); } .x-blocker-btn-secondary { background: transparent; color: rgb(239, 243, 244); border: 1px solid rgb(83, 100, 113); } .x-blocker-btn-secondary:hover { background: rgba(239, 243, 244, 0.1); } .x-tweet-blocked { display: none !important; } `; document.head.appendChild(style); } getFlagEmoji(countryName) { if (!countryName) return null; const emoji = COUNTRY_FLAGS[countryName.trim().toLowerCase()] || '๐ŸŒ'; // Check if we are on Windows (which doesn't support flag emojis) const isWindows = navigator.platform.indexOf('Win') > -1; if (isWindows && emoji !== '๐ŸŒ') { // Convert emoji to Twemoji URL const codePoints = Array.from(emoji) .map(c => c.codePointAt(0).toString(16)) .join('-'); return `${emoji}`; } return emoji; } getDeviceEmoji(deviceString) { if (!deviceString) return null; const d = deviceString.toLowerCase(); // App stores are always mobile if (d.includes('app store')) return '๐Ÿ“ฑ'; // Explicit mobile devices if (d.includes('android') || d.includes('iphone') || d.includes('mobile')) return '๐Ÿ“ฑ'; // Tablets treated as computers if (d.includes('ipad')) return '๐Ÿ’ป'; // Desktop OS if (d.includes('mac') || d.includes('linux') || d.includes('windows')) return '๐Ÿ’ป'; // Web clients if (d.includes('web')) return '๐ŸŒ'; // Unknown = assume mobile (more common than desktop for unknown strings) return '๐Ÿ“ฑ'; } async processElement(element) { // Skip if already processed if (element.dataset.xProcessed) return; const screenName = this.extractUsername(element); if (!screenName) return; // Mark as processed immediately to prevent duplicates element.dataset.xProcessed = 'true'; // Store username for later reference element.dataset.xScreenName = screenName; try { const info = await this.fetchUserInfo(screenName); // Check if country is blocked FIRST before adding any UI if (info && info.location) { const countryLower = info.location.trim().toLowerCase(); if (this.blockedCountries.has(countryLower)) { this.hideTweet(element); return; // Exit early - don't add any badges/shimmers } } // Only add UI elements if NOT blocked const shimmer = document.createElement('span'); shimmer.className = 'x-flag-shimmer'; const insertionPoint = this.findInsertionPoint(element, screenName); if (insertionPoint) insertionPoint.target.insertBefore(shimmer, insertionPoint.ref); // Small delay for shimmer effect await new Promise(resolve => setTimeout(resolve, 100)); shimmer.remove(); if (info && (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}`; // Add VPN/Proxy indicator if location is not accurate if (info.locationAccurate === false) { content += `๐Ÿ”’`; } } const device = info.device; 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); } } hideTweet(element) { // Find the tweet article container const tweet = element.closest('article[data-testid="tweet"]'); if (tweet) { tweet.classList.add('x-tweet-blocked'); } } extractUsername(element) { // 1. Try to find the username link (Timeline/Feed) const link = element.querySelector('a[href^="/"]'); if (link) { const href = link.getAttribute('href'); const match = href.match(/^\/([^/]+)$/); if (match) { const username = match[1]; const invalid = ['home', 'explore', 'notifications', 'messages', 'search', 'settings']; if (!invalid.includes(username)) return username; } } // 2. Profile Header Case (Username is text, not a link) // Look for text starting with @ const textNodes = Array.from(element.querySelectorAll('span, div[dir="ltr"]')); for (const node of textNodes) { const text = node.textContent.trim(); if (text.startsWith('@') && text.length > 1) { const username = text.substring(1); // Basic validation to ensure it's a username and not just random text if (/^[a-zA-Z0-9_]+$/.test(username)) { return username; } } } return null; } findInsertionPoint(container, screenName) { // 1. Profile Header Specific Logic // The profile header has a specific structure where the name and handle are in separate rows // We want to target the first row (Display Name) // Check if this is likely a profile header (no timestamp link, large text) const isProfileHeader = !container.querySelector('time') && container.querySelector('[data-testid="userFollowIndicator"]') !== null || (container.getAttribute('data-testid') === 'UserName' && container.className.includes('r-14gqq1x')); if (isProfileHeader) { // Find the display name container (first div[dir="ltr"]) const nameContainer = container.querySelector('div[dir="ltr"]'); if (nameContainer) { // We want to append to this container, so the flag sits inline with the name/badge // But we need to be careful not to break the flex layout if it exists // The name container usually has spans inside. We want to insert after the last span. const lastSpan = nameContainer.querySelector('span:last-child'); if (lastSpan) { return { target: lastSpan.parentNode, ref: null }; // Append to end of name container } return { target: nameContainer, ref: null }; } } // 2. Timeline/Feed Case // 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 }; } // 3. Fallback: Try to find the name container via href 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; for (const m of mutations) { m.addedNodes.forEach(node => { if (node.nodeType === 1) { // Element // Check if the node itself is a username if (node.matches && node.matches(CONFIG.SELECTORS.USERNAME)) { this.processElement(node); } // Check descendants const elements = node.querySelectorAll(CONFIG.SELECTORS.USERNAME); elements.forEach(el => this.processElement(el)); } }); } }); 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)); } /** * Sidebar & Modal UI */ injectSidebarLink() { // Wait for sidebar to load const checkSidebar = setInterval(() => { // Try multiple selectors to be language-agnostic let nav = document.querySelector('nav[aria-label="Primary"]'); // English // Fallback: look for nav with role="navigation" that contains profile link if (!nav) { const allNavs = document.querySelectorAll('nav[role="navigation"]'); for (const n of allNavs) { if (n.querySelector('[data-testid="AppTabBar_Profile_Link"]')) { nav = n; break; } } } // Additional fallback: look for header > div > nav structure if (!nav) { const headers = document.querySelectorAll('header'); for (const header of headers) { const n = header.querySelector('nav'); if (n && n.querySelector('[data-testid="AppTabBar_Profile_Link"]')) { nav = n; break; } } } if (nav) { clearInterval(checkSidebar); console.log('โœ… Sidebar navigation found, adding Block Countries link'); this.addBlockerLink(nav); } else { console.debug('โณ Waiting for sidebar navigation...'); } }, 500); // Stop after 10 seconds if not found setTimeout(() => { clearInterval(checkSidebar); console.warn('โš ๏ธ Sidebar navigation not found after 10 seconds'); }, 10000); } addBlockerLink(nav) { // Check if already added if (document.getElementById('x-country-blocker-link')) return; // Find the Profile link to insert after it const profileLink = nav.querySelector('[data-testid="AppTabBar_Profile_Link"]'); if (!profileLink) return; const link = document.createElement('a'); link.id = 'x-country-blocker-link'; link.href = '#'; link.setAttribute('role', 'link'); link.className = profileLink.className; link.setAttribute('aria-label', 'Block Countries'); link.innerHTML = `
Block Countries
`; link.addEventListener('click', (e) => { e.preventDefault(); this.showBlockerModal(); }); // Insert after profile link profileLink.parentElement.insertBefore(link, profileLink.nextSibling); } showBlockerModal() { // Create overlay const overlay = document.createElement('div'); overlay.className = 'x-blocker-modal-overlay'; // Create modal const modal = document.createElement('div'); modal.className = 'x-blocker-modal'; // Create header const header = document.createElement('div'); header.className = 'x-blocker-header'; header.innerHTML = `

Block Countries

`; // Create body const body = document.createElement('div'); body.className = 'x-blocker-body'; const info = document.createElement('div'); info.className = 'x-blocker-info'; info.textContent = 'Select countries to block. Tweets from users in these countries will be hidden from your feed.'; const search = document.createElement('input'); search.type = 'text'; search.className = 'x-blocker-search'; search.placeholder = 'Search countries...'; const countriesContainer = document.createElement('div'); countriesContainer.className = 'x-blocker-countries'; body.appendChild(info); body.appendChild(search); body.appendChild(countriesContainer); // Create footer const footer = document.createElement('div'); footer.className = 'x-blocker-footer'; const stats = document.createElement('div'); stats.className = 'x-blocker-stats'; const updateStats = () => { stats.textContent = `${this.blockedCountries.size} countries blocked`; }; updateStats(); const btnContainer = document.createElement('div'); btnContainer.style.display = 'flex'; btnContainer.style.gap = '12px'; const clearBtn = document.createElement('button'); clearBtn.className = 'x-blocker-btn x-blocker-btn-secondary'; clearBtn.textContent = 'Clear All'; clearBtn.addEventListener('click', () => { this.blockedCountries.clear(); this.saveBlockedCountries(); renderCountries(); updateStats(); // Smart clear: only update already-processed tweets (no new API calls) document.querySelectorAll('[data-x-processed][data-x-screen-name]').forEach(el => { const screenName = el.dataset.xScreenName; const cachedInfo = this.cache.get(screenName); if (!cachedInfo) return; const tweet = el.closest('article[data-testid="tweet"]'); // Unhide tweet if it was blocked if (tweet && tweet.classList.contains('x-tweet-blocked')) { tweet.classList.remove('x-tweet-blocked'); } // Re-add badge if it's missing and user has location/device if (!el.querySelector('.x-info-badge') && (cachedInfo.location || cachedInfo.device)) { const badge = document.createElement('span'); badge.className = 'x-info-badge'; let content = ''; if (cachedInfo.location) { const flag = this.getFlagEmoji(cachedInfo.location); if (flag) content += `${flag}`; // Add VPN/Proxy indicator if location is not accurate if (cachedInfo.locationAccurate === false) { content += `๐Ÿ”’`; } } if (cachedInfo.device) { const emoji = this.getDeviceEmoji(cachedInfo.device); content += `${emoji}`; } badge.innerHTML = content; const insertionPoint = this.findInsertionPoint(el, screenName); if (insertionPoint) insertionPoint.target.insertBefore(badge, insertionPoint.ref); } }); }); const doneBtn = document.createElement('button'); doneBtn.className = 'x-blocker-btn x-blocker-btn-primary'; doneBtn.textContent = 'Done'; doneBtn.addEventListener('click', () => { overlay.remove(); }); btnContainer.appendChild(clearBtn); btnContainer.appendChild(doneBtn); footer.appendChild(stats); footer.appendChild(btnContainer); // Assemble modal modal.appendChild(header); modal.appendChild(body); modal.appendChild(footer); overlay.appendChild(modal); // Render countries list const renderCountries = (filter = '') => { countriesContainer.innerHTML = ''; const countries = Object.keys(COUNTRY_FLAGS) .filter(country => country.includes(filter.toLowerCase())) .sort(); countries.forEach(country => { const item = document.createElement('div'); item.className = 'x-country-item'; const isBlocked = this.blockedCountries.has(country); if (isBlocked) item.classList.add('blocked'); const flag = this.getFlagEmoji(country); const flagSpan = document.createElement('span'); flagSpan.className = 'x-country-flag'; if (typeof flag === 'string' && flag.startsWith(' word.charAt(0).toUpperCase() + word.slice(1) ).join(' '); const status = document.createElement('span'); status.className = 'x-country-status'; status.textContent = isBlocked ? 'BLOCKED' : ''; item.appendChild(flagSpan); item.appendChild(name); item.appendChild(status); item.addEventListener('click', () => { const wasBlocked = this.blockedCountries.has(country); if (wasBlocked) { this.blockedCountries.delete(country); } else { this.blockedCountries.add(country); } this.saveBlockedCountries(); renderCountries(filter); updateStats(); // Smart update: only process cached tweets (NO API CALLS) document.querySelectorAll('[data-x-processed][data-x-screen-name]').forEach(el => { const screenName = el.dataset.xScreenName; const cachedInfo = this.cache.get(screenName); if (!cachedInfo || !cachedInfo.location) return; const countryLower = cachedInfo.location.trim().toLowerCase(); const tweet = el.closest('article[data-testid="tweet"]'); if (countryLower === country) { // This tweet's country was toggled if (wasBlocked) { // Unblocking: show tweet and add badge if (tweet) tweet.classList.remove('x-tweet-blocked'); // Add badge if not present (using cached data only) if (!el.querySelector('.x-info-badge')) { const badge = document.createElement('span'); badge.className = 'x-info-badge'; let content = ''; if (cachedInfo.location) { const flag = this.getFlagEmoji(cachedInfo.location); if (flag) content += `${flag}`; // Add VPN/Proxy indicator if location is not accurate if (cachedInfo.locationAccurate === false) { content += `๐Ÿ”’`; } } if (cachedInfo.device) { const emoji = this.getDeviceEmoji(cachedInfo.device); content += `${emoji}`; } badge.innerHTML = content; const insertionPoint = this.findInsertionPoint(el, screenName); if (insertionPoint) insertionPoint.target.insertBefore(badge, insertionPoint.ref); } } else { // Blocking: hide tweet and remove badge if (tweet) tweet.classList.add('x-tweet-blocked'); const badge = el.querySelector('.x-info-badge'); if (badge) badge.remove(); } } }); }); countriesContainer.appendChild(item); }); }; renderCountries(); // Search functionality search.addEventListener('input', (e) => { renderCountries(e.target.value); }); // Close button header.querySelector('.x-blocker-close').addEventListener('click', () => { overlay.remove(); }); // Close on overlay click overlay.addEventListener('click', (e) => { if (e.target === overlay) { overlay.remove(); } }); // Add to page document.body.appendChild(overlay); } /** * 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); console.log('Blocked Countries:', Array.from(this.blockedCountries)); }, openBlocker: () => { this.showBlockerModal(); }, getBlockedCountries: () => { return Array.from(this.blockedCountries); } }; if (typeof cloneInto === 'function') { unsafeWindow.XFlagScript = cloneInto(api, unsafeWindow, { cloneFunctions: true }); } else { unsafeWindow.XFlagScript = api; } } } // Instantiate new XLocationPatcher(); })();