diff --git a/pkg/geoip/geoip.go b/pkg/geoip/geoip.go new file mode 100644 index 0000000..14a4fd8 --- /dev/null +++ b/pkg/geoip/geoip.go @@ -0,0 +1,55 @@ +package geoip + +import ( + "embed" + "net/netip" + + "github.com/enescakir/emoji" + "github.com/oschwald/geoip2-golang/v2" +) + +//go:embed resource/*.mmdb +var dbFS embed.FS + +var geoipReader *geoip2.Reader + +func Init() error { + data, err := dbFS.ReadFile("resource/GeoLite2-Country.mmdb") + if err != nil { + return err + } + + geoipReader, err = geoip2.OpenBytes(data) + return err +} + +type IpGeoInfo struct { + Country *geoip2.Country + Emoji string +} + +func GetIpGeoInfo(ip string) *IpGeoInfo { + if geoipReader == nil || ip == "" { + return nil + } + + ipAddr, err := netip.ParseAddr(ip) + if err != nil { + return nil + } + + country, err := geoipReader.Country(ipAddr) + if err != nil || country.Country.ISOCode == "" { + return nil + } + + emoji, err := emoji.CountryFlag(country.Country.ISOCode) + if err != nil { + return nil + } + + return &IpGeoInfo{ + Country: country, + Emoji: emoji.String(), + } +} diff --git a/pkg/geoip/geoip_test.go b/pkg/geoip/geoip_test.go new file mode 100644 index 0000000..8d1b6c1 --- /dev/null +++ b/pkg/geoip/geoip_test.go @@ -0,0 +1,48 @@ +package geoip + +import ( + "testing" +) + +func initOrSkip(t *testing.T) { + t.Helper() + if err := Init(); err != nil { + t.Skipf("skipping: geoip database unavailable: %v", err) + } +} + +func TestGetIpGeoInfo_Cloudflare(t *testing.T) { + initOrSkip(t) + + ip := "223.167.150.96" + city := GetIpGeoInfo(ip) + if city == nil { + t.Fatalf("GetIpGeoInfo(%q) returned nil, expected a result", ip) + } + + t.Logf("Country: %s (%s)", city.Country.Country.Names.English, city.Country.Country.ISOCode) + t.Logf("Emoji: %s", city.Emoji) +} + +func TestGetIpGeoInfo_EmptyIP(t *testing.T) { + initOrSkip(t) + + if city := GetIpGeoInfo(""); city != nil { + t.Errorf("GetIpGeoInfo(\"\") = %+v, want nil", city) + } +} + +func TestGetIpGeoInfo_InvalidIP(t *testing.T) { + initOrSkip(t) + + if city := GetIpGeoInfo("not-an-ip"); city != nil { + t.Errorf("GetIpGeoInfo(\"not-an-ip\") = %+v, want nil", city) + } +} + +func TestGetIpGeoInfo_UninitializedReader(t *testing.T) { + geoipReader = nil + if city := GetIpGeoInfo("103.21.244.12"); city != nil { + t.Errorf("expected nil when reader is uninitialized, got %+v", city) + } +} diff --git a/pkg/geoip/go.mod b/pkg/geoip/go.mod new file mode 100644 index 0000000..51c8b85 --- /dev/null +++ b/pkg/geoip/go.mod @@ -0,0 +1,10 @@ +module pkg/geoip + +go 1.25.5 + +require ( + github.com/enescakir/emoji v1.0.0 // indirect + github.com/oschwald/geoip2-golang/v2 v2.1.0 // indirect + github.com/oschwald/maxminddb-golang/v2 v2.1.1 // indirect + golang.org/x/sys v0.38.0 // indirect +) diff --git a/pkg/geoip/go.sum b/pkg/geoip/go.sum new file mode 100644 index 0000000..95bf659 --- /dev/null +++ b/pkg/geoip/go.sum @@ -0,0 +1,8 @@ +github.com/enescakir/emoji v1.0.0 h1:W+HsNql8swfCQFtioDGDHCHri8nudlK1n5p2rHCJoog= +github.com/enescakir/emoji v1.0.0/go.mod h1:Bt1EKuLnKDTYpLALApstIkAjdDrS/8IAgTkKp+WKFD0= +github.com/oschwald/geoip2-golang/v2 v2.1.0 h1:DjnLhNJu9WHwTrmoiQFvgmyJoczhdnm7LB23UBI2Amo= +github.com/oschwald/geoip2-golang/v2 v2.1.0/go.mod h1:qdVmcPgrTJ4q2eP9tHq/yldMTdp2VMr33uVdFbHBiBc= +github.com/oschwald/maxminddb-golang/v2 v2.1.1 h1:lA8FH0oOrM4u7mLvowq8IT6a3Q/qEnqRzLQn9eH5ojc= +github.com/oschwald/maxminddb-golang/v2 v2.1.1/go.mod h1:PLdx6PR+siSIoXqqy7C7r3SB3KZnhxWr1Dp6g0Hacl8= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= diff --git a/pkg/geoip/resource/GeoLite2-Country.mmdb b/pkg/geoip/resource/GeoLite2-Country.mmdb new file mode 100644 index 0000000..99091f3 Binary files /dev/null and b/pkg/geoip/resource/GeoLite2-Country.mmdb differ