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

149 lines
3.8 KiB
Go

package geo
import (
"encoding/json"
"fmt"
"net"
"net/http"
"strings"
"time"
)
// -------------------------------
// Public IP + Geolocation (ipwho.is)
// -------------------------------
// GetUserLocation resolves the user's IP into:
// city, latitude, longitude
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)
}
city, province, lat, lon, err := lookupIP(ip.String())
if err != nil {
return "", 0, 0, fmt.Errorf("IP geolocation failed: %w", err)
}
// e.g., "Ottawa, Ontario"
locationName := fmt.Sprintf("%s, %s", city, province)
fmt.Printf("[geo] Detected: %s (%.4f, %.4f)\n", locationName, lat, lon)
return locationName, lat, lon, nil
}
// -------------------------------
// PUBLIC IP DETECTION
// -------------------------------
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")
}
func tryProvider(url string, client *http.Client) (net.IP, error) {
resp, err := client.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("HTTP %d (%s)", resp.StatusCode, url)
}
// JSON-based API fallback
var j struct{ IP string `json:"ip"` }
if err := json.NewDecoder(resp.Body).Decode(&j); err == nil && j.IP != "" {
ip := net.ParseIP(j.IP)
if ip != nil && ip.To4() != nil {
return ip, nil
}
}
// Plain text fallback → must re-fetch body
resp2, err := client.Get(url)
if err != nil {
return nil, err
}
defer resp2.Body.Close()
buf := make([]byte, 64)
n, _ := resp2.Body.Read(buf)
ip := net.ParseIP(strings.TrimSpace(string(buf[:n])))
if ip != nil && ip.To4() != nil {
return ip, nil
}
return nil, fmt.Errorf("invalid response from %s", url)
}
// -------------------------------
// GEO LOOKUP USING ipwho.is
// -------------------------------
func lookupIP(ip string) (city string, province string, lat float64, lon float64, err error) {
url := fmt.Sprintf("https://ipwho.is/%s", ip)
client := &http.Client{Timeout: 6 * time.Second}
resp, err := client.Get(url)
if err != nil {
return "", "", 0, 0, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", "", 0, 0, fmt.Errorf("ipwho.is returned HTTP %d", resp.StatusCode)
}
var data struct {
Success bool `json:"success"`
City string `json:"city"`
Region string `json:"region"`
Lat float64 `json:"latitude"`
Lon float64 `json:"longitude"`
Message string `json:"message"`
}
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
return "", "", 0, 0, fmt.Errorf("decode error: %w", err)
}
if !data.Success {
return "", "", 0, 0, fmt.Errorf("ipwho.is error: %s", data.Message)
}
// Clean fields
city = strings.TrimSpace(data.City)
province = strings.TrimSpace(data.Region)
if city == "" {
city = "Unknown"
}
if province == "" {
province = "Unknown"
}
return city, province, data.Lat, data.Lon, nil
}