Files
rogee c73c2b55cd refactor: rename reads cached egress geo, fallback name-only (no DNS)
RenameCollectionNodes now reads geo info from cached egress-probe fields (country, countryCode, city, flag) on each node instead of doing per-node DNS lookups via DetectGeoWithServer.

When no cached egress data is available (probe not yet run), it falls back to name-based regex only (DetectGeo, no DNS) so the UI is never blocked. The background egress probe (triggered on source create/update) populates the cache asynchronously.

Also: runEgressProbe now stores countryCode and flag emoji from ipwho.is response. egressCacheKey and addCachedEgressInfo updated to include the new fields.

Reverted previous name-first hack on DetectGeoWithServer and DNS cache on geoip.LookupHost — no longer needed since rename doesn't call them.

Collection preview with rename: 30s -> 52ms (with cache miss, name-based fallback).
2026-07-30 19:02:25 +08:00

255 lines
7.0 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package util
import (
"net"
"regexp"
"strings"
"github.com/peterqiu0516/sub-store/internal/geoip"
)
// Flag detection rules: [regex, emoji, countryName, cityExtractor].
// Per review-resolution #16: Go RE2 supports \p{Regional_Indicator} and \uFE0F.
type geoRule struct {
re *regexp.Regexp
emoji string
countryName string
countryNameCN string
cityPatterns []cityPattern
}
type cityPattern struct {
re *regexp.Regexp
name string
nameCN string
}
var geoRules []geoRule
func init() {
rules := []struct {
pattern string
emoji string
countryName string
countryNameCN string
cities []struct{ pattern, name, nameCN string }
}{
{
pattern: `香港|港|hong\s*kong|\bhk\b`,
emoji: "🇭🇰",
countryName: "Hong Kong",
countryNameCN: "香港",
cities: []struct{ pattern, name, nameCN string }{
{`香港|Hong\s*Kong|HK`, "Hong Kong", "香港"},
},
},
{
pattern: `台湾|台灣|taiwan|\btw\b`,
emoji: "🇹🇼",
countryName: "Taiwan",
countryNameCN: "台湾",
cities: []struct{ pattern, name, nameCN string }{
{`台北|臺北|Taipei`, "Taipei", "台北"},
{`高雄|Gaoxiong|Kaohsiung`, "Kaohsiung", "高雄"},
},
},
{
pattern: `新加坡|狮城|獅城|singapore|\bsg\b`,
emoji: "🇸🇬",
countryName: "Singapore",
countryNameCN: "新加坡",
cities: []struct{ pattern, name, nameCN string }{
{`新加坡|Singapore|SG`, "Singapore", "新加坡"},
},
},
{
pattern: `日本|东京|東京|大阪|japan|tokyo|osaka|\bjp\b`,
emoji: "🇯🇵",
countryName: "Japan",
countryNameCN: "日本",
cities: []struct{ pattern, name, nameCN string }{
{`东京|東京|Tokyo`, "Tokyo", "东京"},
{`大阪|Osaka`, "Osaka", "大阪"},
},
},
{
pattern: `美国|美國|洛杉矶|洛杉磯|纽约|紐約|united\s*states|los\s*angeles|new\s*york|\bus\b|\busa\b`,
emoji: "🇺🇸",
countryName: "United States",
countryNameCN: "美国",
cities: []struct{ pattern, name, nameCN string }{
{`洛杉矶|洛杉磯|Los\s*Angeles|LA`, "Los Angeles", "洛杉矶"},
{`纽约|紐約|New\s*York|NYC`, "New York", "纽约"},
{`硅谷|Silicon\s*Valley|San\s*Jose|SJC`, "San Jose", "硅谷"},
{`西雅图|Seattle`, "Seattle", "西雅图"},
{`芝加哥|Chicago`, "Chicago", "芝加哥"},
{`达拉斯|Dallas`, "Dallas", "达拉斯"},
},
},
{
pattern: `英国|英國|伦敦|倫敦|united\s*kingdom|london|\buk\b`,
emoji: "🇬🇧",
countryName: "United Kingdom",
countryNameCN: "英国",
cities: []struct{ pattern, name, nameCN string }{
{`伦敦|倫敦|London`, "London", "伦敦"},
},
},
{
pattern: `德国|德國|法兰克福|法蘭克福|germany|frankfurt|\bde\b`,
emoji: "🇩🇪",
countryName: "Germany",
countryNameCN: "德国",
cities: []struct{ pattern, name, nameCN string }{
{`法兰克福|法蘭克福|Frankfurt`, "Frankfurt", "法兰克福"},
},
},
{
pattern: `韩国|韓國|首尔|首爾|korea|seoul|\bkr\b`,
emoji: "🇰🇷",
countryName: "South Korea",
countryNameCN: "韩国",
cities: []struct{ pattern, name, nameCN string }{
{`首尔|首爾|Seoul`, "Seoul", "首尔"},
},
},
}
for _, r := range rules {
re, err := regexp.Compile("(?i)" + r.pattern)
if err != nil {
continue
}
var cps []cityPattern
for _, c := range r.cities {
cre, err := regexp.Compile("(?i)" + c.pattern)
if err != nil {
continue
}
cps = append(cps, cityPattern{re: cre, name: c.name, nameCN: c.nameCN})
}
geoRules = append(geoRules, geoRule{
re: re,
emoji: r.emoji,
countryName: r.countryName,
countryNameCN: r.countryNameCN,
cityPatterns: cps,
})
}
}
// GeoInfo holds detected geographic information from a proxy name.
type GeoInfo struct {
Flag string // emoji flag
CountryName string // English country name
CountryCN string // Chinese country name
City string // English city name (may be empty)
CityCN string // Chinese city name (may be empty)
}
// DetectFlag returns the flag emoji for a name, or 🏳️ if no match.
func DetectFlag(name string) string {
text := strings.ToLower(name)
for _, rule := range geoRules {
if rule.re.MatchString(text) {
return rule.emoji
}
}
return "🏳️"
}
// DetectGeo returns full geographic info (flag + country + city) from a name.
// It tries GeoIP lookup on the server address first, falling back to
// name-based regex matching.
func DetectGeo(name string) GeoInfo {
return detectGeoFromName(name)
}
// DetectGeoWithServer returns geographic info by looking up the proxy's
// server address via GeoIP first, falling back to name-based detection.
func DetectGeoWithServer(name, server string) GeoInfo {
// Try GeoIP lookup first
if server != "" {
host := server
// Strip port if present
if h, _, err := net.SplitHostPort(server); err == nil {
host = h
}
if result := geoip.LookupHost(host); result != nil {
return GeoInfo{
Flag: geoip.CountryEmoji(result.CountryCode),
CountryName: result.CountryName,
CountryCN: result.CountryNameCN,
City: result.CityName,
CityCN: result.CityNameCN,
}
}
}
// Fallback to name-based detection
return detectGeoFromName(name)
}
// detectGeoFromName does name-based regex matching for geo detection.
func detectGeoFromName(name string) GeoInfo {
text := strings.ToLower(name)
for _, rule := range geoRules {
if rule.re.MatchString(text) {
city := ""
cityCN := ""
for _, cp := range rule.cityPatterns {
if cp.re.MatchString(text) {
city = cp.name
cityCN = cp.nameCN
break
}
}
return GeoInfo{
Flag: rule.emoji,
CountryName: rule.countryName,
CountryCN: rule.countryNameCN,
City: city,
CityCN: cityCN,
}
}
}
return GeoInfo{}
}
// removeFlagRe matches leading flag emoji sequences and whitespace.
// Regional Indicator letters are U+1F1E6U+1F1FF; ZWJ is U+200D; VS16 is U+FE0F.
var removeFlagRe = regexp.MustCompile(`^[\x{1F1E6}-\x{1F1FF}\x{FE0F}\x{200D}\s]+`)
// RemoveFlag strips leading flag emoji and whitespace from a name.
// Per review-resolution #16: handles ZWJ (\u200D) and variation selector (\uFE0F).
func RemoveFlag(name string) string {
cleaned := removeFlagRe.ReplaceAllString(name, "")
// Also strip leading 🏳️
cleaned = strings.TrimLeft(cleaned, "🏳️ ")
return strings.TrimSpace(cleaned)
}
// NormalizeTaiwanFlag maps the Taiwan flag to different flags based on `tw` mode.
// Per review-resolution #19: three modes — ws/tw/default.
func NormalizeTaiwanFlag(flag, mode string) string {
if flag != "🇹🇼" {
return flag
}
switch mode {
case "ws":
return "🇼🇸"
case "tw":
return "🇹🇼"
default:
return "🇨🇳"
}
}
// IsASCII checks if a string contains only ASCII characters.
func IsASCII(s string) bool {
for i := 0; i < len(s); i++ {
if s[i] > 127 {
return false
}
}
return true
}