Spaces:
Sleeping
Sleeping
| 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.ClearVercelCredentials() | |
| 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() | |
| cfg.ClearVercelCredentials() | |
| 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 | |
| } | |