package rules import ( "encoding/json" "os" "path/filepath" "strings" "vpnem/internal/models" ) type Store struct { dataDir string connections *ConnectionStore } func NewStore(dataDir string) *Store { s := &Store{ dataDir: dataDir, connections: NewConnectionStore(dataDir), } _ = s.connections.Load() return s } // Connections returns the connection store for recommendation logic. func (s *Store) Connections() *ConnectionStore { return s.connections } func (s *Store) LoadServers() (*models.ServersResponse, error) { data, err := os.ReadFile(filepath.Join(s.dataDir, "servers.json")) if err != nil { return nil, err } var resp models.ServersResponse if err := json.Unmarshal(data, &resp); err != nil { return nil, err } return &resp, nil } func (s *Store) LoadRuleSets() (*models.RuleSetManifest, error) { data, err := os.ReadFile(filepath.Join(s.dataDir, "rulesets.json")) if err != nil { return nil, err } var manifest models.RuleSetManifest if err := json.Unmarshal(data, &manifest); err != nil { return nil, err } return &manifest, nil } func (s *Store) LoadVersion() (*models.VersionResponse, error) { data, err := os.ReadFile(filepath.Join(s.dataDir, "version.json")) if err != nil { return nil, err } var ver models.VersionResponse if err := json.Unmarshal(data, &ver); err != nil { return nil, err } return &ver, nil } func (s *Store) LoadCatalogV2() (*models.CatalogV2, error) { data, err := os.ReadFile(filepath.Join(s.dataDir, "catalog-v2.json")) if err != nil { return nil, err } var catalog models.CatalogV2 if err := json.Unmarshal(data, &catalog); err != nil { return nil, err } return &catalog, nil } func (s *Store) LoadCatalogV2OrLegacy() (*models.CatalogV2, error) { catalog, err := s.LoadCatalogV2() if err == nil { return catalog, nil } if !os.IsNotExist(err) { return nil, err } servers, err := s.LoadServers() if err != nil { return nil, err } return legacyServersToCatalog(servers.Servers), nil } func (s *Store) LoadRoutingPolicy() (*models.RoutingPolicy, error) { data, err := os.ReadFile(filepath.Join(s.dataDir, "routing-policy.json")) if err != nil { return nil, err } var policy models.RoutingPolicy if err := json.Unmarshal(data, &policy); err != nil { return nil, err } return &policy, nil } func (s *Store) RulesDir() string { return filepath.Join(s.dataDir, "rules") } func (s *Store) ReleasesDir() string { return filepath.Join(s.dataDir, "releases") } func (s *Store) DataDir() string { return s.dataDir } func legacyServersToCatalog(servers []models.Server) *models.CatalogV2 { nodesByID := make(map[string]*models.CatalogNode, len(servers)) order := make([]string, 0, len(servers)) for _, server := range servers { nodeID := server.Tag if existingID, protocolType, ok := splitLegacyTag(server.Tag); ok && existingID != "" { nodeID = existingID server.Type = protocolType } node := nodesByID[nodeID] if node == nil { node = &models.CatalogNode{ ID: nodeID, Name: nodeID, Region: server.Region, Host: server.Server, PublicHost: server.Server, Status: "published", } nodesByID[nodeID] = node order = append(order, nodeID) } node.Protocols = append(node.Protocols, legacyServerToCatalogProtocol(server)) } nodes := make([]models.CatalogNode, 0, len(order)) for _, id := range order { nodes = append(nodes, *nodesByID[id]) } return &models.CatalogV2{ Version: "legacy-adapter", Nodes: nodes, } } func legacyServerToCatalogProtocol(server models.Server) models.CatalogProtocol { protocolType := server.Type if protocolType == "socks" { protocolType = "socks5" } protocol := models.CatalogProtocol{ Type: protocolType, Enabled: true, Port: server.ServerPort, TLS: server.TLS, Extra: make(map[string]any), } switch server.Type { case "vless", "vless-reality", "vmess": protocol.Auth = &models.CatalogAuth{UUID: server.UUID} protocol.Extra["legacy_tag"] = server.Tag if server.Transport != nil { protocol.Extra["transport_type"] = server.Transport.Type if server.Transport.Path != "" { protocol.Extra["path"] = server.Transport.Path } } case "shadowsocks": protocol.Auth = &models.CatalogAuth{Method: server.Method, Password: server.Password} protocol.Extra["legacy_tag"] = server.Tag case "hysteria2": protocol.Auth = &models.CatalogAuth{Password: server.Password} protocol.Extra["legacy_tag"] = server.Tag if server.ObfsPassword != "" { protocol.Extra["obfs_password"] = server.ObfsPassword } if server.UpMbps > 0 { protocol.Extra["up_mbps"] = server.UpMbps } if server.DownMbps > 0 { protocol.Extra["down_mbps"] = server.DownMbps } case "socks": protocol.Extra["legacy_tag"] = server.Tag protocol.Extra["udp_over_tcp"] = server.UDPOverTCP } if len(protocol.Extra) == 0 { protocol.Extra = nil } return protocol } func splitLegacyTag(tag string) (nodeID, protocolType string, ok bool) { for _, candidate := range []string{"vless-reality", "vless", "vmess", "shadowsocks", "hysteria2", "socks", "socks5"} { suffix := "-" + candidate if strings.HasSuffix(tag, suffix) && len(tag) > len(suffix) { return strings.TrimSuffix(tag, suffix), candidate, true } } return "", "", false }