package service import ( "crypto/sha256" "encoding/json" "fmt" "github.com/peterqiu0516/sub-store/internal/database" "github.com/peterqiu0516/sub-store/internal/model" ) // egressCacheKeySkip lists node fields excluded from the egress cache key so // the key stays stable across pipeline stages (parse → filter → alias tag → // rename) and matches the key the background probe wrote. var egressCacheKeySkip = map[string]bool{ "id": true, "name": true, "_sourceAlias": true, "_previewId": true, "latencyMs": true, "latencyError": true, "egressIp": true, "egressCountry": true, "egressRegion": true, "egressError": true, "country": true, "countryCode": true, "region": true, "city": true, "isp": true, "flag": true, "cached": true, } // EgressCacheKey returns the source_cache key for a node's egress probe // result. Pushed down from the handler (HH-773) so the download pipeline // reads the exact entries the probe wrote. func EgressCacheKey(node model.ProxyNode) string { clean := model.ProxyNode{} for k, v := range node { if !egressCacheKeySkip[k] { clean[k] = v } } data, _ := json.Marshal(clean) sum := sha256.Sum256(data) return fmt.Sprintf("egress:%x", sum) } // egressGeoFields are the cached egress-probe geo fields merged into nodes // for collection-level renaming. var egressGeoFields = []string{"country", "countryCode", "region", "city", "flag"} // MergeCachedEgressGeo merges cached geo fields into nodes so collection // renaming can use probed geo data. Pure cache reads — no network I/O, no // probing inside the download pipeline. geoFromNode in the filter package // consumes the merged fields. func MergeCachedEgressGeo(repo *database.CacheRepo, nodes []model.ProxyNode) []model.ProxyNode { if repo == nil { return nodes } for _, node := range nodes { if node == nil { continue } entry, ok := repo.SafeGet(EgressCacheKey(node)) if !ok { continue } var cached map[string]any if json.Unmarshal([]byte(entry.Content), &cached) != nil { continue } for _, field := range egressGeoFields { if v, ok := cached[field]; ok && v != nil { node[field] = v } } } return nodes } // StripEgressGeoFields removes the merged geo fields after renaming so they // do not leak into rendered subscription output. No parser or filter sets // these fields natively (they exist on nodes only via the egress merge), so // stripping is safe. func StripEgressGeoFields(nodes []model.ProxyNode) { for _, node := range nodes { if node == nil { continue } for _, field := range egressGeoFields { delete(node, field) } } }