// ==UserScript== // @name Twitter Account Location Flag // @namespace http://tampermonkey.net/ // @version 1.0.2 // @description Shows country flag emoji next to Twitter usernames based on account location // @author X1aoS0ng // @match https://x.com/* // @match https://twitter.com/* // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_xmlhttpRequest // @connect x.com // @connect twitter.com // @run-at document-end // @license MIT // @downloadURL https://update.greasyfork.icu/scripts/556757/Twitter%20Account%20Location%20Flag.user.js // @updateURL https://update.greasyfork.icu/scripts/556757/Twitter%20Account%20Location%20Flag.meta.js // ==/UserScript== (function () { 'use strict'; // ==================== Region Emojis Mapping (US State Flags) ==================== const REGION_EMOJIS = { "East Asia & Pacific": "๐ŸŒ", "Europe & Central Asia": "๐ŸŒ", "Latin America & Caribbean": "๐ŸŒŽ", "Middle East & North Africa": "๐Ÿ•Œ", "North America": "๐ŸŒŽ", "South Asia": "๐ŸŒ", "Sub-Saharan Africa": "๐ŸŒ", "Global": "๐ŸŒ", "Worldwide": "๐ŸŒ" }; // ==================== Country Flags Mapping ==================== const COUNTRY_FLAGS = { // A "Afghanistan": "๐Ÿ‡ฆ๐Ÿ‡ซ", "Albania": "๐Ÿ‡ฆ๐Ÿ‡ฑ", "Algeria": "๐Ÿ‡ฉ๐Ÿ‡ฟ", "Andorra": "๐Ÿ‡ฆ๐Ÿ‡ฉ", "Angola": "๐Ÿ‡ฆ๐Ÿ‡ด", "Antigua and Barbuda": "๐Ÿ‡ฆ๐Ÿ‡ฌ", "Argentina": "๐Ÿ‡ฆ๐Ÿ‡ท", "Armenia": "๐Ÿ‡ฆ๐Ÿ‡ฒ", "Australia": "๐Ÿ‡ฆ๐Ÿ‡บ", "Austria": "๐Ÿ‡ฆ๐Ÿ‡น", "Azerbaijan": "๐Ÿ‡ฆ๐Ÿ‡ฟ", // B "Bahamas": "๐Ÿ‡ง๐Ÿ‡ธ", "Bahrain": "๐Ÿ‡ง๐Ÿ‡ญ", "Bangladesh": "๐Ÿ‡ง๐Ÿ‡ฉ", "Barbados": "๐Ÿ‡ง๐Ÿ‡ง", "Belarus": "๐Ÿ‡ง๐Ÿ‡พ", "Belgium": "๐Ÿ‡ง๐Ÿ‡ช", "Belize": "๐Ÿ‡ง๐Ÿ‡ฟ", "Benin": "๐Ÿ‡ง๐Ÿ‡ฏ", "Bhutan": "๐Ÿ‡ง๐Ÿ‡น", "Bolivia": "๐Ÿ‡ง๐Ÿ‡ด", "Bosnia and Herzegovina": "๐Ÿ‡ง๐Ÿ‡ฆ", "Botswana": "๐Ÿ‡ง๐Ÿ‡ผ", "Brazil": "๐Ÿ‡ง๐Ÿ‡ท", "Brunei": "๐Ÿ‡ง๐Ÿ‡ณ", "Bulgaria": "๐Ÿ‡ง๐Ÿ‡ฌ", "Burkina Faso": "๐Ÿ‡ง๐Ÿ‡ซ", "Burundi": "๐Ÿ‡ง๐Ÿ‡ฎ", // C "Cambodia": "๐Ÿ‡ฐ๐Ÿ‡ญ", "Cameroon": "๐Ÿ‡จ๐Ÿ‡ฒ", "Canada": "๐Ÿ‡จ๐Ÿ‡ฆ", "Cape Verde": "๐Ÿ‡จ๐Ÿ‡ป", "Central African Republic": "๐Ÿ‡จ๐Ÿ‡ซ", "Chad": "๐Ÿ‡น๐Ÿ‡ฉ", "Chile": "๐Ÿ‡จ๐Ÿ‡ฑ", "China": "๐Ÿ‡จ๐Ÿ‡ณ", "Colombia": "๐Ÿ‡จ๐Ÿ‡ด", "Comoros": "๐Ÿ‡ฐ๐Ÿ‡ฒ", "Congo": "๐Ÿ‡จ๐Ÿ‡ฌ", "Costa Rica": "๐Ÿ‡จ๐Ÿ‡ท", "Croatia": "๐Ÿ‡ญ๐Ÿ‡ท", "Cuba": "๐Ÿ‡จ๐Ÿ‡บ", "Cyprus": "๐Ÿ‡จ๐Ÿ‡พ", "Czech Republic": "๐Ÿ‡จ๐Ÿ‡ฟ", "Czechia": "๐Ÿ‡จ๐Ÿ‡ฟ", // D "Democratic Republic of the Congo": "๐Ÿ‡จ๐Ÿ‡ฉ", "Denmark": "๐Ÿ‡ฉ๐Ÿ‡ฐ", "Djibouti": "๐Ÿ‡ฉ๐Ÿ‡ฏ", "Dominica": "๐Ÿ‡ฉ๐Ÿ‡ฒ", "Dominican Republic": "๐Ÿ‡ฉ๐Ÿ‡ด", // E "Ecuador": "๐Ÿ‡ช๐Ÿ‡จ", "Egypt": "๐Ÿ‡ช๐Ÿ‡ฌ", "El Salvador": "๐Ÿ‡ธ๐Ÿ‡ป", "Equatorial Guinea": "๐Ÿ‡ฌ๐Ÿ‡ถ", "Eritrea": "๐Ÿ‡ช๐Ÿ‡ท", "Estonia": "๐Ÿ‡ช๐Ÿ‡ช", "Eswatini": "๐Ÿ‡ธ๐Ÿ‡ฟ", "Ethiopia": "๐Ÿ‡ช๐Ÿ‡น", "Europe": "๐Ÿ‡ช๐Ÿ‡บ", "European Union": "๐Ÿ‡ช๐Ÿ‡บ", // F "Fiji": "๐Ÿ‡ซ๐Ÿ‡ฏ", "Finland": "๐Ÿ‡ซ๐Ÿ‡ฎ", "France": "๐Ÿ‡ซ๐Ÿ‡ท", // G "Gabon": "๐Ÿ‡ฌ๐Ÿ‡ฆ", "Gambia": "๐Ÿ‡ฌ๐Ÿ‡ฒ", "Georgia": "๐Ÿ‡ฌ๐Ÿ‡ช", "Germany": "๐Ÿ‡ฉ๐Ÿ‡ช", "Ghana": "๐Ÿ‡ฌ๐Ÿ‡ญ", "Greece": "๐Ÿ‡ฌ๐Ÿ‡ท", "Grenada": "๐Ÿ‡ฌ๐Ÿ‡ฉ", "Guatemala": "๐Ÿ‡ฌ๐Ÿ‡น", "Guinea": "๐Ÿ‡ฌ๐Ÿ‡ณ", "Guinea-Bissau": "๐Ÿ‡ฌ๐Ÿ‡ผ", "Guyana": "๐Ÿ‡ฌ๐Ÿ‡พ", // H "Haiti": "๐Ÿ‡ญ๐Ÿ‡น", "Honduras": "๐Ÿ‡ญ๐Ÿ‡ณ", "Hong Kong": "๐Ÿ‡ญ๐Ÿ‡ฐ", "Hungary": "๐Ÿ‡ญ๐Ÿ‡บ", // I "Iceland": "๐Ÿ‡ฎ๐Ÿ‡ธ", "India": "๐Ÿ‡ฎ๐Ÿ‡ณ", "Indonesia": "๐Ÿ‡ฎ๐Ÿ‡ฉ", "Iran": "๐Ÿ‡ฎ๐Ÿ‡ท", "Iraq": "๐Ÿ‡ฎ๐Ÿ‡ถ", "Ireland": "๐Ÿ‡ฎ๐Ÿ‡ช", "Israel": "๐Ÿ‡ฎ๐Ÿ‡ฑ", "Italy": "๐Ÿ‡ฎ๐Ÿ‡น", "Ivory Coast": "๐Ÿ‡จ๐Ÿ‡ฎ", // J "Jamaica": "๐Ÿ‡ฏ๐Ÿ‡ฒ", "Japan": "๐Ÿ‡ฏ๐Ÿ‡ต", "Jordan": "๐Ÿ‡ฏ๐Ÿ‡ด", // K "Kazakhstan": "๐Ÿ‡ฐ๐Ÿ‡ฟ", "Kenya": "๐Ÿ‡ฐ๐Ÿ‡ช", "Kiribati": "๐Ÿ‡ฐ๐Ÿ‡ฎ", "Korea": "๐Ÿ‡ฐ๐Ÿ‡ท", "Kosovo": "๐Ÿ‡ฝ๐Ÿ‡ฐ", "Kuwait": "๐Ÿ‡ฐ๐Ÿ‡ผ", "Kyrgyzstan": "๐Ÿ‡ฐ๐Ÿ‡ฌ", // L "Laos": "๐Ÿ‡ฑ๐Ÿ‡ฆ", "Latvia": "๐Ÿ‡ฑ๐Ÿ‡ป", "Lebanon": "๐Ÿ‡ฑ๐Ÿ‡ง", "Lesotho": "๐Ÿ‡ฑ๐Ÿ‡ธ", "Liberia": "๐Ÿ‡ฑ๐Ÿ‡ท", "Libya": "๐Ÿ‡ฑ๐Ÿ‡พ", "Liechtenstein": "๐Ÿ‡ฑ๐Ÿ‡ฎ", "Lithuania": "๐Ÿ‡ฑ๐Ÿ‡น", "Luxembourg": "๐Ÿ‡ฑ๐Ÿ‡บ", // M "Madagascar": "๐Ÿ‡ฒ๐Ÿ‡ฌ", "Malawi": "๐Ÿ‡ฒ๐Ÿ‡ผ", "Malaysia": "๐Ÿ‡ฒ๐Ÿ‡พ", "Maldives": "๐Ÿ‡ฒ๐Ÿ‡ป", "Mali": "๐Ÿ‡ฒ๐Ÿ‡ฑ", "Malta": "๐Ÿ‡ฒ๐Ÿ‡น", "Marshall Islands": "๐Ÿ‡ฒ๐Ÿ‡ญ", "Mauritania": "๐Ÿ‡ฒ๐Ÿ‡ท", "Mauritius": "๐Ÿ‡ฒ๐Ÿ‡บ", "Mexico": "๐Ÿ‡ฒ๐Ÿ‡ฝ", "Micronesia": "๐Ÿ‡ซ๐Ÿ‡ฒ", "Moldova": "๐Ÿ‡ฒ๐Ÿ‡ฉ", "Monaco": "๐Ÿ‡ฒ๐Ÿ‡จ", "Mongolia": "๐Ÿ‡ฒ๐Ÿ‡ณ", "Montenegro": "๐Ÿ‡ฒ๐Ÿ‡ช", "Morocco": "๐Ÿ‡ฒ๐Ÿ‡ฆ", "Mozambique": "๐Ÿ‡ฒ๐Ÿ‡ฟ", "Myanmar": "๐Ÿ‡ฒ๐Ÿ‡ฒ", // N "Namibia": "๐Ÿ‡ณ๐Ÿ‡ฆ", "Nauru": "๐Ÿ‡ณ๐Ÿ‡ท", "Nepal": "๐Ÿ‡ณ๐Ÿ‡ต", "Netherlands": "๐Ÿ‡ณ๐Ÿ‡ฑ", "New Zealand": "๐Ÿ‡ณ๐Ÿ‡ฟ", "Nicaragua": "๐Ÿ‡ณ๐Ÿ‡ฎ", "Niger": "๐Ÿ‡ณ๐Ÿ‡ช", "Nigeria": "๐Ÿ‡ณ๐Ÿ‡ฌ", "North Korea": "๐Ÿ‡ฐ๐Ÿ‡ต", "North Macedonia": "๐Ÿ‡ฒ๐Ÿ‡ฐ", "Norway": "๐Ÿ‡ณ๐Ÿ‡ด", // O "Oman": "๐Ÿ‡ด๐Ÿ‡ฒ", // P "Pakistan": "๐Ÿ‡ต๐Ÿ‡ฐ", "Palau": "๐Ÿ‡ต๐Ÿ‡ผ", "Palestine": "๐Ÿ‡ต๐Ÿ‡ธ", "Panama": "๐Ÿ‡ต๐Ÿ‡ฆ", "Papua New Guinea": "๐Ÿ‡ต๐Ÿ‡ฌ", "Paraguay": "๐Ÿ‡ต๐Ÿ‡พ", "Peru": "๐Ÿ‡ต๐Ÿ‡ช", "Philippines": "๐Ÿ‡ต๐Ÿ‡ญ", "Poland": "๐Ÿ‡ต๐Ÿ‡ฑ", "Portugal": "๐Ÿ‡ต๐Ÿ‡น", // Q "Qatar": "๐Ÿ‡ถ๐Ÿ‡ฆ", // R "Romania": "๐Ÿ‡ท๐Ÿ‡ด", "Russia": "๐Ÿ‡ท๐Ÿ‡บ", "Rwanda": "๐Ÿ‡ท๐Ÿ‡ผ", // S "Saint Kitts and Nevis": "๐Ÿ‡ฐ๐Ÿ‡ณ", "Saint Lucia": "๐Ÿ‡ฑ๐Ÿ‡จ", "Saint Vincent and the Grenadines": "๐Ÿ‡ป๐Ÿ‡จ", "Samoa": "๐Ÿ‡ผ๐Ÿ‡ธ", "San Marino": "๐Ÿ‡ธ๐Ÿ‡ฒ", "Sao Tome and Principe": "๐Ÿ‡ธ๐Ÿ‡น", "Saudi Arabia": "๐Ÿ‡ธ๐Ÿ‡ฆ", "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": "๐Ÿ‡ธ๐Ÿ‡พ", // T "Taiwan": "๐Ÿ‡น๐Ÿ‡ผ", "Tajikistan": "๐Ÿ‡น๐Ÿ‡ฏ", "Tanzania": "๐Ÿ‡น๐Ÿ‡ฟ", "Thailand": "๐Ÿ‡น๐Ÿ‡ญ", "Timor-Leste": "๐Ÿ‡น๐Ÿ‡ฑ", "Togo": "๐Ÿ‡น๐Ÿ‡ฌ", "Tonga": "๐Ÿ‡น๐Ÿ‡ด", "Trinidad and Tobago": "๐Ÿ‡น๐Ÿ‡น", "Tunisia": "๐Ÿ‡น๐Ÿ‡ณ", "Turkey": "๐Ÿ‡น๐Ÿ‡ท", "Turkmenistan": "๐Ÿ‡น๐Ÿ‡ฒ", "Tuvalu": "๐Ÿ‡น๐Ÿ‡ป", // U "Uganda": "๐Ÿ‡บ๐Ÿ‡ฌ", "Ukraine": "๐Ÿ‡บ๐Ÿ‡ฆ", "United Arab Emirates": "๐Ÿ‡ฆ๐Ÿ‡ช", "United Kingdom": "๐Ÿ‡ฌ๐Ÿ‡ง", "United States": "๐Ÿ‡บ๐Ÿ‡ธ", "Uruguay": "๐Ÿ‡บ๐Ÿ‡พ", "Uzbekistan": "๐Ÿ‡บ๐Ÿ‡ฟ", // V "Vanuatu": "๐Ÿ‡ป๐Ÿ‡บ", "Vatican City": "๐Ÿ‡ป๐Ÿ‡ฆ", "Venezuela": "๐Ÿ‡ป๐Ÿ‡ช", "Vietnam": "๐Ÿ‡ป๐Ÿ‡ณ", // Y "Yemen": "๐Ÿ‡พ๐Ÿ‡ช", // Z "Zambia": "๐Ÿ‡ฟ๐Ÿ‡ฒ", "Zimbabwe": "๐Ÿ‡ฟ๐Ÿ‡ผ" }; function getCountryFlag(locationName) { if (!locationName) { return null; } // Try region emojis first (exact match) if (REGION_EMOJIS[locationName]) { return REGION_EMOJIS[locationName]; } // Try country flags (exact match) if (COUNTRY_FLAGS[locationName]) { return COUNTRY_FLAGS[locationName]; } // Try case-insensitive match in regions const normalized = locationName.trim(); for (const [region, emoji] of Object.entries(REGION_EMOJIS)) { if (region.toLowerCase() === normalized.toLowerCase()) { return emoji; } } // Try case-insensitive match in countries for (const [country, flag] of Object.entries(COUNTRY_FLAGS)) { if (country.toLowerCase() === normalized.toLowerCase()) { return flag; } } return null; } // ==================== Cache Management ==================== let locationCache = new Map(); const CACHE_KEY = 'twitter_location_cache'; const CACHE_EXPIRY_DAYS = 30; // Load cache from GM storage function loadCache() { try { const cached = GM_getValue(CACHE_KEY, '{}'); const cacheObj = JSON.parse(cached); const now = Date.now(); for (const [username, data] of Object.entries(cacheObj)) { if (data.expiry && data.expiry > now && data.location !== null) { locationCache.set(username, data.location); } } } catch (error) { } } // Save cache to GM storage function saveCache() { try { const cacheObj = {}; const now = Date.now(); const expiry = now + (CACHE_EXPIRY_DAYS * 24 * 60 * 60 * 1000); for (const [username, location] of locationCache.entries()) { cacheObj[username] = { location: location, expiry: expiry, cachedAt: now }; } GM_setValue(CACHE_KEY, JSON.stringify(cacheObj)); } catch (error) { console.error('Error saving cache:', error); } } // Save a single entry to cache function saveCacheEntry(username, location) { locationCache.set(username, location); if (!saveCacheEntry.timeout) { saveCacheEntry.timeout = setTimeout(() => { saveCache(); saveCacheEntry.timeout = null; }, 5000); } } // ==================== Rate Limiting ==================== const requestQueue = []; let isProcessingQueue = false; let lastRequestTime = 0; const MIN_REQUEST_INTERVAL = 2000; const MAX_CONCURRENT_REQUESTS = 2; let activeRequests = 0; let rateLimitResetTime = 0; const processingUsernames = new Set(); // ==================== Direct API Call (No Page Script Needed) ==================== // Get CSRF token from cookies function getCsrfToken() { const cookies = document.cookie.split(';'); for (let cookie of cookies) { const [name, value] = cookie.trim().split('='); if (name === 'ct0') { return value; } } return null; } // Debug: Log all cookies to find the auth token function debugCookies() { const cookies = document.cookie.split(';'); for (let cookie of cookies) { const [name, value] = cookie.trim().split('='); } } function fetchLocationFromAPI(screenName) { return new Promise((resolve, reject) => { const csrfToken = getCsrfToken(); if (!csrfToken) { debugCookies(); resolve(null); return; } const variables = JSON.stringify({ screenName: screenName }); const url = `https://x.com/i/api/graphql/XRqGa7EeokUU5kppkh13EA/AboutAccountQuery?variables=${encodeURIComponent(variables)}`; // Use GM_xmlhttpRequest which automatically includes cookies GM_xmlhttpRequest({ method: 'GET', url: url, headers: { 'Authorization': 'Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA', 'X-Csrf-Token': csrfToken, 'X-Twitter-Auth-Type': 'OAuth2Session', 'X-Twitter-Active-User': 'yes', 'Content-Type': 'application/json' }, onload: function (response) { if (response.status === 200) { try { const data = JSON.parse(response.responseText); const location = data?.data?.user_result_by_screen_name?.result?.about_profile?.account_based_in || null; resolve(location); } catch (error) { resolve(null); } } else { resolve(null); } }, onerror: function (error) { resolve(null); } }); }); } // ==================== API Request Functions ==================== async function processRequestQueue() { if (isProcessingQueue || requestQueue.length === 0) return; if (rateLimitResetTime > 0) { const now = Math.floor(Date.now() / 1000); if (now < rateLimitResetTime) { const waitTime = (rateLimitResetTime - now) * 1000; setTimeout(processRequestQueue, Math.min(waitTime, 60000)); return; } else { rateLimitResetTime = 0; } } isProcessingQueue = true; while (requestQueue.length > 0 && activeRequests < MAX_CONCURRENT_REQUESTS) { const now = Date.now(); const timeSinceLastRequest = now - lastRequestTime; if (timeSinceLastRequest < MIN_REQUEST_INTERVAL) { await new Promise(resolve => setTimeout(resolve, MIN_REQUEST_INTERVAL - timeSinceLastRequest)); } const { screenName, resolve, reject } = requestQueue.shift(); activeRequests++; lastRequestTime = Date.now(); makeLocationRequest(screenName) .then(location => resolve(location)) .catch(error => reject(error)) .finally(() => { activeRequests--; setTimeout(processRequestQueue, 200); }); } isProcessingQueue = false; } async function makeLocationRequest(screenName) { try { const location = await fetchLocationFromAPI(screenName); if (location) { saveCacheEntry(screenName, location); } else { saveCacheEntry(screenName, null); } return location; } catch (error) { return null; } } async function getUserLocation(screenName) { if (locationCache.has(screenName)) { const cached = locationCache.get(screenName); if (cached !== null) { return cached; } else { locationCache.delete(screenName); } } return new Promise((resolve, reject) => { requestQueue.push({ screenName, resolve, reject }); processRequestQueue(); }); } // ==================== Username Extraction ==================== function extractUsername(element) { const usernameElement = element.querySelector('[data-testid="UserName"], [data-testid="User-Name"]'); if (usernameElement) { const links = usernameElement.querySelectorAll('a[href^="/"]'); for (const link of links) { const href = link.getAttribute('href'); const match = href.match(/^\/([^\/\?]+)/); if (match && match[1]) { const username = match[1]; const excludedRoutes = ['home', 'explore', 'notifications', 'messages', 'i', 'compose', 'search', 'settings', 'bookmarks', 'lists', 'communities']; if (!excludedRoutes.includes(username) && !username.startsWith('hashtag') && !username.startsWith('search') && username.length > 0 && username.length < 20) { return username; } } } } const allLinks = element.querySelectorAll('a[href^="/"]'); const seenUsernames = new Set(); for (const link of allLinks) { const href = link.getAttribute('href'); if (!href) continue; const match = href.match(/^\/([^\/\?]+)/); if (!match || !match[1]) continue; const potentialUsername = match[1]; if (seenUsernames.has(potentialUsername)) continue; seenUsernames.add(potentialUsername); const excludedRoutes = ['home', 'explore', 'notifications', 'messages', 'i', 'compose', 'search', 'settings', 'bookmarks', 'lists', 'communities', 'hashtag']; if (excludedRoutes.some(route => potentialUsername === route || potentialUsername.startsWith(route))) { continue; } if (potentialUsername.includes('status') || potentialUsername.match(/^\d+$/)) { continue; } const text = link.textContent?.trim() || ''; const linkText = text.toLowerCase(); const usernameLower = potentialUsername.toLowerCase(); if (text.startsWith('@')) { return potentialUsername; } if (linkText === usernameLower || linkText === `@${usernameLower}`) { return potentialUsername; } const parent = link.closest('[data-testid="UserName"], [data-testid="User-Name"]'); if (parent) { if (potentialUsername.length > 0 && potentialUsername.length < 20 && !potentialUsername.includes('/')) { return potentialUsername; } } if (text && text.trim().startsWith('@')) { const atUsername = text.trim().substring(1); if (atUsername === potentialUsername) { return potentialUsername; } } } const textContent = element.textContent || ''; const atMentionMatches = textContent.matchAll(/@([a-zA-Z0-9_]+)/g); for (const match of atMentionMatches) { const username = match[1]; const link = element.querySelector(`a[href="/${username}"], a[href^="/${username}?"]`); if (link) { const isInUserNameContainer = link.closest('[data-testid="UserName"], [data-testid="User-Name"]'); if (isInUserNameContainer) { return username; } } } return null; } function findHandleSection(container, screenName) { return Array.from(container.querySelectorAll('div')).find(div => { const link = div.querySelector(`a[href="/${screenName}"]`); if (link) { const text = link.textContent?.trim(); return text === `@${screenName}`; } return false; }); } // ==================== UI Functions ==================== function createLoadingShimmer() { const shimmer = document.createElement('span'); shimmer.setAttribute('data-twitter-flag-shimmer', 'true'); shimmer.style.display = 'inline-block'; shimmer.style.width = '20px'; shimmer.style.height = '16px'; shimmer.style.marginLeft = '4px'; shimmer.style.marginRight = '4px'; shimmer.style.verticalAlign = 'middle'; shimmer.style.borderRadius = '2px'; shimmer.style.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%)'; shimmer.style.backgroundSize = '200% 100%'; shimmer.style.animation = 'shimmer 1.5s infinite'; if (!document.getElementById('twitter-flag-shimmer-style')) { const style = document.createElement('style'); style.id = 'twitter-flag-shimmer-style'; style.textContent = ` @keyframes shimmer { 0% { background-position: -200% 0; } 100% { background-position: 200% 0; } } @keyframes marquee-border { 0% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } 100% { background-position: 0% 50%; } } `; document.head.appendChild(style); } return shimmer; } // ==================== Add Flag to Username ==================== async function addFlagToUsername(usernameElement, screenName) { if (usernameElement.dataset.flagAdded === 'true') { return; } if (processingUsernames.has(screenName)) { await new Promise(resolve => setTimeout(resolve, 500)); if (usernameElement.dataset.flagAdded === 'true') return; usernameElement.dataset.flagAdded = 'waiting'; return; } usernameElement.dataset.flagAdded = 'processing'; processingUsernames.add(screenName); const userNameContainer = usernameElement.querySelector('[data-testid="UserName"], [data-testid="User-Name"]'); const shimmerSpan = createLoadingShimmer(); let shimmerInserted = false; if (userNameContainer) { const handleSection = findHandleSection(userNameContainer, screenName); if (handleSection && handleSection.parentNode) { try { handleSection.parentNode.insertBefore(shimmerSpan, handleSection); shimmerInserted = true; } catch (e) { try { userNameContainer.appendChild(shimmerSpan); shimmerInserted = true; } catch (e2) { } } } else { try { userNameContainer.appendChild(shimmerSpan); shimmerInserted = true; } catch (e) { } } } try { const location = await getUserLocation(screenName); if (shimmerInserted && shimmerSpan.parentNode) { shimmerSpan.remove(); } if (!location) { usernameElement.dataset.flagAdded = 'failed'; return; } const flag = getCountryFlag(location); if (!flag) { if (shimmerInserted && shimmerSpan.parentNode) { shimmerSpan.remove(); } usernameElement.dataset.flagAdded = 'failed'; return; } let usernameLink = null; const containerForLink = userNameContainer || usernameElement.querySelector('[data-testid="UserName"], [data-testid="User-Name"]'); if (containerForLink) { const containerLinks = containerForLink.querySelectorAll('a[href^="/"]'); for (const link of containerLinks) { const text = link.textContent?.trim(); const href = link.getAttribute('href'); const match = href.match(/^\/([^\/\?]+)/); if (match && match[1] === screenName) { if (text === `@${screenName}` || text === screenName) { usernameLink = link; break; } } } } if (!usernameLink && containerForLink) { const containerLinks = containerForLink.querySelectorAll('a[href^="/"]'); for (const link of containerLinks) { const text = link.textContent?.trim(); if (text === `@${screenName}`) { usernameLink = link; break; } } } if (!usernameLink) { const links = usernameElement.querySelectorAll('a[href^="/"]'); for (const link of links) { const href = link.getAttribute('href'); const text = link.textContent?.trim(); if ((href === `/${screenName}` || href.startsWith(`/${screenName}?`)) && (text === `@${screenName}` || text === screenName)) { usernameLink = link; break; } } } if (!usernameLink) { const links = usernameElement.querySelectorAll('a[href^="/"]'); for (const link of links) { const href = link.getAttribute('href'); const match = href.match(/^\/([^\/\?]+)/); if (match && match[1] === screenName) { const hasVerificationBadge = link.closest('[data-testid="User-Name"]')?.querySelector('[data-testid="icon-verified"]'); if (!hasVerificationBadge || link.textContent?.trim() === `@${screenName}`) { usernameLink = link; break; } } } } if (!usernameLink) { if (shimmerInserted && shimmerSpan.parentNode) { shimmerSpan.remove(); } usernameElement.dataset.flagAdded = 'failed'; return; } const existingFlag = usernameElement.querySelector('[data-twitter-flag]'); if (existingFlag) { if (shimmerInserted && shimmerSpan.parentNode) { shimmerSpan.remove(); } usernameElement.dataset.flagAdded = 'true'; return; } const flagSpan = document.createElement('span'); flagSpan.style.display = 'inline-block'; flagSpan.style.maxWidth = '17px'; flagSpan.textContent = ` ${flag}`; flagSpan.setAttribute('data-twitter-flag', 'true'); // ๅฐบๅฏธๅ’Œๅฝข็Šถ๏ผš้•ฟๆ–นๅฝข + ๅฐๅœ†่ง’ flagSpan.style.padding = '0px 7px'; // ้ซ˜ๅบฆๆ›ดไฝŽ flagSpan.style.borderRadius = '4px'; // ๅฐๅœ†่ง’๏ผŒไธๅ†ๆ˜ฏ่ƒถๅ›Š flagSpan.style.fontSize = '1em'; // ๆ›ดๆŽฅ่ฟ‘ Twitter ็š„้ซ˜ๅบฆ flagSpan.style.lineHeight = '1.5'; // ้ฟๅ… emoji ๆ’‘้ซ˜ // ่พนๆก†๏ผšๆต…็ฐ่‰ฒ flagSpan.style.border = '1px solid rgba(255,255,255,0.4)'; // ่ƒŒๆ™ฏ๏ผš่ฝปๅพฎๆทฑ็ฐ่‰ฒ๏ผˆๆ›ดๅƒ Twitter๏ผ‰ flagSpan.style.background = 'rgba(255,255,255,0.15)'; // ไธ้œ€่ฆๅŠจ็”ปไบ†๏ผŒๅŽปๆމๆ›ดๅนฒๅ‡€ flagSpan.style.backgroundImage = ''; flagSpan.style.animation = ''; flagSpan.style.backgroundClip = ''; flagSpan.style.backgroundOrigin = ''; flagSpan.style.backgroundSize = ''; const containerForFlag = userNameContainer || usernameElement.querySelector('[data-testid="UserName"], [data-testid="User-Name"]'); if (!containerForFlag) { if (shimmerInserted && shimmerSpan.parentNode) { shimmerSpan.remove(); } usernameElement.dataset.flagAdded = 'failed'; return; } const handleSection = findHandleSection(containerForFlag, screenName); let inserted = false; if (handleSection && handleSection.parentNode === containerForFlag) { try { containerForFlag.insertBefore(flagSpan, handleSection); inserted = true; } catch (e) { } } if (!inserted && handleSection && handleSection.parentNode) { try { const handleParent = handleSection.parentNode; if (handleParent !== containerForFlag && handleParent.parentNode) { handleParent.parentNode.insertBefore(flagSpan, handleParent); inserted = true; } else if (handleParent === containerForFlag) { containerForFlag.insertBefore(flagSpan, handleSection); inserted = true; } } catch (e) { } } if (!inserted && handleSection) { try { const displayNameLink = containerForFlag.querySelector('a[href^="/"]'); if (displayNameLink) { const displayNameContainer = displayNameLink.closest('div'); if (displayNameContainer && displayNameContainer.parentNode) { if (displayNameContainer.parentNode === handleSection.parentNode) { displayNameContainer.parentNode.insertBefore(flagSpan, handleSection); inserted = true; } else { displayNameContainer.parentNode.insertBefore(flagSpan, displayNameContainer.nextSibling); inserted = true; } } } } catch (e) { } } if (!inserted) { try { containerForFlag.appendChild(flagSpan); inserted = true; } catch (e) { } } if (inserted) { usernameElement.dataset.flagAdded = 'true'; const waitingContainers = document.querySelectorAll(`[data-flag-added="waiting"]`); waitingContainers.forEach(container => { const waitingUsername = extractUsername(container); if (waitingUsername === screenName) { addFlagToUsername(container, screenName).catch(() => { }); } }); } else { if (shimmerInserted && shimmerSpan.parentNode) { shimmerSpan.remove(); } usernameElement.dataset.flagAdded = 'failed'; } } catch (error) { if (shimmerInserted && shimmerSpan.parentNode) { shimmerSpan.remove(); } usernameElement.dataset.flagAdded = 'failed'; } finally { processingUsernames.delete(screenName); } } // ==================== Process Usernames ==================== async function processUsernames() { const containers = document.querySelectorAll('article[data-testid="tweet"], [data-testid="UserCell"], [data-testid="User-Names"], [data-testid="User-Name"]'); let foundCount = 0; let processedCount = 0; let skippedCount = 0; for (const container of containers) { const screenName = extractUsername(container); if (screenName) { foundCount++; const status = container.dataset.flagAdded; if (!status || status === 'failed') { processedCount++; addFlagToUsername(container, screenName).catch(err => { container.dataset.flagAdded = 'failed'; }); } else { skippedCount++; } } } } // ==================== Observer for Dynamic Content ==================== let observer = null; function initObserver() { if (observer) { observer.disconnect(); } observer = new MutationObserver((mutations) => { let shouldProcess = false; let addedNodesCount = 0; for (const mutation of mutations) { if (mutation.addedNodes.length > 0) { shouldProcess = true; addedNodesCount += mutation.addedNodes.length; } } if (shouldProcess) { setTimeout(processUsernames, 500); } }); observer.observe(document.body, { childList: true, subtree: true }); } // ==================== Initialization ==================== function init() { loadCache(); setTimeout(() => { processUsernames(); }, 2000); initObserver(); let lastUrl = location.href; new MutationObserver(() => { const url = location.href; if (url !== lastUrl) { lastUrl = url; setTimeout(processUsernames, 2000); } }).observe(document, { subtree: true, childList: true }); setInterval(() => { saveCache(); }, 30000); } // Run when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();