platform: modularize api/gui, add docs-tests-web foundation, and refresh root config
This commit is contained in:
136
selective-vpn-api/app/vpn_locations_cache_parse.go
Normal file
136
selective-vpn-api/app/vpn_locations_cache_parse.go
Normal file
@@ -0,0 +1,136 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func parseVPNLocationsOutput(raw string) ([]vpnLocationItem, error) {
|
||||
stdout := stripANSI(raw)
|
||||
type parsedLocation struct {
|
||||
iso string
|
||||
label string
|
||||
name string
|
||||
}
|
||||
var parsed []parsedLocation
|
||||
|
||||
for _, ln := range strings.Split(stdout, "\n") {
|
||||
line := strings.TrimSpace(ln)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(line, "ISO ") {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(line, "VPN ") || strings.HasPrefix(line, "You can connect") {
|
||||
continue
|
||||
}
|
||||
|
||||
parts := strings.Fields(line)
|
||||
if len(parts) < 4 {
|
||||
continue
|
||||
}
|
||||
iso := strings.ToUpper(strings.TrimSpace(parts[0]))
|
||||
ping := strings.TrimSpace(parts[len(parts)-1])
|
||||
|
||||
if len(iso) != 2 {
|
||||
continue
|
||||
}
|
||||
if !isDigitsOnly(ping) {
|
||||
continue
|
||||
}
|
||||
|
||||
name := strings.Join(parts[1:len(parts)-1], " ")
|
||||
label := fmt.Sprintf("%s %s (%s ms)", iso, name, ping)
|
||||
parsed = append(parsed, parsedLocation{
|
||||
iso: iso,
|
||||
label: label,
|
||||
name: name,
|
||||
})
|
||||
}
|
||||
|
||||
if len(parsed) == 0 {
|
||||
trimmed := strings.TrimSpace(stdout)
|
||||
if trimmed == "" {
|
||||
return nil, fmt.Errorf("empty list-locations output")
|
||||
}
|
||||
return nil, fmt.Errorf("no locations parsed from output")
|
||||
}
|
||||
|
||||
namesByISO := map[string][][]string{}
|
||||
for _, it := range parsed {
|
||||
namesByISO[it.iso] = append(namesByISO[it.iso], strings.Fields(it.name))
|
||||
}
|
||||
commonPrefixByISO := map[string][]string{}
|
||||
for iso, names := range namesByISO {
|
||||
if len(names) < 2 {
|
||||
continue
|
||||
}
|
||||
pfx := commonPrefixTokens(names)
|
||||
if len(pfx) > 0 {
|
||||
commonPrefixByISO[iso] = pfx
|
||||
}
|
||||
}
|
||||
|
||||
var out []vpnLocationItem
|
||||
for _, it := range parsed {
|
||||
target := strings.ToUpper(strings.TrimSpace(it.iso))
|
||||
if pfx := commonPrefixByISO[it.iso]; len(pfx) > 0 {
|
||||
tokens := strings.Fields(it.name)
|
||||
if len(tokens) > len(pfx) {
|
||||
city := strings.TrimSpace(strings.Join(tokens[len(pfx):], " "))
|
||||
if city != "" {
|
||||
target = city
|
||||
}
|
||||
}
|
||||
}
|
||||
item, ok := normalizeVPNLocationItem(vpnLocationItem{
|
||||
Label: it.label,
|
||||
ISO: it.iso,
|
||||
Target: target,
|
||||
})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
out = append(out, item)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func commonPrefixTokens(items [][]string) []string {
|
||||
if len(items) == 0 {
|
||||
return nil
|
||||
}
|
||||
prefix := append([]string{}, items[0]...)
|
||||
for i := 1; i < len(items); i++ {
|
||||
row := items[i]
|
||||
n := len(prefix)
|
||||
if len(row) < n {
|
||||
n = len(row)
|
||||
}
|
||||
j := 0
|
||||
for j < n {
|
||||
if !strings.EqualFold(prefix[j], row[j]) {
|
||||
break
|
||||
}
|
||||
j++
|
||||
}
|
||||
prefix = prefix[:j]
|
||||
if len(prefix) == 0 {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return prefix
|
||||
}
|
||||
|
||||
func isDigitsOnly(s string) bool {
|
||||
if s == "" {
|
||||
return false
|
||||
}
|
||||
for _, ch := range s {
|
||||
if ch < '0' || ch > '9' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
Reference in New Issue
Block a user