This repository has been archived on 2025-11-19. You can view files and clone it, but cannot push or open issues or pull requests.
Skyfeed_archive/internal/geo/geolocate.go

114 lines
2.8 KiB
Go

package geo
import (
"encoding/json"
"fmt"
"net"
"net/http"
"path/filepath"
"time"
"git.leaktechnologies.dev/Leak_Technologies/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)
}