Greasy Fork

Greasy Fork is available in English.

逮捕中国五毛自干五大外宣(Tampermonkey 版)

识别并标记 X(Twitter)上的中国五毛、自干五和大外宣账号,显示用户真实位置

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         逮捕中国五毛自干五大外宣(Tampermonkey 版)
// @namespace    http://tampermonkey.net/
// @version      2.0.0
// @description  识别并标记 X(Twitter)上的中国五毛、自干五和大外宣账号,显示用户真实位置
// @author       ChatGPT / Gemini (@Toyler_d)
// @license      MIT
// @match        https://x.com/*
// @match        https://twitter.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=x.com
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @grant        GM_deleteValue
// @grant        GM_listValues
// @grant        unsafeWindow
// ==/UserScript==

(function() {
    'use strict';

    // Inject CSS
    const css = `
/* ============================================ 
   X 用户标签增强 - 样式表
   ============================================ */

/* 徽章容器 */
.inty-wumao-badge-container {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin-left: 8px;
  font-size: 13px;
  vertical-align: middle;
}

/* 位置徽章 */
.inty-wumao-location-badge {
  display: inline-flex;
  align-items: center;
  padding: 2px 8px;
  background-color: rgba(29, 155, 240, 0.1);
  color: rgb(29, 155, 240);
  border-radius: 12px;
  font-size: 13px;
  font-weight: 400;
  line-height: 16px;
  white-space: nowrap;
  transition: background-color 0.2s ease;
  user-select: none;
  animation: inty-wumaoFadeIn 0.3s ease-out;
}

.inty-wumao-location-badge:hover {
  background-color: rgba(29, 155, 240, 0.15);
}

.inty-wumao-location-badge svg {
  flex-shrink: 0;
  vertical-align: middle;
  opacity: 0.8;
}

/* 中国用户特殊标记 */
.inty-wumao-location-badge.china-user {
  background-color: rgba(244, 67, 54, 0.15);
  color: rgb(244, 67, 54);
  font-weight: 600;
  border: 1px solid rgba(244, 67, 54, 0.3);
  box-shadow: 0 0 8px rgba(244, 67, 54, 0.2);
  animation: inty-wumaoFadeIn 0.3s ease-out, inty-wumaoChinaPulse 2s ease-in-out infinite;
}

.inty-wumao-location-badge.china-user:hover {
  background-color: rgba(244, 67, 54, 0.25);
  box-shadow: 0 0 12px rgba(244, 67, 54, 0.3);
}

/* 台湾用户特殊标记 */
.inty-wumao-location-badge.taiwan-user {
  background-color: rgba(33, 150, 243, 0.15);
  color: rgb(33, 150, 243);
  font-weight: 600;
  border: 1px solid rgba(33, 150, 243, 0.3);
  box-shadow: 0 0 8px rgba(33, 150, 243, 0.2);
  animation: inty-wumaoFadeIn 0.3s ease-out, inty-wumaoTaiwanPulse 2s ease-in-out infinite;
}

.inty-wumao-location-badge.taiwan-user:hover {
  background-color: rgba(33, 150, 243, 0.25);
  box-shadow: 0 0 12px rgba(33, 150, 243, 0.3);
}

/* 标签徽章 */
.inty-wumao-label-badge {
  display: inline-flex;
  align-items: center;
  padding: 2px 8px;
  background: #fff3cd;
  color: #856404;
  border-radius: 12px;
  font-size: 12px;
  font-weight: 600;
  border: 1px solid #ffeaa7;
  user-select: none;
  animation: inty-wumaoFadeIn 0.3s ease-out;
}

/* 编辑按钮 */
.inty-wumao-edit-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  padding: 0;
  background: #f7f9fa;
  border: 1px solid #e1e8ed;
  border-radius: 50%;
  cursor: pointer;
  font-size: 12px;
  transition: all 0.2s;
}

.inty-wumao-edit-btn:hover {
  background: #e1e8ed;
  transform: scale(1.1);
}

/* 编辑对话框遮罩 */
.inty-wumao-dialog {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 999999;
  animation: inty-wumaoDialogFadeIn 0.2s;
}

/* 对话框内容 */
.inty-wumao-dialog-content {
  background: white;
  border-radius: 16px;
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
  width: 90%;
  max-width: 450px;
  animation: inty-wumaoSlideUp 0.3s;
}

/* 对话框头部 */
.inty-wumao-dialog-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 16px 20px;
  border-bottom: 1px solid #e1e8ed;
}

.inty-wumao-dialog-header h3 {
  margin: 0;
  font-size: 16px;
  font-weight: 700;
  color: #0f1419;
}

.inty-wumao-dialog-close {
  background: none;
  border: none;
  font-size: 20px;
  cursor: pointer;
  color: #536471;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background 0.2s;
}

.inty-wumao-dialog-close:hover {
  background: #f7f9fa;
}

/* 对话框主体 */
.inty-wumao-dialog-body {
  padding: 20px;
}

.inty-wumao-dialog-body label {
  display: block;
  font-size: 14px;
  font-weight: 600;
  color: #0f1419;
  margin-bottom: 8px;
}

.inty-wumao-input {
  width: 100%;
  padding: 12px;
  font-size: 14px;
  border: 2px solid #e1e8ed;
  border-radius: 8px;
  box-sizing: border-box;
  transition: border-color 0.2s;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}

.inty-wumao-input:focus {
  outline: none;
  border-color: #1d9bf0;
}

.inty-wumao-dialog-info {
  margin-top: 12px;
  padding: 10px;
  background: #f7f9fa;
  border-radius: 6px;
}

.inty-wumao-dialog-info small {
  color: #536471;
  font-size: 13px;
}

/* 对话框底部 */
.inty-wumao-dialog-footer {
  display: flex;
  justify-content: space-between;
  padding: 16px 20px;
  border-top: 1px solid #e1e8ed;
  gap: 10px;
}

.inty-wumao-btn-delete,
.inty-wumao-btn-save {
  padding: 10px 20px;
  border: none;
  border-radius: 20px;
  font-size: 14px;
  font-weight: 700;
  cursor: pointer;
  transition: all 0.2s;
}

.inty-wumao-btn-delete {
  background: #f7f9fa;
  color: #f4212e;
  flex: 0 0 auto;
}

.inty-wumao-btn-delete:hover {
  background: #ffe1e3;
}

.inty-wumao-btn-save {
  background: #1d9bf0;
  color: white;
  flex: 1;
}

.inty-wumao-btn-save:hover {
  background: #1a8cd8;
}

/* 动画 */
@keyframes inty-wumaoFadeIn {
  from {
    opacity: 0;
    transform: translateX(-5px);
  }
  to {
    opacity: 1;
    transform: translateX(0);
  }
}

@keyframes inty-wumaoDialogFadeIn {
  from { opacity: 0; }
  to { opacity: 1; }
}

@keyframes inty-wumaoSlideUp {
  from {
    transform: translateY(20px);
    opacity: 0;
  }
  to {
    transform: translateY(0);
    opacity: 1;
  }
}

@keyframes inty-wumaoChinaPulse {
  0%, 100% {
    box-shadow: 0 0 8px rgba(244, 67, 54, 0.2);
  }
  50% {
    box-shadow: 0 0 16px rgba(244, 67, 54, 0.4);
  }
}

@keyframes inty-wumaoTaiwanPulse {
  0%, 100% {
    box-shadow: 0 0 8px rgba(33, 150, 243, 0.2);
  }
  50% {
    box-shadow: 0 0 16px rgba(33, 150, 243, 0.4);
  }
}

/* 深色模式支持 */
@media (prefers-color-scheme: dark) {
  .inty-wumao-location-badge {
    background-color: rgba(29, 155, 240, 0.15);
    color: rgb(29, 155, 240);
  }

  .inty-wumao-location-badge:hover {
    background-color: rgba(29, 155, 240, 0.2);
  }

  .inty-wumao-location-badge.china-user {
    background-color: rgba(244, 67, 54, 0.2);
    color: rgb(255, 107, 107);
    border: 1px solid rgba(244, 67, 54, 0.4);
  }

  .inty-wumao-location-badge.china-user:hover {
    background-color: rgba(244, 67, 54, 0.3);
  }

  .inty-wumao-location-badge.taiwan-user {
    background-color: rgba(33, 150, 243, 0.2);
    color: rgb(100, 181, 246);
    border: 1px solid rgba(33, 150, 243, 0.4);
  }

  .inty-wumao-location-badge.taiwan-user:hover {
    background-color: rgba(33, 150, 243, 0.3);
  }

  .inty-wumao-label-badge {
    background: #5c4a1f;
    color: #ffd95a;
    border-color: #7d6a33;
  }

  .inty-wumao-edit-btn {
    background: #2c3640;
    border-color: #3d4f5c;
  }

  .inty-wumao-edit-btn:hover {
    background: #3d4f5c;
  }

  .inty-wumao-dialog-content {
    background: #15202b;
    color: #ffffff;
  }

  .inty-wumao-dialog-header {
    border-bottom-color: #38444d;
  }

  .inty-wumao-dialog-header h3,
  .inty-wumao-dialog-body label {
    color: #ffffff;
  }

  .inty-wumao-dialog-close {
    color: #8b98a5;
  }

  .inty-wumao-dialog-close:hover {
    background: #1e2732;
  }

  .inty-wumao-input {
    background: #192734;
    border-color: #38444d;
    color: #ffffff;
  }

  .inty-wumao-input:focus {
    border-color: #1d9bf0;
  }

  .inty-wumao-dialog-info {
    background: #192734;
  }

  .inty-wumao-dialog-info small {
    color: #8b98a5;
  }

  .inty-wumao-dialog-footer {
    border-top-color: #38444d;
  }

  .inty-wumao-btn-delete {
    background: #2c3640;
  }

  .inty-wumao-btn-delete:hover {
    background: #5a2630;
  }
}

/* 移动端适配 */
@media (max-width: 500px) {
  .inty-wumao-badge-container {
    gap: 4px;
    margin-left: 4px;
  }

  .inty-wumao-location-badge,
  .inty-wumao-label-badge {
    font-size: 12px;
    padding: 1px 6px;
  }

  .inty-wumao-location-badge svg {
    width: 12px;
    height: 12px;
  }

  .inty-wumao-edit-btn {
    width: 20px;
    height: 20px;
    font-size: 11px;
  }
}

/* 确保徽章在用户名容器中正确对齐 */
[data-testid="User-Name"] .inty-wumao-badge-container {
  margin-top: 2px;
}
    `;
    GM_addStyle(css);

    // 1. Inject translations.js content
    const LOCATION_TRANSLATIONS_EXTENDED = `
United States|美国|🇺🇸
USA|美国|🇺🇸
US|美国|🇺🇸
United Kingdom|英国|🇬🇧
UK|英国|🇬🇧
Great Britain|英国|🇬🇧
England|英格兰|🏴󠁧󠁢󠁥󠁮󠁧󠁿
Scotland|苏格兰|🏴󠁧󠁢󠁳󠁣󠁴󠁿
Wales|威尔士|🏴󠁧󠁢󠁷󠁬󠁳󠁿
China|中国|🇨🇳
PRC|中国|🇨🇳
People's Republic of China|中国|🇨🇳
Taiwan|台湾|🇹🇼
ROC|台湾|🇹🇼
Hong Kong|香港|🇭🇰
Macau|澳门|🇲🇴
Macao|澳门|🇲🇴
Japan|日本|🇯🇵
South Korea|韩国|🇰🇷
Korea|韩国|🇰🇷
North Korea|朝鲜|🇰🇵
Singapore|新加坡|🇸🇬
Malaysia|马来西亚|🇲🇾
Thailand|泰国|🇹🇭
Vietnam|越南|🇻🇳
Indonesia|印度尼西亚|🇮🇩
Philippines|菲律宾|🇵🇭
India|印度|🇮🇳
Pakistan|巴基斯坦|🇵🇰
Bangladesh|孟加拉国|🇧🇩
Myanmar|缅甸|🇲🇲
Cambodia|柬埔寨|🇰🇭
Laos|老挝|🇱🇦
Mongolia|蒙古|🇲🇳
Nepal|尼泊尔|🇳🇵
Sri Lanka|斯里兰卡|🇱🇰
Israel|以色列|🇮🇱
Palestine|巴勒斯坦|🇵🇸
Saudi Arabia|沙特阿拉伯|🇸🇦
UAE|阿联酋|🇦🇪
United Arab Emirates|阿联酋|🇦🇪
Qatar|卡塔尔|🇶🇦
Kuwait|科威特|🇰🇼
Iran|伊朗|🇮🇷
Iraq|伊拉克|🇮🇶
Syria|叙利亚|🇸🇾
Lebanon|黎巴嫩|🇱🇧
Jordan|约旦|🇯🇴
Turkey|土耳其|🇹🇷
Germany|德国|🇩🇪
France|法国|🇫🇷
Italy|意大利|🇮🇹
Spain|西班牙|🇪🇸
Russia|俄罗斯|🇷🇺
Netherlands|荷兰|🇳🇱
Belgium|比利时|🇧🇪
Switzerland|瑞士|🇨🇭
Austria|奥地利|🇦🇹
Sweden|瑞典|🇸🇪
Norway|挪威|🇳🇴
Denmark|丹麦|🇩🇰
Finland|芬兰|🇫🇮
Poland|波兰|🇵🇱
Czech Republic|捷克|🇨🇿
Hungary|匈牙利|🇭🇺
Romania|罗马尼亚|🇷🇴
Greece|希腊|🇬🇷
Portugal|葡萄牙|🇵🇹
Ireland|爱尔兰|🇮🇪
Ukraine|乌克兰|🇺🇦
Belarus|白俄罗斯|🇧🇾
Iceland|冰岛|🇮🇸
Canada|加拿大|🇨🇦
Mexico|墨西哥|🇲🇽
Brazil|巴西|🇧🇷
Argentina|阿根廷|🇦🇷
Chile|智利|🇨🇱
Colombia|哥伦比亚|🇨🇴
Peru|秘鲁|🇵🇪
Venezuela|委内瑞拉|🇻🇪
Cuba|古巴|🇨🇺
Jamaica|牙买加|🇯🇲
Australia|澳大利亚|🇦🇺
New Zealand|新西兰|🇳🇿
Egypt|埃及|🇪🇬
South Africa|南非|🇿🇦
Nigeria|尼日利亚|🇳🇬
Kenya|肯尼亚|🇰🇪
Ethiopia|埃塞俄比亚|🇪🇹
Morocco|摩洛哥|🇲🇦
Algeria|阿尔及利亚|🇩🇿
Beijing|北京|🇨🇳
Shanghai|上海|🇨🇳
Guangzhou|广州|🇨🇳
Shenzhen|深圳|🇨🇳
Chengdu|成都|🇨🇳
Chongqing|重庆|🇨🇳
Hangzhou|杭州|🇨🇳
Wuhan|武汉|🇨🇳
Nanjing|南京|🇨🇳
Xi'an|西安|🇨🇳
Tianjin|天津|🇨🇳
Suzhou|苏州|🇨🇳
Changsha|长沙|🇨🇳
Shenyang|沈阳|🇨🇳
Qingdao|青岛|🇨🇳
Zhengzhou|郑州|🇨🇳
Dalian|大连|🇨🇳
Jinan|济南|🇨🇳
Xiamen|厦门|🇨🇳
Fuzhou|福州|🇨🇳
Kunming|昆明|🇨🇳
Harbin|哈尔滨|🇨🇳
Changchun|长春|🇨🇳
Shijiazhuang|石家庄|🇨🇳
Hefei|合肥|🇨🇳
Nanchang|南昌|🇨🇳
Guiyang|贵阳|🇨🇳
Taiyuan|太原|🇨🇳
Lanzhou|兰州|🇨🇳
Hohhot|呼和浩特|🇨🇳
Urumqi|乌鲁木齐|🇨🇳
Yinchuan|银川|🇨🇳
Xining|西宁|🇨🇳
Lhasa|拉萨|🇨🇳
Haikou|海口|🇨🇳
Sanya|三亚|🇨🇳
Ningbo|宁波|🇨🇳
Wenzhou|温州|🇨🇳
Dongguan|东莞|🇨🇳
Foshan|佛山|🇨🇳
Zhuhai|珠海|🇨🇳
Nanning|南宁|🇨🇳
Guangdong|广东|🇨🇳
Zhejiang|浙江|🇨🇳
Jiangsu|江苏|🇨🇳
Sichuan|四川|🇨🇳
Hubei|湖北|🇨🇳
Hunan|湖南|🇨🇳
Hebei|河北|🇨🇳
Henan|河南|🇨🇳
Shandong|山东|🇨🇳
Shaanxi|陕西|🇨🇳
Liaoning|辽宁|🇨🇳
Jilin|吉林|🇨🇳
Heilongjiang|黑龙江|🇨🇳
Anhui|安徽|🇨🇳
Fujian|福建|🇨🇳
Jiangxi|江西|🇨🇳
Shanxi|山西|🇨🇳
Inner Mongolia|内蒙古|🇨🇳
Xinjiang|新疆|🇨🇳
Tibet|西藏|🇨🇳
Ningxia|宁夏|🇨🇳
Qinghai|青海|🇨🇳
Gansu|甘肃|🇨🇳
Yunnan|云南|🇨🇳
Guizhou|贵州|🇨🇳
Hainan|海南|🇨🇳
Taipei|台北|🇹🇼
Kaohsiung|高雄|🇹🇼
Taichung|台中|🇹🇼
Tainan|台南|🇹🇼
Hsinchu|新竹|🇹🇼
Keelung|基隆|🇹🇼
Chiayi|嘉义|🇹🇼
Taoyuan|桃园|🇹🇼
Changhua|彰化|🇹🇼
Pingtung|屏东|🇹🇼
Yilan|宜兰|🇹🇼
Hualien|花莲|🇹🇼
Taitung|台东|🇹🇼
Penghu|澎湖|🇹🇼
Kinmen|金门|🇹🇼
Matsu|马祖|🇹🇼
New York|纽约|🇺🇸
NYC|纽约|🇺🇸
Los Angeles|洛杉矶|🇺🇸
LA|洛杉矶|🇺🇸
San Francisco|旧金山|🇺🇸
SF|旧金山|🇺🇸
Chicago|芝加哥|🇺🇸
Washington|华盛顿|🇺🇸
DC|华盛顿特区|🇺🇸
Seattle|西雅图|🇺🇸
Boston|波士顿|🇺🇸
Miami|迈阿密|🇺🇸
Atlanta|亚特兰大|🇺🇸
Houston|休斯顿|🇺🇸
Dallas|达拉斯|🇺🇸
Phoenix|凤凰城|🇺🇸
Philadelphia|费城|🇺🇸
San Diego|圣地亚哥|🇺🇸
Las Vegas|拉斯维加斯|🇺🇸
Denver|丹佛|🇺🇸
Portland|波特兰|🇺🇸
Austin|奥斯汀|🇺🇸
Nashville|纳什维尔|🇺🇸
Detroit|底特律|🇺🇸
San Jose|圣何塞|🇺🇸
Texas|德克萨斯|🇺🇸
California|加利福尼亚|🇺🇸
Florida|佛罗里达|🇺🇸
New York State|纽约州|🇺🇸
Illinois|伊利诺伊|🇺🇸
Pennsylvania|宾夕法尼亚|🇺🇸
Ohio|俄亥俄|🇺🇸
Georgia|乔治亚|🇺🇸
Michigan|密歇根|🇺🇸
Massachusetts|马萨诸塞|🇺🇸
Virginia|弗吉尼亚|🇺🇸
Colorado|科罗拉多|🇺🇸
Oregon|俄勒冈|🇺🇸
Washington State|华盛顿州|🇺🇸
Toronto|多伦多|🇨🇦
Vancouver|温哥华|🇨🇦
Montreal|蒙特利尔|🇨🇦
Ottawa|渥太华|🇨🇦
Calgary|卡尔加里|🇨🇦
Edmonton|埃德蒙顿|🇨🇦
London|伦敦|🇬🇧
Manchester|曼彻斯特|🇬🇧
Birmingham|伯明翰|🇬🇧
Liverpool|利物浦|🇬🇧
Edinburgh|爱丁堡|🏴󠁧󠁢󠁳󠁣󠁴󠁿
Glasgow|格拉斯哥|🏴󠁧󠁢󠁳󠁣󠁴󠁿
Oxford|牛津|🇬🇧
Cambridge|剑桥|🇬🇧
Tokyo|东京|🇯🇵
Osaka|大阪|🇯🇵
Kyoto|京都|🇯🇵
Yokohama|横滨|🇯🇵
Nagoya|名古屋|🇯🇵
Sapporo|札幌|🇯🇵
Fukuoka|福冈|🇯🇵
Kobe|神户|🇯🇵
Hiroshima|广岛|🇯🇵
Nara|奈良|🇯🇵
Seoul|首尔|🇰🇷
Busan|釜山|🇰🇷
Incheon|仁川|🇰🇷
Daegu|大邱|🇰🇷
Daejeon|大田|🇰🇷
Bangkok|曼谷|🇹🇭
Hanoi|河内|🇻🇳
Ho Chi Minh|胡志明市|🇻🇳
Saigon|西贡|🇻🇳
Jakarta|雅加达|🇮🇩
Manila|马尼拉|🇵🇭
Kuala Lumpur|吉隆坡|🇲🇾
Yangon|仰光|🇲🇲
Phnom Penh|金边|🇰🇭
Mumbai|孟买|🇮🇳
Delhi|德里|🇮🇳
Bangalore|班加罗尔|🇮🇳
Kolkata|加尔各答|🇮🇳
Chennai|金奈|🇮🇮🇳
Hyderabad|海得拉巴|🇮🇳
Karachi|卡拉奇|🇵🇰
Islamabad|伊斯兰堡|🇵🇰
Dhaka|达卡|🇧🇩
Kathmandu|加德满都|🇳🇵
Dubai|迪拜|🇦🇪
Abu Dhabi|阿布扎比|🇦🇪
Riyadh|利雅得|🇸🇦
Doha|多哈|🇶🇦
Tel Aviv|特拉维夫|🇮🇱
Jerusalem|耶路撒冷|🇮🇱
Tehran|德黑兰|🇮🇷
Baghdad|巴格达|🇮🇶
Damascus|大马士革|🇸🇾
Beirut|贝鲁特|🇱🇧
Istanbul|伊斯坦布尔|🇹🇷
Ankara|安卡拉|🇹🇷
Paris|巴黎|🇫🇷
Marseille|马赛|🇫🇷
Lyon|里昂|🇫🇷
Nice|尼斯|🇫🇷
Berlin|柏林|🇩🇪
Munich|慕尼黑|🇩🇪
Frankfurt|法兰克福|🇩🇪
Hamburg|汉堡|🇩🇪
Cologne|科隆|🇩🇪
Rome|罗马|🇮🇹
Milan|米兰|🇮🇹
Venice|威尼斯|🇮🇹
Florence|佛罗伦萨|🇮🇹
Naples|那不勒斯|🇮🇹
Madrid|马德里|🇪🇸
Barcelona|巴塞罗那|🇪🇸
Valencia|瓦伦西亚|🇪🇸
Seville|塞维利亚|🇪🇸
Amsterdam|阿姆斯特丹|🇳🇱
Rotterdam|鹿特丹|🇳🇱
Brussels|布鲁塞尔|🇧🇪
Zurich|苏黎世|🇨🇭
Geneva|日内瓦|🇨🇭
Vienna|维也纳|🇦🇹
Stockholm|斯德哥尔摩|🇸🇪
Oslo|奥斯陆|🇳🇴
Copenhagen|哥本哈根|🇩🇰
Helsinki|赫尔辛基|🇫🇮
Warsaw|华沙|🇵🇱
Prague|布拉格|🇨🇿
Budapest|布达佩斯|🇭🇺
Bucharest|布加勒斯特|🇷🇴
Athens|雅典|🇬🇷
Lisbon|里斯本|🇵🇹
Dublin|都柏林|🇮🇪
Moscow|莫斯科|🇷🇺
St Petersburg|圣彼得堡|🇷🇺
Kyiv|基辅|🇺🇦
Kiev|基辅|🇺🇦
Mexico City|墨西哥城|🇲🇽
Sao Paulo|圣保罗|🇧🇷
Rio de Janeiro|里约热内卢|🇧🇷
Buenos Aires|布宜诺斯艾利斯|🇦🇷
Santiago|圣地亚哥|🇨🇱
Bogota|波哥大|🇨🇴
Lima|利马|🇵🇪
Caracas|加拉加斯|🇻🇪
Havana|哈瓦那|🇨🇺
Sydney|悉尼|🇦🇺
Melbourne|墨尔本|🇦🇺
Brisbane|布里斯班|🇦🇺
Perth|珀斯|🇦🇺
Adelaide|阿德莱德|🇦🇺
Auckland|奥克兰|🇳🇿
Wellington|惠灵顿|🇳🇿
Cairo|开罗|🇪🇬
Cape Town|开普敦|🇿🇦
Johannesburg|约翰内斯堡|🇿🇦
Lagos|拉各斯|🇳🇬
Nairobi|内罗毕|🇰🇪
Addis Ababa|亚的斯亚贝巴|🇪🇹
Casablanca|卡萨布兰卡|🇲🇦
Algiers|阿尔及尔|🇩🇿
    `.trim();

    function parseTranslations() {
      const translations = {};
      const lines = LOCATION_TRANSLATIONS_EXTENDED.split('\n');

      for (const line of lines) {
        const parts = line.split('|');
        if (parts.length === 3) {
          const [en, cn, flag] = parts;
          translations[en] = { cn, flag };
        }
      }
      return translations;
    }
    const PARSED_TRANSLATIONS = parseTranslations();
    console.log('[Inty-Wumao] 加载了', Object.keys(PARSED_TRANSLATIONS).length, '个位置翻译');


    // 2. Inject interceptor.js content into the main world
    const interceptorScript = document.createElement('script');
    interceptorScript.textContent = `
(function() {
  console.log('[Interceptor] 🎯 Started');

  const found = new Map();

  // Intercept fetch
  const originalFetch = window.fetch;
  window.fetch = async function(...args) {
    const response = await originalFetch.apply(this, args);
    const url = args[0];
    
    if (typeof url === 'string' && url.includes('/i/api/graphql/')) {
      try {
        const clone = response.clone();
        const data = await clone.json();
        extractLocations(data);
      } catch (e) {}
    }
    
    return response;
  };

  // Intercept XMLHttpRequest
  const originalOpen = XMLHttpRequest.prototype.open;
  const originalSend = XMLHttpRequest.prototype.send;

  XMLHttpRequest.prototype.open = function(method, url, ...rest) {
    this._url = url;
    return originalOpen.apply(this, [method, url, ...rest]);
  };

  XMLHttpRequest.prototype.send = function(...args) {
    this.addEventListener('load', function() {
      if (this._url && this._url.includes('/i/api/graphql/')) {
        try {
          const data = JSON.parse(this.responseText);
          extractLocations(data);
        } catch (e) {}
      }
    });
    return originalSend.apply(this, args);
  };

  function extractLocations(obj, depth = 0) {
    if (depth > 20 || !obj || typeof obj !== 'object') return;

    // Find user objects
    if (obj.legacy?.screen_name) {
      const username = obj.legacy.screen_name;
      const location = obj.legacy.location || obj.about_profile?.account_based_in;

      // Silently detect users

      if (location && typeof location === 'string' && location.trim()) {
        const loc = location.trim();
        const key = username.toLowerCase();

        if (found.get(key) !== loc) {
          found.set(key, loc);
          window.postMessage({
            type: 'INTY_WUMAO_LOCATION_INTERCEPTED',
            username: username,
            location: loc
          }, '*');
          console.log('[Interceptor] ✅ Found location:', username, '→', loc);
        }
      }
    }

    // Recurse
    for (const key in obj) {
      if (obj.hasOwnProperty(key)) {
        extractLocations(obj[key], depth + 1);
      }
    }
  }

  console.log('[Interceptor] ✅ Ready');
})();
    `;
    document.documentElement.appendChild(interceptorScript);
    interceptorScript.remove(); // Clean up script tag

    // 3. background.js logic integration (for active API query)
    const CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 小时

    function isValidUsername(username) {
      if (!username || typeof username !== 'string') return false;
      return /^[a-zA-Z0-9_]{1,15}$/.test(username);
    }

    function sanitizeLocation(location) {
      if (!location || typeof location !== 'string') return null;
      const trimmed = location.trim();
      if (trimmed.length < 1 || trimmed.length > 100) return null;
      if (['null', 'undefined', 'N/A', 'n/a'].includes(trimmed.toLowerCase())) return null;
      return trimmed;
    }

    async function getCachedLocation(username) {
      const cached = await GM_getValue(`loc_${username}`, null);
      if (cached && cached.timestamp && typeof cached.timestamp === 'number') {
        if (Date.now() - cached.timestamp < CACHE_DURATION) {
          const sanitized = sanitizeLocation(cached.location);
          return sanitized;
        }
      }
      return null;
    }

    async function cacheLocation(username, location) {
      const sanitized = sanitizeLocation(location);
      if (!sanitized || !isValidUsername(username)) {
        return;
      }
      await GM_setValue(`loc_${username}`, {
        location: sanitized,
        timestamp: Date.now()
      });
    }

    async function getCsrfTokenFromCookies() {
      return new Promise(resolve => {
        GM_xmlhttpRequest({
          method: "GET",
          url: "https://x.com", // Or any twitter.com domain
          onload: function(response) {
            // GM_xmlhttpRequest might not expose Set-Cookie headers in `responseHeaders` for security reasons
            // Rely on document.cookie for same-origin cookies, though it might not include httpOnly cookies
            const docCookies = document.cookie.split('; ');
            const ct0FromDoc = docCookies.find(c => c.startsWith('ct0='));
            if (ct0FromDoc) {
                resolve(ct0FromDoc.substring(4)); // "ct0=".length is 4
                return;
            }
            resolve(null);
          },
          onerror: function() {
            resolve(null);
          }
        });
      });
    }
    
    // Fallback for getting CSRF from document.cookie directly, less reliable for Tampermonkey's isolation
    // Userscript usually runs in a sandbox, so document.cookie might not contain httpOnly cookies.
    // The GM_xmlhttpRequest method is preferred, but this can serve as a secondary attempt.
    function getCsrfFromDocumentCookie() {
        const docCookies = document.cookie.split('; ');
        const ct0FromDoc = docCookies.find(c => c.startsWith('ct0='));
        if (ct0FromDoc) {
            return ct0FromDoc.substring(4);
        }
        return null;
    }


    async function fetchLocationFromAPI(username) {
      try {
        if (!isValidUsername(username)) {
          return null;
        }

        const cached = await getCachedLocation(username);
        if (cached) {
          return cached;
        }
        
        // Try getting token via GM_xmlhttpRequest, then fallback to document.cookie (less reliable)
        let csrfToken = await getCsrfTokenFromCookies();
        if (!csrfToken) {
             csrfToken = getCsrfFromDocumentCookie(); // Fallback
        }
        
        if (!csrfToken) {
          console.log('[X-Buddy] 无法获取 CSRF Token');
          return null;
        }

        const queryId = 'XRqGa7EeokUU5kppkh13EA'; // This might change and need update
        const variables = JSON.stringify({ screenName: username });
        const url = `https://x.com/i/api/graphql/${queryId}/AboutAccountQuery?variables=${encodeURIComponent(variables)}`;

        return new Promise(resolve => {
            GM_xmlhttpRequest({
                method: "GET",
                url: url,
                headers: {
                    'accept': '*/*',
                    'authorization': 'Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA',
                    'content-type': 'application/json',
                    'x-csrf-token': csrfToken,
                    'x-twitter-active-user': 'yes',
                    'x-twitter-auth-type': 'OAuth2Session',
                    'x-twitter-client-language': 'en',
                },
                // credentials: 'include' is handled by Tampermonkey automatically including cookies for same-origin
                onload: async function(response) {
                    if (response.status !== 200) {
                        if (response.status === 429) {
                            console.error(`[X-Buddy] ⚠️ 速率限制!API 请求过于频繁,请稍后再试`);
                            console.error(`[X-Buddy] 建议:等待几分钟后刷新页面`);
                        } else {
                            console.error(`[X-Buddy] API 请求失败: ${response.status}`);
                            console.error(`[X-Buddy] 错误详情:`, response.responseText.substring(0, 200));
                        }
                        console.error(`[X-Buddy] 请求的用户名:`, username);
                        resolve(null);
                        return;
                    }

                    try {
                        const data = JSON.parse(response.responseText);
                        console.log(`[X-Buddy] ✅ API 响应成功:`, username);
                        console.log(`[X-Buddy] 📦 完整数据:`, data);

                        const userResult = data?.data?.user_result_by_screen_name?.result;
                        console.log(`[X-Buddy] 👤 User result:`, userResult);

                        let location = userResult?.about_profile?.account_based_in;
                        console.log(`[X-Buddy] 🔍 Path 1 (about_profile.account_based_in):`, location);

                        if (!location) {
                            location = userResult?.legacy?.location;
                            console.log(`[X-Buddy] 🔍 Path 2 (legacy.location):`, location);
                        }

                        if (location && typeof location === 'string' && location.trim().length > 0) {
                            const sanitized = sanitizeLocation(location);
                            if (sanitized) {
                                console.log(`[X-Buddy] ✅ 获取到位置:`, username, '→', sanitized);
                                await cacheLocation(username, sanitized);
                                resolve(sanitized);
                                return;
                            }
                        }

                        console.log(`[X-Buddy] ❌ 用户无位置信息:`, username);
                        console.log(`[X-Buddy] 📄 API 返回数据结构:`, JSON.stringify(data).substring(0, 500));
                        resolve(null);

                    } catch (parseError) {
                        console.error('[X-Buddy] 解析 API 响应失败:', parseError);
                        resolve(null);
                    }
                },
                onerror: function(error) {
                    console.error('[X-Buddy] GM_xmlhttpRequest 请求失败:', error);
                    resolve(null);
                }
            });
        });

      } catch (error) {
        console.error('[X-Buddy] 获取位置失败:', error);
        return null;
      }
    }

    async function clearOldCache() {
      const allKeys = await GM_listValues(); // Tampermonkey API to list all stored keys
      const keysToRemove = [];
      const now = Date.now();

      for (const key of allKeys) {
        if (key.startsWith('loc_')) {
          const value = await GM_getValue(key, null);
          if (value && typeof value === 'object' && value.timestamp && typeof value.timestamp === 'number') {
            if (now - value.timestamp > CACHE_DURATION) {
              keysToRemove.push(key);
            }
          }
        }
      }

      for (const key of keysToRemove) {
        await GM_deleteValue(key);
      }
      if (keysToRemove.length > 0) {
        console.log(`[X-Buddy] 清理了 ${keysToRemove.length} 个过期缓存`);
      }
    }

    // 启动时清理旧缓存
    clearOldCache();

    // 每小时定期清理一次
    setInterval(clearOldCache, 60 * 60 * 1000);
    console.log('[X-Buddy] 后台功能已启动 (Tampermonkey)');


    // 4. content/content.js logic integration
    console.log('[X-Location] 🚀 Content script loaded (Viewport-aware mode)');

    const cache = new Map();
    const processed = new Map();
    let requestCount = 0;
    const MAX_REQUESTS_PER_MINUTE = 40;
    const RETRY_FAILED_AFTER = 60000;

    setInterval(() => {
      const now = Date.now();
      let cleared = 0;
      for (const [id, timestamp] of processed.entries()) {
        if (now - timestamp > RETRY_FAILED_AFTER) {
          processed.delete(id);
          cleared++;
        }
      }
      if (cleared > 0) {
        console.log(`[X-Location] 🧹 Cleared ${cleared} expired processed items`);
      }
    }, 10000);
    
    setInterval(() => { requestCount = 0; }, 60000);

    // Listen to interceptor
    window.addEventListener('message', (e) => {
      if (e.source !== window) return;
      if (e.data.type !== 'INTY_WUMAO_LOCATION_INTERCEPTED') return;

      const { username, location } = e.data;
      if (username && location) {
        cache.set(username.toLowerCase(), location);
        console.log('[X-Location] 📥 Intercepted:', username, '→', location);
        updateUI(username, location);
      }
    });

    async function getLocation(username) {
      const key = username.toLowerCase();

      if (cache.has(key)) {
        return cache.get(key);
      }

      // Check storage (using GM_getValue)
      const stored = await GM_getValue(`loc_${key}`, null);
      if (stored?.location) {
        const loc = stored.location;
        cache.set(key, loc);
        return loc;
      }

      if (requestCount >= MAX_REQUESTS_PER_MINUTE) {
        console.warn('[X-Location] ⛔ Rate limit reached:', requestCount, '/', MAX_REQUESTS_PER_MINUTE);
        return null;
      }

      try {
        requestCount++;
        console.log('[X-Location] 🌐 API request:', username, `(${requestCount}/${MAX_REQUESTS_PER_MINUTE})`);
        
        // Direct call to fetchLocationFromAPI
        const location = await fetchLocationFromAPI(username);

        if (location) {
          cache.set(key, location);
          await cacheLocation(username, location); // Cache after successful API fetch
          console.log('[X-Location] ✅', username, '→', location);
          return location;
        }
      } catch (e) {
        console.error('[X-Location] ⚠️ API error:', e.message);
      }

      return null;
    }

    function extractUsername(tweet) {
      try {
        const selectors = [
          'a[href^="/"][role="link"]',
          '[data-testid="User-Name"] a',
          'a[role="link"]'
        ];

        for (const selector of selectors) {
          const links = tweet.querySelectorAll(selector);
          for (const link of links) {
            const href = link.getAttribute('href');
            if (!href) continue;
            const match = href.match(`^/([a-zA-Z0-9_]{1,15})(?:/|$)`);
            if (match) {
              const username = match[1];
              const blacklist = ['home', 'notifications', 'messages', 'explore', 'compose', 'i', 'search', 'settings'];
              if (!blacklist.includes(username)) {
                return username;
              }
            }
          }
        }
      } catch (e) {
        console.error('[X-Location] Extract error:', e);
      }
      return null;
    }

    function translate(location) {
      const translations = PARSED_TRANSLATIONS || {}; // Use local constant
      for (const [eng, obj] of Object.entries(translations)) {
        if (location.includes(eng)) {
          return { text: location.replace(eng, obj.cn), flag: obj.flag };
        }
      }
      return { text: location, flag: '' };
    }

    function createBadge(location) {
      const { text, flag } = translate(location);
      const badge = document.createElement('span');
      badge.className = 'inty-wumao-location-badge';
      badge.textContent = (flag || '📍') + ' ' + text;

      const lower = location.toLowerCase();
      if (lower.includes('china') || lower.includes('beijing') || lower.includes('中国')) {
        badge.classList.add('china-user');
      } else if (lower.includes('taiwan') || lower.includes('台湾')) {
        badge.classList.add('taiwan-user');
      }
      return badge;
    }

    function isVisible(element) {
      const rect = element.getBoundingClientRect();
      const windowHeight = window.innerHeight || document.documentElement.clientHeight;
      const windowWidth = window.innerWidth || document.documentElement.clientWidth;

      return (
        rect.top < windowHeight &&
        rect.bottom > 0 &&
        rect.left < windowWidth &&
        rect.right > 0
      );
    }

    async function processTweet(tweet) {
      if (!isVisible(tweet)) {
        return;
      }

      const username = extractUsername(tweet);
      if (!username) {
        return;
      }

      const container = tweet.querySelector('[data-testid="User-Name"]');
      if (!container) {
        return;
      }

      if (container.querySelector('.inty-wumao-badge-container')) {
        return;
      }

      if (container.dataset.intyWumaoProcessed) {
        return;
      }
      container.dataset.intyWumaoProcessed = 'processing';

      console.log('[X-Location] 🔍 Processing:', username);
      const location = await getLocation(username);

      if (!location) {
        container.dataset.intyWumaoProcessed = 'failed';
        return;
      }

      container.dataset.intyWumaoProcessed = 'success';

      const wrapper = document.createElement('div');
      wrapper.className = 'inty-wumao-badge-container';
      wrapper.appendChild(createBadge(location));
      container.appendChild(wrapper);
      console.log('[X-Location] ✅ Badge added:', username, '→', location);
    }

    function updateUI(username, location) {
      document.querySelectorAll('article[data-testid="tweet"]').forEach(tweet => {
        const tweetUser = extractUsername(tweet);
        if (tweetUser && tweetUser.toLowerCase() === username.toLowerCase()) {
          const container = tweet.querySelector('[data-testid="User-Name"]');
          if (container && !container.querySelector('.inty-wumao-badge-container') && !container.dataset.intyWumaoProcessed) {
            container.dataset.intyWumaoProcessed = 'success';
            const wrapper = document.createElement('div');
            wrapper.className = 'inty-wumao-badge-container';
            wrapper.appendChild(createBadge(location));
            container.appendChild(wrapper);
            console.log('[X-Location] ✅ Badge added via interceptor:', username, '→', location);
          }
        }
      });
    }

    function scan() {
      const tweets = document.querySelectorAll('article[data-testid="tweet"]');
      const visibleTweets = Array.from(tweets).filter(isVisible);

      if (visibleTweets.length === 0) return;

      visibleTweets.forEach((tweet, index) => {
        processTweet(tweet);
      });
    }

    function init() {
      const observer = new MutationObserver(() => {
        const tweets = document.querySelectorAll('article[data-testid="tweet"]');
        const visibleTweets = Array.from(tweets).filter(isVisible);
        visibleTweets.forEach(processTweet);
      });

      if (document.body) {
        observer.observe(document.body, { childList: true, subtree: true });
      } else {
        setTimeout(init, 100);
        return;
      }

      setTimeout(scan, 2000);
      setInterval(scan, 10000);

      let scrollTimeout;
      window.addEventListener('scroll', () => {
        clearTimeout(scrollTimeout);
        scrollTimeout = setTimeout(scan, 300);
      }, { passive: true });

      console.log('[X-Location] ✅ Ready');
    }

    if (document.readyState === 'loading') {
      document.addEventListener('DOMContentLoaded', init);
    } else {
      setTimeout(init, 100);
    }

})();