package main import ( "encoding/json" "fmt" "io" "net/http" "strings" "time" ) type Visit struct { RemoteAddr string Location string } type GeolocationResponse struct { Country string `json:"country"` City string `json:"city"` RegionName string `json:"regionName"` } type VisitHandler struct { geolocationApiBaseUrl string visitsCache map[string]int64 pastRequests []int64 rateLimitAmount int rateLimitDurationSeconds int cacheDurationSeconds int } func (v *VisitHandler) buildUrl(ipAddress string) string { return fmt.Sprintf("%s/json/%s?fields=country,regionName,city", v.geolocationApiBaseUrl, ipAddress) } func (v *VisitHandler) limitRate() bool { now := time.Now() idxToRemove := make([]int, 0) for idx, ts := range v.pastRequests { if now.Sub(time.Unix(ts, 0)).Seconds() >= float64(v.rateLimitDurationSeconds) { idxToRemove = append(idxToRemove, idx) } } for i := len(idxToRemove) - 1; i >= 0; i-- { idx := idxToRemove[i] v.pastRequests = append(v.pastRequests[:idx], v.pastRequests[idx+1:]...) } if len(v.pastRequests) == 0 { v.pastRequests = append(v.pastRequests, now.Unix()) return true } retVal := false if len(v.pastRequests) < v.rateLimitAmount { v.pastRequests = append(v.pastRequests, now.Unix()) retVal = true } return retVal } func (v *VisitHandler) getLocation(ipAddress string) *GeolocationResponse { if !v.limitRate() { return nil } resp, err := http.Get(v.buildUrl(ipAddress)) if err != nil { return nil } body, err := io.ReadAll(resp.Body) if err != nil { return nil } var geoResponse GeolocationResponse err = json.Unmarshal(body, &geoResponse) return &geoResponse } func (v *VisitHandler) createVisit(ipAddress string) *Visit { loc := v.getLocation(ipAddress) if loc == nil { return nil } locationParts := []string{} if loc.City != "" { locationParts = append(locationParts, loc.City) } if loc.RegionName != "" { locationParts = append(locationParts, loc.RegionName) } if loc.Country != "" { locationParts = append(locationParts, loc.Country) } return &Visit{ RemoteAddr: ipAddress, Location: strings.Join(locationParts, ", "), } } func (v *VisitHandler) HandleVisit(remoteAddr string) *Visit { if v.visitsCache == nil { v.visitsCache = make(map[string]int64) } if v.visitsCache[remoteAddr] == 0 || time.Since(time.Unix(v.visitsCache[remoteAddr], 0)).Seconds() > float64(v.cacheDurationSeconds) { visit := v.createVisit(remoteAddr) if visit == nil { return nil } v.visitsCache[visit.RemoteAddr] = time.Now().Unix() return visit } return nil } type VisitHandlerArgs struct { baseUrl string rateLimitAmount int rateLimitDurationSeconds int cacheDurationSeconds int } func SetupVisitHandler(args *VisitHandlerArgs) *VisitHandler { return &VisitHandler{ geolocationApiBaseUrl: args.baseUrl, visitsCache: make(map[string]int64), pastRequests: make([]int64, 0), rateLimitAmount: args.rateLimitAmount, rateLimitDurationSeconds: args.rateLimitDurationSeconds, cacheDurationSeconds: args.cacheDurationSeconds, } }