package shared import ( "crypto/md5" "encoding/json" "fmt" "net/http" "strconv" "strings" "ds2api/internal/config" "ds2api/internal/util" ) var intFrom = util.IntFrom var WriteJSON = util.WriteJSON var IntFrom = util.IntFrom func ReverseAccounts(a []config.Account) { reverseAccounts(a) } func IntFromQuery(r *http.Request, key string, d int) int { return intFromQuery(r, key, d) } func NilIfEmpty(s string) any { return nilIfEmpty(s) } func NilIfZero(v int64) any { return nilIfZero(v) } func MaskSecretPreview(secret string) string { return maskSecretPreview(secret) } func ToStringSlice(v any) ([]string, bool) { return toStringSlice(v) } func ToAccount(m map[string]any) config.Account { return toAccount(m) } func ToAPIKeys(v any) ([]config.APIKey, bool) { return toAPIKeys(v) } func NormalizeAPIKeyForStorage(item config.APIKey) config.APIKey { return normalizeAPIKeyForStorage(item) } func APIKeyHasMetadata(item config.APIKey) bool { return apiKeyHasMetadata(item) } func MergeAPIKeysPreferStructured(existing, incoming []config.APIKey) ([]config.APIKey, int) { return mergeAPIKeysPreferStructured(existing, incoming) } func MergeAPIKeyRecord(existing, incoming config.APIKey) config.APIKey { return mergeAPIKeyRecord(existing, incoming) } func FieldString(m map[string]any, key string) string { return fieldString(m, key) } func FieldStringOptional(m map[string]any, key string) (string, bool) { return fieldStringOptional(m, key) } func StatusOr(v int, d int) int { return statusOr(v, d) } func AccountMatchesIdentifier(acc config.Account, identifier string) bool { return accountMatchesIdentifier(acc, identifier) } func NormalizeAccountForStorage(acc config.Account) config.Account { return normalizeAccountForStorage(acc) } func ToProxy(m map[string]any) config.Proxy { return toProxy(m) } func FindProxyByID(c config.Config, proxyID string) (config.Proxy, bool) { return findProxyByID(c, proxyID) } func AccountDedupeKey(acc config.Account) string { return accountDedupeKey(acc) } func NormalizeAndDedupeAccounts(accounts []config.Account) []config.Account { return normalizeAndDedupeAccounts(accounts) } func FindAccountByIdentifier(store ConfigStore, identifier string) (config.Account, bool) { return findAccountByIdentifier(store, identifier) } func ComputeSyncHash(store ConfigStore) string { if store == nil { return "" } snap := store.Snapshot().Clone() snap.ClearAccountTokens() snap.VercelSyncHash = "" snap.VercelSyncTime = 0 b, _ := json.Marshal(snap) sum := md5.Sum(b) return fmt.Sprintf("%x", sum) } func SyncHashForJSON(s string) string { var cfg config.Config if err := json.Unmarshal([]byte(s), &cfg); err != nil { return "" } cfg.VercelSyncHash = "" cfg.VercelSyncTime = 0 cfg.ClearAccountTokens() b, err := json.Marshal(cfg) if err != nil { return "" } sum := md5.Sum(b) return fmt.Sprintf("%x", sum) } func reverseAccounts(a []config.Account) { for i, j := 0, len(a)-1; i < j; i, j = i+1, j-1 { a[i], a[j] = a[j], a[i] } } func intFromQuery(r *http.Request, key string, d int) int { v := r.URL.Query().Get(key) if v == "" { return d } n, err := strconv.Atoi(v) if err != nil { return d } return n } func nilIfEmpty(s string) any { if s == "" { return nil } return s } func nilIfZero(v int64) any { if v == 0 { return nil } return v } func maskSecretPreview(secret string) string { secret = strings.TrimSpace(secret) if secret == "" { return "" } if len(secret) <= 4 { return strings.Repeat("*", len(secret)) } return secret[:2] + "****" + secret[len(secret)-2:] } func toStringSlice(v any) ([]string, bool) { arr, ok := v.([]any) if !ok { return nil, false } out := make([]string, 0, len(arr)) for _, item := range arr { out = append(out, strings.TrimSpace(fmt.Sprintf("%v", item))) } return out, true } func toAccount(m map[string]any) config.Account { email := fieldString(m, "email") mobile := config.NormalizeMobileForStorage(fieldString(m, "mobile")) return config.Account{ Name: fieldString(m, "name"), Remark: fieldString(m, "remark"), Email: email, Mobile: mobile, Password: fieldString(m, "password"), ProxyID: fieldString(m, "proxy_id"), } } func toAPIKeys(v any) ([]config.APIKey, bool) { arr, ok := v.([]any) if !ok { return nil, false } out := make([]config.APIKey, 0, len(arr)) seen := map[string]struct{}{} for _, item := range arr { switch x := item.(type) { case map[string]any: key := fieldString(x, "key") if key == "" { continue } if _, ok := seen[key]; ok { continue } seen[key] = struct{}{} out = append(out, config.APIKey{ Key: key, Name: fieldString(x, "name"), Remark: fieldString(x, "remark"), }) default: key := strings.TrimSpace(fmt.Sprintf("%v", item)) if key == "" { continue } if _, ok := seen[key]; ok { continue } seen[key] = struct{}{} out = append(out, config.APIKey{Key: key}) } } return out, true } func normalizeAPIKeyForStorage(item config.APIKey) config.APIKey { return config.APIKey{ Key: strings.TrimSpace(item.Key), Name: strings.TrimSpace(item.Name), Remark: strings.TrimSpace(item.Remark), } } func apiKeyHasMetadata(item config.APIKey) bool { return strings.TrimSpace(item.Name) != "" || strings.TrimSpace(item.Remark) != "" } func mergeAPIKeysPreferStructured(existing, incoming []config.APIKey) ([]config.APIKey, int) { if len(existing) == 0 && len(incoming) == 0 { return nil, 0 } merged := make([]config.APIKey, 0, len(existing)+len(incoming)) index := make(map[string]int, len(existing)+len(incoming)) for _, item := range existing { item = normalizeAPIKeyForStorage(item) if item.Key == "" { continue } if _, ok := index[item.Key]; ok { continue } index[item.Key] = len(merged) merged = append(merged, item) } imported := 0 for _, item := range incoming { item = normalizeAPIKeyForStorage(item) if item.Key == "" { continue } if idx, ok := index[item.Key]; ok { keep := merged[idx] next := mergeAPIKeyRecord(keep, item) if next != keep { merged[idx] = next imported++ } continue } index[item.Key] = len(merged) merged = append(merged, item) imported++ } if len(merged) == 0 { return nil, imported } return merged, imported } func mergeAPIKeyRecord(existing, incoming config.APIKey) config.APIKey { existing = normalizeAPIKeyForStorage(existing) incoming = normalizeAPIKeyForStorage(incoming) if existing.Key == "" { return incoming } if apiKeyHasMetadata(existing) { return existing } if apiKeyHasMetadata(incoming) { return incoming } return existing } func fieldString(m map[string]any, key string) string { v, ok := m[key] if !ok || v == nil { return "" } return strings.TrimSpace(fmt.Sprintf("%v", v)) } func fieldStringOptional(m map[string]any, key string) (string, bool) { v, ok := m[key] if !ok || v == nil { return "", false } return strings.TrimSpace(fmt.Sprintf("%v", v)), true } func statusOr(v int, d int) int { if v == 0 { return d } return v } func accountMatchesIdentifier(acc config.Account, identifier string) bool { id := strings.TrimSpace(identifier) if id == "" { return false } if strings.TrimSpace(acc.Email) == id { return true } if mobileKey := config.CanonicalMobileKey(id); mobileKey != "" && mobileKey == config.CanonicalMobileKey(acc.Mobile) { return true } return acc.Identifier() == id } func normalizeAccountForStorage(acc config.Account) config.Account { acc.Name = strings.TrimSpace(acc.Name) acc.Remark = strings.TrimSpace(acc.Remark) acc.Email = strings.TrimSpace(acc.Email) acc.Mobile = config.NormalizeMobileForStorage(acc.Mobile) acc.ProxyID = strings.TrimSpace(acc.ProxyID) return acc } func toProxy(m map[string]any) config.Proxy { return config.NormalizeProxy(config.Proxy{ ID: fieldString(m, "id"), Name: fieldString(m, "name"), Type: fieldString(m, "type"), Host: fieldString(m, "host"), Port: intFrom(m["port"]), Username: fieldString(m, "username"), Password: fieldString(m, "password"), }) } func findProxyByID(c config.Config, proxyID string) (config.Proxy, bool) { id := strings.TrimSpace(proxyID) if id == "" { return config.Proxy{}, false } for _, proxy := range c.Proxies { proxy = config.NormalizeProxy(proxy) if proxy.ID == id { return proxy, true } } return config.Proxy{}, false } func accountDedupeKey(acc config.Account) string { if email := strings.TrimSpace(acc.Email); email != "" { return "email:" + email } if mobile := config.CanonicalMobileKey(acc.Mobile); mobile != "" { return "mobile:" + mobile } if id := strings.TrimSpace(acc.Identifier()); id != "" { return "id:" + id } return "" } func normalizeAndDedupeAccounts(accounts []config.Account) []config.Account { if len(accounts) == 0 { return nil } out := make([]config.Account, 0, len(accounts)) seen := make(map[string]struct{}, len(accounts)) for _, acc := range accounts { acc = normalizeAccountForStorage(acc) key := accountDedupeKey(acc) if key == "" { continue } if _, ok := seen[key]; ok { continue } seen[key] = struct{}{} out = append(out, acc) } return out } func findAccountByIdentifier(store ConfigStore, identifier string) (config.Account, bool) { id := strings.TrimSpace(identifier) if id == "" { return config.Account{}, false } if acc, ok := store.FindAccount(id); ok { return acc, true } accounts := store.Snapshot().Accounts for _, acc := range accounts { if accountMatchesIdentifier(acc, id) { return acc, true } } return config.Account{}, false }