Shows country flag emoji next to Twitter usernames based on account location
在 Twitter / X 上自动为用户名旁边添加 所属国家 / 地区的国旗 + 低调跑马灯边框
让你一眼区分不同地区的账号,同时尽量保持 UI「原生风」与「低调骚」。
这个 Userscript 会在你浏览 Twitter / X(https://x.com/* / https://twitter.com/*)时:
AboutAccountQuery)查询该账号的 Account location(account_based_in)。Japan → 🇯🇵)Europe & Central Asia → 🌍) 显示名 @username 🇯🇵
油猴脚本管理器(至少一个):
账号已登录 Twitter / X,否则无法调用内部 API 获取位置信息
// ==UserScript== 元数据保留所有逻辑代码原样复制
脚本 已启用
作用域包含:https://x.com/* 和 https://twitter.com/*
脚本内置了两类映射:
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": "🌐"
};
Czech Republic / Czechia 这种别名)。匹配规则:
null 不显示国旗。匹配逻辑封装在:
function getCountryFlag(locationName) { ... }
你可以在映射表中增加 / 修改自己的国家显示风格,比如把 Europe 强行显示为 🇪🇺 已经在代码中实现。
脚本使用 GM_getValue / GM_setValue 做持久化缓存:
CACHE_KEY = 'twitter_location_cache'CACHE_EXPIRY_DAYS = 30(默认缓存 30 天)缓存结构大致为:
{
"username1": {
"location": "Japan",
"expiry": 1730000000000,
"cachedAt": 1727000000000
},
"username2": {
"location": "Europe & Central Asia",
"expiry": ...,
"cachedAt": ...
}
}
加载时:
location !== null 的缓存恢复到内存 Map 中。写入时:
saveCacheEntry(username, location) 更新 MapsetTimeout 延迟 5 秒批量持久化,避免频繁 GM_setValue。你可以修改:
const CACHE_EXPIRY_DAYS = 30;
来调整缓存有效期。
为防止触发 X / Twitter 的风控、429 或其他限制,脚本实现了一个简单的 请求队列 + 限流:
MIN_REQUEST_INTERVAL = 2000 毫秒(两次 API 请求之间最少间隔 2 秒)MAX_CONCURRENT_REQUESTS = 2(最多同时处理两个请求)维护:
requestQueue:待处理用户名队列activeRequests:当前正在处理的请求数量lastRequestTime:上一次请求的时间rateLimitResetTime:如果检测到官方限流,可以在这里记录恢复时间(预留逻辑)每当需要查询某个用户位置时,会调用:
getUserLocation(screenName) // 返回 Promise<string | null>
如果缓存中已有数据,直接返回;否则:
requestQueueprocessRequestQueue() 按照限流规则依次调用 makeLocationRequest(screenName)脚本使用 GM_xmlhttpRequest 直接调用 Twitter / X 的 GraphQL 接口:
const url = 'https://x.com/i/api/graphql/XRqGa7EeokUU5kppkh13EA/AboutAccountQuery?variables=...';
关键点:
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;
}
若获取不到 ct0,脚本会放弃请求(返回 null)。
GM_xmlhttpRequest({
method: 'GET',
url: url,
headers: {
'Authorization': 'Bearer AAAAAAAAAA......',
'X-Csrf-Token': csrfToken,
'X-Twitter-Auth-Type': 'OAuth2Session',
'X-Twitter-Active-User': 'yes',
'Content-Type': 'application/json'
},
onload: (response) => { ... },
onerror: () => resolve(null)
});
const location = data?.data?.user_result_by_screen_name?.result?.about_profile?.account_based_in || null;
⚠ 注意:
- 这类内部 API 可能在未来随 X 的后端更新而变动,如失效需要更新 query id 或字段路径。
- 本脚本仅在前端调用官方接口,不篡改请求,不向第三方服务器转发。
脚本会在以下区域检索用户名容器:
article[data-testid="tweet"],
[data-testid="UserCell"],
[data-testid="User-Names"],
[data-testid="User-Name"]
并用 extractUsername(element) 尝试从中解析出 screenName:
data-testid="UserName" / "User-Name" 内的链接 href="/username" 获取过滤掉:
/home / /explore / /messages 等路由hashtag / search 等特殊路径status/id 等非用户名也支持从文本中的 @username 正则匹配,并验证其是否对应 a[href="/username"]
解析成功后,会调用 addFlagToUsername(usernameElement, screenName):
dataset.flagAdded 避免重复操作插入位置尽量选择:
@handle 之间 或上下位置X / Twitter 的时间线是无限滚动 + 动态注入的,因此脚本使用 MutationObserver 实现自动刷新:
observer = new MutationObserver((mutations) => {
let shouldProcess = false;
for (const mutation of mutations) {
if (mutation.addedNodes.length > 0) {
shouldProcess = true;
}
}
if (shouldProcess) {
setTimeout(processUsernames, 500);
}
});
额外还监听 URL 变化(单页应用路由):
location.href 改变,延迟 2 秒重新扫描用户名。在查询位置期间,脚本会插入一个灰色的“小矩形 shimmer”作为占位符:
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';
...
}
并在 <head> 中自动注入 @keyframes shimmer。
你可以采用类似这样的设置(示例,供 README 描述用):
flagSpan.style.marginLeft = '4px';
flagSpan.style.marginRight = '4px';
flagSpan.style.display = 'inline-block';
flagSpan.style.verticalAlign = 'middle';
flagSpan.style.color = 'inherit';
// 自动尺寸 + 小圆角
flagSpan.style.padding = '1px 6px';
flagSpan.style.borderRadius = '4px';
flagSpan.style.fontSize = '0.95em';
flagSpan.style.lineHeight = '1.2';
flagSpan.style.border = '1px solid transparent';
// 低调灰色跑马灯边框
flagSpan.style.backgroundImage =
'linear-gradient(rgba(15,20,25,0.9), rgba(15,20,25,0.9)),' +
'linear-gradient(120deg,' +
'rgba(120,120,120,0.0),' +
'rgba(180,180,180,0.7),' +
'rgba(120,120,120,0.0)' +
')';
flagSpan.style.backgroundOrigin = 'border-box';
flagSpan.style.backgroundClip = 'padding-box, border-box';
// 宽度自动随内容变化
flagSpan.style.backgroundSize = '100% 100%, 200% 100%';
// 慢速低调动画
flagSpan.style.animation = 'marquee-border 6s linear infinite';
对应的 @keyframes 已经写在:
@keyframes marquee-border {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
整体效果:远看就是一小块淡灰色标签,仔细看边缘在慢慢流动,非常低调。
如果系统默认字体对 emoji 支持不理想,可以通过 data-twitter-flag 设置字体族:
[data-twitter-flag] {
font-family: "Apple Color Emoji","Segoe UI Emoji","Noto Color Emoji",
"Twemoji Mozilla","EmojiOne Color",system-ui,sans-serif !important;
}
这样可以尽量保证 🇯🇵 🇺🇸 等国旗 emoji 能被正常渲染出来。
本脚本:
你可以随时手动清除缓存,例如在控制台或脚本中调用:
GM_deleteValue('twitter_location_cache');
可能原因:
Account based in / Locationabout_profile.account_based_inREGION_EMOJIS / COUNTRY_FLAGS 中没有匹配项
→ 你可以手动添加对应映射在脚本环境中执行:
GM_deleteValue('twitter_location_cache');
刷新页面后,会重新从 API 查询。
建议:
font-family 强制使用 emoji 字体;JP、US)或通过图片 / SVG 的方案来实现。几处常见可自定义点:
const CACHE_EXPIRY_DAYS = 30;
const MIN_REQUEST_INTERVAL = 2000;
const MAX_CONCURRENT_REQUESTS = 2;
REGION_EMOJIS 与 COUNTRY_FLAGS 中增删改调整 flagSpan.style.* 样式实现不同风格:
该脚本依赖于 X / Twitter 当前的页面结构与内部 API:
AboutAccountQuery)参数或字段变更可能导致查询失败如遇到报错或脚本失效,需要根据最新前端 / 网络请求调整对应逻辑。