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