collinenlucy.nl/visits.go
2025-09-03 14:39:11 +02:00

142 lines
3.1 KiB
Go

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,
}
}