feat: social graph features

This commit is contained in:
Krispire Admin
2026-06-04 19:29:48 +05:30
parent d9678ebb3b
commit efa1eaf23b
28 changed files with 1882 additions and 78 deletions
+92
View File
@@ -0,0 +1,92 @@
// Package geoip resolves a request to an ISO-3166-1 alpha-2 country code,
// best-effort. Two sources, in priority order:
//
// 1. Cloudflare's CF-IPCountry header — instant, no network, present when
// the site is proxied through Cloudflare.
// 2. A GeoIP lookup on the client IP via the free ip-api.com endpoint —
// the fallback for when we're not behind Cloudflare (the current
// deployment is DNS-only).
//
// Everything is fire-and-forget: every failure path returns "" and the
// caller simply leaves the country unknown. Nothing here blocks a request
// on the critical path — callers run LookupCountryByIP from a goroutine.
package geoip
import (
"context"
"encoding/json"
"net"
"net/http"
"strings"
"time"
)
// CountryFromCloudflare reads the CF-IPCountry header. Cloudflare sets it
// to a 2-letter code, or to a sentinel ("XX" unknown, "T1" Tor) we treat
// as absent. Returns "" when the header isn't a usable country.
func CountryFromCloudflare(r *http.Request) string {
cc := strings.ToUpper(strings.TrimSpace(r.Header.Get("CF-IPCountry")))
if len(cc) != 2 || cc == "XX" || cc == "T1" {
return ""
}
return cc
}
// ClientIP returns the best guess at the caller's IP. The chi RealIP
// middleware already rewrites r.RemoteAddr from X-Forwarded-For /
// X-Real-IP, so we just strip the port.
func ClientIP(r *http.Request) string {
host, _, err := net.SplitHostPort(strings.TrimSpace(r.RemoteAddr))
if err != nil {
// RemoteAddr may already be bare (no port) behind some proxies.
return strings.TrimSpace(r.RemoteAddr)
}
return host
}
// LookupCountryByIP resolves an IP to a country code via ip-api.com.
// Returns "" for empty / private / loopback IPs (no point asking) and on
// any error, timeout, or non-success response. Respects ctx for
// cancellation/timeout — callers should pass a short-timeout context.
func LookupCountryByIP(ctx context.Context, ip string) string {
ip = strings.TrimSpace(ip)
if ip == "" {
return ""
}
parsed := net.ParseIP(ip)
if parsed == nil || parsed.IsLoopback() || parsed.IsPrivate() ||
parsed.IsLinkLocalUnicast() || parsed.IsUnspecified() {
return ""
}
// Free tier is HTTP-only; countryCode keeps the payload tiny.
url := "http://ip-api.com/json/" + ip + "?fields=status,countryCode"
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return ""
}
client := &http.Client{Timeout: 4 * time.Second}
resp, err := client.Do(req)
if err != nil {
return ""
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return ""
}
var body struct {
Status string `json:"status"`
CountryCode string `json:"countryCode"`
}
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
return ""
}
if body.Status != "success" {
return ""
}
cc := strings.ToUpper(strings.TrimSpace(body.CountryCode))
if len(cc) != 2 {
return ""
}
return cc
}