114 lines
2.8 KiB
Go
114 lines
2.8 KiB
Go
package geo
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"github.com/leaktechnologies/skyfeed/internal/config"
|
|
"github.com/oschwald/geoip2-golang"
|
|
)
|
|
|
|
// GetUserLocation resolves the user's public IP into a city and coordinates.
|
|
// It will try multiple fallback IP providers if the first one fails.
|
|
func GetUserLocation() (string, float64, float64, error) {
|
|
fmt.Println("[geo] Detecting location...")
|
|
|
|
ip, err := fetchPublicIP()
|
|
if err != nil {
|
|
return "", 0, 0, fmt.Errorf("failed to resolve public IP: %w", err)
|
|
}
|
|
|
|
dbPath := filepath.Join(config.DataDir, "GeoLite2-City.mmdb")
|
|
db, err := geoip2.Open(dbPath)
|
|
if err != nil {
|
|
return "", 0, 0, fmt.Errorf("failed to open GeoLite2 database: %w", err)
|
|
}
|
|
defer db.Close()
|
|
|
|
record, err := db.City(ip)
|
|
if err != nil {
|
|
return "", 0, 0, fmt.Errorf("geoip lookup failed: %w", err)
|
|
}
|
|
|
|
city := record.City.Names["en"]
|
|
prov := ""
|
|
if len(record.Subdivisions) > 0 {
|
|
prov = record.Subdivisions[0].Names["en"]
|
|
}
|
|
|
|
lat := record.Location.Latitude
|
|
lon := record.Location.Longitude
|
|
|
|
if city == "" && prov == "" {
|
|
return "", 0, 0, fmt.Errorf("no location info found for IP %s", ip.String())
|
|
}
|
|
|
|
fmt.Printf("[geo] Detected: %s, %s (%.4f, %.4f)\n", city, prov, lat, lon)
|
|
return fmt.Sprintf("%s, %s", city, prov), lat, lon, nil
|
|
}
|
|
|
|
// fetchPublicIP tries multiple reliable endpoints for the public IPv4 address.
|
|
func fetchPublicIP() (net.IP, error) {
|
|
providers := []string{
|
|
"https://ipv4.icanhazip.com",
|
|
"https://api.ipify.org?format=json",
|
|
"https://ifconfig.co/json",
|
|
}
|
|
|
|
client := &http.Client{Timeout: 5 * time.Second}
|
|
|
|
for _, url := range providers {
|
|
ip, err := tryProvider(url, client)
|
|
if err == nil && ip != nil {
|
|
return ip, nil
|
|
}
|
|
fmt.Println("[geo] Fallback:", err)
|
|
}
|
|
|
|
return nil, fmt.Errorf("all IP detection methods failed")
|
|
}
|
|
|
|
// tryProvider queries a single IP API endpoint and parses IPv4 results.
|
|
func tryProvider(url string, client *http.Client) (net.IP, error) {
|
|
resp, err := client.Get(url)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("network error (%s): %w", url, err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("HTTP %d (%s)", resp.StatusCode, url)
|
|
}
|
|
|
|
// Some APIs return plain text, others JSON
|
|
var result struct {
|
|
IP string `json:"ip"`
|
|
}
|
|
|
|
// Try decode JSON
|
|
if err := json.NewDecoder(resp.Body).Decode(&result); err == nil && result.IP != "" {
|
|
ip := net.ParseIP(result.IP)
|
|
if ip != nil && ip.To4() != nil {
|
|
return ip, nil
|
|
}
|
|
}
|
|
|
|
// Fallback: plain text
|
|
resp2, err := client.Get(url)
|
|
if err == nil {
|
|
defer resp2.Body.Close()
|
|
buf := make([]byte, 64)
|
|
n, _ := resp2.Body.Read(buf)
|
|
ip := net.ParseIP(string(buf[:n]))
|
|
if ip != nil && ip.To4() != nil {
|
|
return ip, nil
|
|
}
|
|
}
|
|
|
|
return nil, fmt.Errorf("no valid IP found from %s", url)
|
|
}
|