// ==UserScript== // @name Xbout-Userscript // @namespace https://github.com/code-ga/Xbout-Userscript // @version 1.9 // @description Display the user's location, device type, and registration year on the X (Twitter) page. // @author code-gal // @match https://x.com/* // @match https://twitter.com/* // @icon https://abs.twimg.com/favicons/twitter.2.ico // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @license MIT // @downloadURL none // ==/UserScript== (function() { 'use strict'; // Inject CSS Styles GM_addStyle(` .xbout-badge { display: inline-flex !important; align-items: center; font-size: 13px; line-height: 1; white-space: nowrap; margin-left: 4px; opacity: 0; transition: opacity 0.2s ease-out; color: #536471; cursor: default; flex-shrink: 0; /* Prevent shrinking */ } .xbout-badge.loaded { opacity: 1; } .xbout-item { display: inline-flex; align-items: center; margin: 0 2px; padding: 1px 2px; border-radius: 4px; } .xbout-item:hover { background-color: rgba(0, 0, 0, 0.05); } .xbout-dot { color: rgb(83, 100, 113); margin: 0 2px; } .xbout-sep { color: #536471; margin: 0 3px; font-size: 10px; opacity: 0.4; } .xbout-year { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; font-weight: 500; font-size: 12px; } .xbout-device-icon { width: 1.1em; height: 1.1em; vertical-align: -0.15em; filter: grayscale(100%); opacity: 0.7; } .xbout-ad-mark { font-size: 10px; border: 1px solid currentColor; border-radius: 3px; padding: 1px 3px; line-height: 1; margin-left: 2px; opacity: 0.8; font-weight: 500; } /* Dark mode adjustments */ @media (prefers-color-scheme: dark) { .xbout-badge, .xbout-dot, .xbout-year, .xbout-sep { color: #71767b; } .xbout-item:hover { background-color: rgba(255, 255, 255, 0.1); } .xbout-device-icon { filter: grayscale(100%) invert(1); } } `); // Configuration const CONFIG = { MIN_DELAY: 2000, // 2s MAX_DELAY: 4000, // 4s RATE_LIMIT_WAIT: 300000, // 5m CACHE_DURATION: 7 * 24 * 60 * 60 * 1000, STORAGE_KEY: 'xbout_cache_v2', BEARER_TOKEN: 'AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs=1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA', FALLBACK_QUERY_ID: 'zs_jFPFT78rBpXv9Z3U2YQ', ICONS: { APPLE: '', ANDROID: '', WEB: '', AD: '' // Warning icon } }; // Extended Country Mapping const countryToFlag = { 'china': '๐Ÿ‡จ๐Ÿ‡ณ', 'japan': '๐Ÿ‡ฏ๐Ÿ‡ต', 'south korea': '๐Ÿ‡ฐ๐Ÿ‡ท', 'korea': '๐Ÿ‡ฐ๐Ÿ‡ท', 'taiwan': '๐Ÿ‡น๐Ÿ‡ผ', 'hong kong': '๐Ÿ‡ญ๐Ÿ‡ฐ', 'singapore': '๐Ÿ‡ธ๐Ÿ‡ฌ', 'india': '๐Ÿ‡ฎ๐Ÿ‡ณ', 'thailand': '๐Ÿ‡น๐Ÿ‡ญ', 'vietnam': '๐Ÿ‡ป๐Ÿ‡ณ', 'malaysia': '๐Ÿ‡ฒ๐Ÿ‡พ', 'indonesia': '๐Ÿ‡ฎ๐Ÿ‡ฉ', 'philippines': '๐Ÿ‡ต๐Ÿ‡ญ', 'pakistan': '๐Ÿ‡ต๐Ÿ‡ฐ', 'bangladesh': '๐Ÿ‡ง๐Ÿ‡ฉ', 'nepal': '๐Ÿ‡ณ๐Ÿ‡ต', 'sri lanka': '๐Ÿ‡ฑ๐Ÿ‡ฐ', 'myanmar': '๐Ÿ‡ฒ๐Ÿ‡ฒ', 'cambodia': '๐Ÿ‡ฐ๐Ÿ‡ญ', 'mongolia': '๐Ÿ‡ฒ๐Ÿ‡ณ', 'saudi arabia': '๐Ÿ‡ธ๐Ÿ‡ฆ', 'united arab emirates': '๐Ÿ‡ฆ๐Ÿ‡ช', 'uae': '๐Ÿ‡ฆ๐Ÿ‡ช', 'israel': '๐Ÿ‡ฎ๐Ÿ‡ฑ', 'turkey': '๐Ÿ‡น๐Ÿ‡ท', 'tรผrkiye': '๐Ÿ‡น๐Ÿ‡ท', 'iran': '๐Ÿ‡ฎ๐Ÿ‡ท', 'iraq': '๐Ÿ‡ฎ๐Ÿ‡ถ', 'qatar': '๐Ÿ‡ถ๐Ÿ‡ฆ', 'kuwait': '๐Ÿ‡ฐ๐Ÿ‡ผ', 'jordan': '๐Ÿ‡ฏ๐Ÿ‡ด', 'lebanon': '๐Ÿ‡ฑ๐Ÿ‡ง', 'bahrain': '๐Ÿ‡ง๐Ÿ‡ญ', 'oman': '๐Ÿ‡ด๐Ÿ‡ฒ', 'united kingdom': '๐Ÿ‡ฌ๐Ÿ‡ง', 'uk': '๐Ÿ‡ฌ๐Ÿ‡ง', 'england': '๐Ÿ‡ฌ๐Ÿ‡ง', 'france': '๐Ÿ‡ซ๐Ÿ‡ท', 'germany': '๐Ÿ‡ฉ๐Ÿ‡ช', 'italy': '๐Ÿ‡ฎ๐Ÿ‡น', 'spain': '๐Ÿ‡ช๐Ÿ‡ธ', 'portugal': '๐Ÿ‡ต๐Ÿ‡น', 'netherlands': '๐Ÿ‡ณ๐Ÿ‡ฑ', 'belgium': '๐Ÿ‡ง๐Ÿ‡ช', 'switzerland': '๐Ÿ‡จ๐Ÿ‡ญ', 'austria': '๐Ÿ‡ฆ๐Ÿ‡น', 'sweden': '๐Ÿ‡ธ๐Ÿ‡ช', 'norway': '๐Ÿ‡ณ๐Ÿ‡ด', 'denmark': '๐Ÿ‡ฉ๐Ÿ‡ฐ', 'finland': '๐Ÿ‡ซ๐Ÿ‡ฎ', 'poland': '๐Ÿ‡ต๐Ÿ‡ฑ', 'russia': '๐Ÿ‡ท๐Ÿ‡บ', 'ukraine': '๐Ÿ‡บ๐Ÿ‡ฆ', 'greece': '๐Ÿ‡ฌ๐Ÿ‡ท', 'czech republic': '๐Ÿ‡จ๐Ÿ‡ฟ', 'czechia': '๐Ÿ‡จ๐Ÿ‡ฟ', 'hungary': '๐Ÿ‡ญ๐Ÿ‡บ', 'romania': '๐Ÿ‡ท๐Ÿ‡ด', 'ireland': '๐Ÿ‡ฎ๐Ÿ‡ช', 'scotland': '๐Ÿด๓ ง๓ ข๓ ณ๓ ฃ๓ ด๓ ฟ', 'united states': '๐Ÿ‡บ๐Ÿ‡ธ', 'usa': '๐Ÿ‡บ๐Ÿ‡ธ', 'us': '๐Ÿ‡บ๐Ÿ‡ธ', 'canada': '๐Ÿ‡จ๐Ÿ‡ฆ', 'mexico': '๐Ÿ‡ฒ๐Ÿ‡ฝ', 'brazil': '๐Ÿ‡ง๐Ÿ‡ท', 'argentina': '๐Ÿ‡ฆ๐Ÿ‡ท', 'chile': '๐Ÿ‡จ๐Ÿ‡ฑ', 'colombia': '๐Ÿ‡จ๐Ÿ‡ด', 'peru': '๐Ÿ‡ต๐Ÿ‡ช', 'venezuela': '๐Ÿ‡ป๐Ÿ‡ช', 'australia': '๐Ÿ‡ฆ๐Ÿ‡บ', 'new zealand': '๐Ÿ‡ณ๐Ÿ‡ฟ', 'south africa': '๐Ÿ‡ฟ๐Ÿ‡ฆ', 'egypt': '๐Ÿ‡ช๐Ÿ‡ฌ', 'nigeria': '๐Ÿ‡ณ๐Ÿ‡ฌ', 'kenya': '๐Ÿ‡ฐ๐Ÿ‡ช', 'morocco': '๐Ÿ‡ฒ๐Ÿ‡ฆ', 'ethiopia': '๐Ÿ‡ช๐Ÿ‡น', 'ghana': '๐Ÿ‡ฌ๐Ÿ‡ญ', 'algeria': '๐Ÿ‡ฉ๐Ÿ‡ฟ', 'angola': '๐Ÿ‡ฆ๐Ÿ‡ด', 'benin': '๐Ÿ‡ง๐Ÿ‡ฏ', 'botswana': '๐Ÿ‡ง๐Ÿ‡ผ', 'burkina faso': '๐Ÿ‡ง๐Ÿ‡ซ', 'burundi': '๐Ÿ‡ง๐Ÿ‡ฎ', 'cameroon': '๐Ÿ‡จ๐Ÿ‡ฒ', 'cape verde': '๐Ÿ‡จ๐Ÿ‡ป', 'chad': '๐Ÿ‡น๐Ÿ‡ฉ', 'comoros': '๐Ÿ‡ฐ๐Ÿ‡ฒ', 'congo': '๐Ÿ‡จ๐Ÿ‡ฌ', 'djibouti': '๐Ÿ‡ฉ๐Ÿ‡ฏ', 'equatorial guinea': '๐Ÿ‡ฌt', 'eritrea': '๐Ÿ‡ช๐Ÿ‡ท', 'eswatini': '๐Ÿ‡ธ๐Ÿ‡ฟ', 'gabon': '๐Ÿ‡ฌ๐Ÿ‡ฆ', 'gambia': '๐Ÿ‡ฌ๐Ÿ‡ฒ', 'guinea': '๐Ÿ‡ฌ๐Ÿ‡ณ', 'guinea-bissau': '๐Ÿ‡ฌ๐Ÿ‡ผ', 'ivory coast': '๐Ÿ‡จ๐Ÿ‡ฎ', 'lesotho': '๐Ÿ‡ฑ๐Ÿ‡ธ', 'liberia': '๐Ÿ‡ฑ๐Ÿ‡ท', 'libya': '๐Ÿ‡ฑ๐Ÿ‡พ', 'madagascar': '๐Ÿ‡ฒ๐Ÿ‡ฌ', 'malawi': '๐Ÿ‡ฒ๐Ÿ‡ผ', 'mali': '๐Ÿ‡ฒ๐Ÿ‡ฑ', 'mauritania': '๐Ÿ‡ฒ๐Ÿ‡ท', 'mauritius': '๐Ÿ‡ฒ๐Ÿ‡บ', 'mozambique': '๐Ÿ‡ฒ๐Ÿ‡ฟ', 'namibia': '๐Ÿ‡ณ๐Ÿ‡ฆ', 'niger': '๐Ÿ‡ณ๐Ÿ‡ช', 'rwanda': '๐Ÿ‡ท๐Ÿ‡ผ', 'senegal': '๐Ÿ‡ธ๐Ÿ‡ณ', 'seychelles': '๐Ÿ‡ธ๐Ÿ‡จ', 'sierra leone': '๐Ÿ‡ธ๐Ÿ‡ฑ', 'somalia': '๐Ÿ‡ธ๐Ÿ‡ด', 'south sudan': '๐Ÿ‡ธ๐Ÿ‡ธ', 'sudan': '๐Ÿ‡ธ๐Ÿ‡ฉ', 'tanzania': '๐Ÿ‡น๐Ÿ‡ฟ', 'togo': '๐Ÿ‡น๐Ÿ‡ฌ', 'tunisia': '๐Ÿ‡น๐Ÿ‡ณ', 'uganda': '๐Ÿ‡บ๐Ÿ‡ฌ', 'zambia': '๐Ÿ‡ฟ๐Ÿ‡ฒ', 'zimbabwe': '๐Ÿ‡ฟ๐Ÿ‡ผ', 'afghanistan': '๐Ÿ‡ฆ๐Ÿ‡ซ', 'armenia': '๐Ÿ‡ฆ๐Ÿ‡ฒ', 'azerbaijan': '๐Ÿ‡ฆ๐Ÿ‡ฟ', 'bhutan': '๐Ÿ‡ง๐Ÿ‡น', 'brunei': '๐Ÿ‡ง๐Ÿ‡ณ', 'cyprus': '๐Ÿ‡จ๐Ÿ‡พ', 'georgia': '๐Ÿ‡ฌ๐Ÿ‡ช', 'kazakhstan': '๐Ÿ‡ฐ๐Ÿ‡ฟ', 'kyrgyzstan': '๐Ÿ‡ฐ๐Ÿ‡ฌ', 'laos': '๐Ÿ‡ฑ๐Ÿ‡ฆ', 'maldives': '๐Ÿ‡ฒ๐Ÿ‡ป', 'north korea': '๐Ÿ‡ฐ๐Ÿ‡ต', 'syria': '๐Ÿ‡ธ๐Ÿ‡พ', 'tajikistan': '๐Ÿ‡น๐Ÿ‡ฏ', 'timor-leste': '๐Ÿ‡น๐Ÿ‡ฑ', 'turkmenistan': '๐Ÿ‡น๐Ÿ‡ฒ', 'uzbekistan': '๐Ÿ‡บ๐Ÿ‡ฟ', 'yemen': '๐Ÿ‡พ๐Ÿ‡ช', 'albania': '๐Ÿ‡ฆ๐Ÿ‡ฑ', 'andorra': '๐Ÿ‡ฆ๐Ÿ‡ฉ', 'belarus': '๐Ÿ‡ง๐Ÿ‡พ', 'bosnia': '๐Ÿ‡ง๐Ÿ‡ฆ', 'bulgaria': '๐Ÿ‡ง๐Ÿ‡ฌ', 'croatia': '๐Ÿ‡ญ๐Ÿ‡ท', 'estonia': '๐Ÿ‡ช๐Ÿ‡ช', 'iceland': '๐Ÿ‡ฎ๐Ÿ‡ธ', 'kosovo': '๐Ÿ‡ฝ๐Ÿ‡ฐ', 'latvia': '๐Ÿ‡ฑ๐Ÿ‡ป', 'liechtenstein': '๐Ÿ‡ฑ๐Ÿ‡ฎ', 'lithuania': '๐Ÿ‡ฑ๐Ÿ‡น', 'luxembourg': '๐Ÿ‡ฑ๐Ÿ‡บ', 'malta': '๐Ÿ‡ฒ๐Ÿ‡น', 'moldova': '๐Ÿ‡ฒ๐Ÿ‡ฉ', 'monaco': '๐Ÿ‡ฒ๐Ÿ‡จ', 'montenegro': '๐Ÿ‡ฒ๐Ÿ‡ช', 'north macedonia': '๐Ÿ‡ฒ๐Ÿ‡ฐ', 'san marino': '๐Ÿ‡ธ๐Ÿ‡ฒ', 'serbia': '๐Ÿ‡ท๐Ÿ‡ธ', 'slovakia': '๐Ÿ‡ธ๐Ÿ‡ฐ', 'slovenia': '๐Ÿ‡ธ๐Ÿ‡ฎ', 'vatican': '๐Ÿ‡ป๐Ÿ‡ฆ', 'antigua': '๐Ÿ‡ฆ๐Ÿ‡ฌ', 'bahamas': '๐Ÿ‡ง๐Ÿ‡ธ', 'barbados': '๐Ÿ‡ง๐Ÿ‡ง', 'belize': '๐Ÿ‡ง๐Ÿ‡ฟ', 'costa rica': '๐Ÿ‡จ๐Ÿ‡ท', 'cuba': '๐Ÿ‡จ๐Ÿ‡บ', 'dominica': '๐Ÿ‡ฉ๐Ÿ‡ฒ', 'dominican republic': '๐Ÿ‡ฉ๐Ÿ‡ด', 'el salvador': '๐Ÿ‡ธ๐Ÿ‡ป', 'grenada': '๐Ÿ‡ฌ๐Ÿ‡ฉ', 'guatemala': '๐Ÿ‡ฌ๐Ÿ‡น', 'haiti': '๐Ÿ‡ญ๐Ÿ‡น', 'honduras': '๐Ÿ‡ญ๐Ÿ‡ณ', 'jamaica': '๐Ÿ‡ฏ๐Ÿ‡ฒ', 'nicaragua': '๐Ÿ‡ณ๐Ÿ‡ฎ', 'panama': '๐Ÿ‡ต๐Ÿ‡ฆ', 'saint kitts': '๐Ÿ‡ฐ๐Ÿ‡ณ', 'saint lucia': '๐Ÿ‡ฑ๐Ÿ‡จ', 'saint vincent': '๐Ÿ‡ป๐Ÿ‡จ', 'trinidad': '๐Ÿ‡น๐Ÿ‡น', 'bolivia': '๐Ÿ‡ง๐Ÿ‡ด', 'ecuador': '๐Ÿ‡ช๐Ÿ‡จ', 'guyana': '๐Ÿ‡ฌ๐Ÿ‡พ', 'paraguay': '๐Ÿ‡ต๐Ÿ‡พ', 'suriname': '๐Ÿ‡ธ๐Ÿ‡ท', 'uruguay': '๐Ÿ‡บ๐Ÿ‡พ', 'fiji': '๐Ÿ‡ซ๐Ÿ‡ฏ', 'kiribati': '๐Ÿ‡ฐ๐Ÿ‡ฎ', 'marshall islands': '๐Ÿ‡ฒ๐Ÿ‡ญ', 'micronesia': '๐Ÿ‡ซ๐Ÿ‡ฒ', 'nauru': '๐Ÿ‡ณ๐Ÿ‡ท', 'palau': '๐Ÿ‡ต๐Ÿ‡ผ', 'papua new guinea': '๐Ÿ‡ต๐Ÿ‡ฌ', 'samoa': '๐Ÿ‡ผ๐Ÿ‡ธ', 'solomon islands': '๐Ÿ‡ธ๐Ÿ‡ง', 'tonga': '๐Ÿ‡น๐Ÿ‡ด', 'tuvalu': '๐Ÿ‡น๐Ÿ‡ป', 'vanuatu': '๐Ÿ‡ป๐Ÿ‡บ' }; // --- State Management --- let queryId = null; let requestQueue = []; let isProcessing = false; let rateLimitUntil = 0; const pendingRequests = new Map(); const elementState = new WeakMap(); const sessionRequestedUsers = new Set(); // --- Cache Manager --- const Cache = (() => { let store = null; let saveTimer = null; const load = () => { if (store) return; try { store = GM_getValue(CONFIG.STORAGE_KEY, {}); } catch (e) { store = {}; } }; const save = () => { if (saveTimer) clearTimeout(saveTimer); saveTimer = setTimeout(() => { GM_setValue(CONFIG.STORAGE_KEY, store); saveTimer = null; }, 2000); }; return { get: (username) => { load(); const record = store[username]; if (!record) return null; if (Date.now() > record.expiry) { delete store[username]; return null; } return record.data; }, set: (username, data) => { load(); if (Math.random() < 0.05) { const now = Date.now(); for(const key in store) { if(store[key].expiry < now) delete store[key]; } } store[username] = { data, expiry: Date.now() + CONFIG.CACHE_DURATION }; save(); } }; })(); // --- Helpers --- function getFlagInfo(location) { if (!location) return null; const loc = location.toLowerCase().trim(); let emoji = '๐ŸŒ'; if (countryToFlag[loc]) { emoji = countryToFlag[loc]; } else { for (const [country, flag] of Object.entries(countryToFlag)) { if (loc.includes(country) || country.includes(loc)) { emoji = flag; break; } } } return { emoji, name: location }; } function getDeviceInfo(source) { if (!source) return null; const text = source.replace(/<[^>]*>?/gm, ''); const s = text.toLowerCase(); let icon = null; if (s.includes('iphone') || s.includes('ipad') || s.includes('mac') || s.includes('ios') || s.includes('app store')) { icon = CONFIG.ICONS.APPLE; } else if (s.includes('android')) { icon = CONFIG.ICONS.ANDROID; } else if (s.includes('web') || s.includes('browser') || s.includes('chrome') || s.includes('edge') || s.includes('safari')) { icon = CONFIG.ICONS.WEB; } if (icon) { return { icon, name: text }; } return null; } function getYear(createdAt) { if (!createdAt) return ''; const match = createdAt.match(/(\d{4})$/); return match ? match[1] : ''; } function getCsrfToken() { const match = document.cookie.match(/ct0=([^;]+)/); return match ? match[1] : null; } // --- Networking --- async function fetchQueryId() { try { const entries = performance.getEntriesByType('resource'); for (const entry of entries) { const match = entry.name.match(/graphql\/([^/]+)\/AboutAccountQuery/); if (match) return match[1]; } } catch (e) {} return CONFIG.FALLBACK_QUERY_ID; } function initQueryIdListener() { try { const observer = new PerformanceObserver((list) => { for (const entry of list.getEntries()) { const match = entry.name.match(/graphql\/([^/]+)\/AboutAccountQuery/); if (match) queryId = match[1]; } }); observer.observe({ entryTypes: ['resource'] }); } catch (e) {} } async function fetchUserInfo(username) { const cached = Cache.get(username); if (cached) return cached; if (pendingRequests.has(username)) return pendingRequests.get(username); const promise = (async () => { const csrfToken = getCsrfToken(); if (!csrfToken) return null; const qId = queryId || await fetchQueryId(); const url = `https://x.com/i/api/graphql/${qId}/AboutAccountQuery?variables=${encodeURIComponent(JSON.stringify({ screenName: username }))}`; const resp = await fetch(url, { method: 'GET', credentials: 'include', headers: { 'authorization': `Bearer ${CONFIG.BEARER_TOKEN}`, 'content-type': 'application/json', 'x-csrf-token': csrfToken, 'x-twitter-active-user': 'yes', 'x-twitter-auth-type': 'OAuth2Session' } }); if (resp.status === 429) throw new Error('RATE_LIMIT'); if (!resp.ok) return null; const data = await resp.json(); const result = data?.data?.user_result_by_screen_name?.result; if (result) { const info = { location: result.about_profile?.account_based_in || null, source: result.about_profile?.source || null, createdAt: result.core?.created_at || null }; if (info.location || info.source || info.createdAt) { Cache.set(username, info); } return info; } return null; })(); pendingRequests.set(username, promise); try { return await promise; } catch (e) { throw e; } finally { pendingRequests.delete(username); } } async function processQueue() { if (isProcessing) return; isProcessing = true; while (requestQueue.length > 0) { const now = Date.now(); if (now < rateLimitUntil) { const wait = rateLimitUntil - now; console.log(`[Xbout] Rate limited. Resuming in ${Math.ceil(wait/1000)}s`); await new Promise(r => setTimeout(r, wait)); continue; } const { username, resolve } = requestQueue.shift(); try { const info = await fetchUserInfo(username); resolve(info); const delay = Math.floor(Math.random() * (CONFIG.MAX_DELAY - CONFIG.MIN_DELAY + 1) + CONFIG.MIN_DELAY); await new Promise(r => setTimeout(r, delay)); } catch (e) { if (e.message === 'RATE_LIMIT') { console.warn(`[Xbout] 429 Rate Limit Hit. Pausing for ${CONFIG.RATE_LIMIT_WAIT / 1000 / 60} minutes.`); rateLimitUntil = Date.now() + CONFIG.RATE_LIMIT_WAIT; requestQueue.unshift({ username, resolve }); } else { resolve(null); } } } isProcessing = false; } function queueRequest(username) { return new Promise((resolve) => { if (Cache.get(username)) { resolve(Cache.get(username)); return; } if (sessionRequestedUsers.has(username) && !pendingRequests.has(username)) { resolve(null); return; } sessionRequestedUsers.add(username); requestQueue.push({ username, resolve }); processQueue(); }); } // --- DOM & UI --- function createBadge(info, username, isAd = false) { if (!info) return null; const flagInfo = getFlagInfo(info.location); const deviceInfo = getDeviceInfo(info.source); const year = getYear(info.createdAt); if (!flagInfo && !deviceInfo && !year && !isAd) return null; const badge = document.createElement('div'); badge.className = 'xbout-badge'; badge.dataset.user = username; const dot = document.createElement('span'); dot.className = 'xbout-dot'; dot.textContent = 'ยท'; badge.appendChild(dot); if (flagInfo) { const item = document.createElement('span'); item.className = 'xbout-item'; item.textContent = flagInfo.emoji; item.title = `Location: ${flagInfo.name}`; badge.appendChild(item); } if (deviceInfo) { if (flagInfo) badge.appendChild(createSep()); const item = document.createElement('span'); item.className = 'xbout-item'; item.innerHTML = deviceInfo.icon; item.title = `Source: ${deviceInfo.name}`; badge.appendChild(item); } if (year) { if (flagInfo || deviceInfo) badge.appendChild(createSep()); const item = document.createElement('span'); item.className = 'xbout-item xbout-year'; item.textContent = year; item.title = `Joined: ${year}`; badge.appendChild(item); } if (isAd) { if (flagInfo || deviceInfo || year) badge.appendChild(createSep()); const item = document.createElement('span'); item.className = 'xbout-ad-mark'; item.textContent = 'Ad'; item.title = 'Promoted Content'; badge.appendChild(item); } setTimeout(() => badge.classList.add('loaded'), 50); return badge; } function createSep() { const sep = document.createElement('span'); sep.className = 'xbout-sep'; sep.textContent = '|'; return sep; } const intersectionObserver = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const target = entry.target; const username = target.dataset.xboutUser; intersectionObserver.unobserve(target); const state = elementState.get(target) || {}; if (state.requested) return; state.requested = true; elementState.set(target, state); queueRequest(username).then(info => { if (info && !elementState.get(target).rendered) { const header = target.closest('[data-testid="User-Name"]'); const isAd = header && !header.querySelector('time'); const badge = createBadge(info, username, isAd); if (badge) { const insertTarget = findInsertTarget(target, isAd); if (insertTarget) { insertTarget.after(badge); } else { target.after(badge); } const newState = elementState.get(target); newState.rendered = true; elementState.set(target, newState); } } }); } }); }, { rootMargin: '100px' }); function findInsertTarget(userLink, isAd) { const header = userLink.closest('[data-testid="User-Name"]'); if (!header) return userLink; // Strategy 1: Normal Tweet with Time const time = header.querySelector('time'); if (time) { return time.parentElement; } // Strategy 2: Ad Tweet (No time, inside tweet) // Insert at the very end of the header container if (isAd) { return header.lastElementChild || userLink; } // Strategy 3: Profile Header or others if (header.lastElementChild) { return header.lastElementChild; } return userLink; } function processLink(link) { if (link.hasAttribute('data-xbout-user')) return; const text = (link.textContent || '').trim(); if (!text.startsWith('@') || text.includes(' ')) return; const username = text.slice(1); const blacklist = ['home', 'explore', 'notifications', 'messages', 'settings', 'search', 'privacy', 'tos', 'about', 'support']; if (blacklist.includes(username.toLowerCase())) return; link.dataset.xboutUser = username; elementState.set(link, { requested: false, rendered: false }); intersectionObserver.observe(link); } function scanNode(node) { if (node.nodeType !== 1) return; const check = (link) => { if (link.closest('[data-testid="User-Name"]')) { processLink(link); } }; if (node.tagName === 'A' && node.getAttribute('href')?.startsWith('/')) { check(node); } node.querySelectorAll('a[href^="/"]').forEach(check); } // --- Optimized Mutation Observer --- const domObserver = new MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.addedNodes.length) { for (const node of mutation.addedNodes) { scanNode(node); } } } }); function init() { console.log('[Xbout] v1.9.1 Initialized'); initQueryIdListener(); // Initial scan document.querySelectorAll('[data-testid="User-Name"]').forEach(container => { const links = container.querySelectorAll('a[href^="/"]'); links.forEach(processLink); }); // Observer config domObserver.observe(document.body, { childList: true, subtree: true }); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();