diff --git a/go.mod b/go.mod index ffd8218d9..9f62536ee 100644 --- a/go.mod +++ b/go.mod @@ -7,12 +7,12 @@ require ( github.com/fatih/color v1.10.0 github.com/gin-gonic/gin v1.6.3 github.com/nyaruka/phonenumbers v1.0.66 - github.com/onlinecity/go-phone-iso3166 v0.0.1 github.com/sirupsen/logrus v1.8.1 github.com/spf13/cobra v1.1.3 github.com/stretchr/testify v1.7.0 github.com/sundowndev/dorkgen v1.2.0 github.com/swaggo/swag v1.7.0 + github.com/tchap/go-patricia v2.3.0+incompatible gopkg.in/h2non/gock.v1 v1.0.16 ) @@ -31,8 +31,6 @@ require ( github.com/go-playground/validator/v10 v10.2.0 // indirect github.com/golang/protobuf v1.3.4 // indirect github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect - github.com/hashicorp/go-immutable-radix v1.1.0 // indirect - github.com/hashicorp/golang-lru v0.5.4 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/json-iterator/go v1.1.9 // indirect github.com/leodido/go-urn v1.2.0 // indirect diff --git a/go.sum b/go.sum index 0f4fbf100..5652c2172 100644 --- a/go.sum +++ b/go.sum @@ -112,21 +112,16 @@ github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyN github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-immutable-radix v1.1.0 h1:vN9wG1D6KG6YHRTWr8512cxGOVgTMEfgEdSj/hr8MPc= -github.com/hashicorp/go-immutable-radix v1.1.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= -github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= @@ -184,8 +179,6 @@ github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uY github.com/nyaruka/phonenumbers v1.0.66 h1:OVFSLMXhNR7CS3yq6ekm2uggokeggwJVO31h7OgB3OU= github.com/nyaruka/phonenumbers v1.0.66/go.mod h1:sDaTZ/KPX5f8qyV9qN+hIm+4ZBARJrupC6LuhshJq1U= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= -github.com/onlinecity/go-phone-iso3166 v0.0.1 h1:srN6o8NjxBWIrlK6Z+zD9wGMSGYi4itWA/fRyaxetqs= -github.com/onlinecity/go-phone-iso3166 v0.0.1/go.mod h1:n8+yIOCu9O63MH3WVwlWq1YVF6ZuAG5xlZ4mZ5ZzKF8= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -238,6 +231,8 @@ github.com/sundowndev/dorkgen v1.2.0 h1:dnB3tyV0n0IJ60KkaID35zoHRDvCbtJLcX9pLudQ github.com/sundowndev/dorkgen v1.2.0/go.mod h1:7CCMM/9plw5FEpuUgdqO+shHhKon1+T3TzcKGaDKyqk= github.com/swaggo/swag v1.7.0 h1:5bCA/MTLQoIqDXXyHfOpMeDvL9j68OY/udlK4pQoo4E= github.com/swaggo/swag v1.7.0/go.mod h1:BdPIL73gvS9NBsdi7M1JOxLvlbfvNRaBP8m6WT6Aajo= +github.com/tchap/go-patricia v2.3.0+incompatible h1:GkY4dP3cEfEASBPPkWd+AmjYxhmDkqO9/zg7R0lSQRs= +github.com/tchap/go-patricia v2.3.0+incompatible/go.mod h1:bmLyhP68RS6kStMGxByiQ23RP/odRBOTVjwp2cDyi6I= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= diff --git a/lib/number/number.go b/lib/number/number.go index 3fce5eee2..afa805b81 100644 --- a/lib/number/number.go +++ b/lib/number/number.go @@ -1,7 +1,10 @@ package number import ( + "errors" + "fmt" "github.com/nyaruka/phonenumbers" + "github.com/sundowndev/phoneinfoga/v2/lib/phonegeocode" ) // Number is a phone number @@ -16,10 +19,14 @@ type Number struct { } func NewNumber(number string) (res *Number, err error) { - n := "+" + FormatNumber(number) - country := ParseCountryCode(n) + n := fmt.Sprintf("+%s", FormatNumber(number)) - num, err := phonenumbers.Parse(n, country) + country, err := phonegeocode.Country(number) + if err != nil { + return nil, errors.New("country code not recognized") + } + + num, err := phonenumbers.Parse(n, "") if err != nil { return nil, err } diff --git a/lib/number/number_test.go b/lib/number/number_test.go index 6a0c3ae7c..fe672b3be 100644 --- a/lib/number/number_test.go +++ b/lib/number/number_test.go @@ -35,7 +35,7 @@ func TestNumber(t *testing.T) { E164: "+15552221212", International: "15552221212", CountryCode: 1, - Country: "", + Country: "US", Carrier: "", }, }, diff --git a/lib/number/utils.go b/lib/number/utils.go index ecd16cfec..4bf5ad87b 100644 --- a/lib/number/utils.go +++ b/lib/number/utils.go @@ -1,28 +1,15 @@ package number import ( + _ "embed" "regexp" - "strconv" - - phoneiso3166 "github.com/onlinecity/go-phone-iso3166" ) // FormatNumber formats a phone number to remove // unnecessary chars and avoid dealing with unwanted input. func FormatNumber(n string) string { re := regexp.MustCompile(`[_\W]+`) - number := re.ReplaceAllString(n, "") - - return number -} - -// ParseCountryCode parses a phone number and returns ISO country code. -// This is required in order to use the phonenumbers library. -func ParseCountryCode(n string) string { - var number uint64 - number, _ = strconv.ParseUint(FormatNumber(n), 10, 64) - - return phoneiso3166.E164.Lookup(number) + return re.ReplaceAllString(n, "") } // IsValid indicate if a phone number has a valid format. diff --git a/lib/number/utils_test.go b/lib/number/utils_test.go index a0e5a436a..48d702c48 100644 --- a/lib/number/utils_test.go +++ b/lib/number/utils_test.go @@ -20,26 +20,6 @@ func TestUtils(t *testing.T) { }) }) - t.Run("ParseCountryCode", func(t *testing.T) { - t.Run("should parse country code correctly", func(t *testing.T) { - result := ParseCountryCode("+33 679368229") - - assert.Equal(t, result, "FR", "they should be equal") - }) - - t.Run("should parse country code correctly", func(t *testing.T) { - result := ParseCountryCode("+1 315-284-1580") - - assert.Equal(t, result, "US", "they should be equal") - }) - - t.Run("should parse country code correctly", func(t *testing.T) { - result := ParseCountryCode("4566118311") - - assert.Equal(t, result, "DK", "they should be equal") - }) - }) - t.Run("IsValid", func(t *testing.T) { t.Run("should validate phone number", func(t *testing.T) { result := IsValid("+1 315-284-1580") diff --git a/lib/phonegeocode/README.md b/lib/phonegeocode/README.md new file mode 100644 index 000000000..5ad0f0cca --- /dev/null +++ b/lib/phonegeocode/README.md @@ -0,0 +1,59 @@ +# Phone Geocode + +**This package was copied from [github.com/davegardnerisme/phonegeocode](https://github.com/davegardnerisme/phonegeocode) then modified.** + +---- + +Internationalised phone number geocoding by country for Go. + +I built this because I needed to turn phone numbers into countries, and that's +really _all_ I needed. If [libphonenumber](https://code.google.com/p/libphonenumber/) +existed for Go, I would probably just use that. AFAIK it doesn't. + +This is based on work in [github.com/relops/prefixes](https://github.com/relops/prefixes), +however it has a different implementation, using a [Trie](http://en.wikipedia.org/wiki/Trie) +data structure - specifically [github.com/tchap/go-patricia](https://github.com/tchap/go-patricia). + +The way it works is that we have a list of prefixes that identify a country, and +we simply match the _most specific prefix_ to find the country code. This deals +with Canada where the country code is `+1` and shared with US. + +All the data lives in a CSV and can be used via codegen to create our Trie. + +``` +go run data/codegen.go > ./data.go && go fmt +``` + +## Usage + +``` +// cc = GB, err = nil +cc, err := phonegeocode.Country("+447999111222") + +// cc = "", err = phonegeocode.ErrCountryNotFound +cc, err := phonegeocode.Country("+999999999998") +``` + +## License + +The MIT License (MIT) + +Copyright (c) 2016 Dave Gardner + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/lib/phonegeocode/data.go b/lib/phonegeocode/data.go new file mode 100644 index 000000000..4f366a5a1 --- /dev/null +++ b/lib/phonegeocode/data.go @@ -0,0 +1,294 @@ +package phonegeocode + +import ( + gotrie "github.com/tchap/go-patricia/patricia" +) + +func initPrefixes() *gotrie.Trie { + prefixes := gotrie.NewTrie() + + prefixes.Insert(gotrie.Prefix("1403"), "CA") + prefixes.Insert(gotrie.Prefix("1587"), "CA") + prefixes.Insert(gotrie.Prefix("1780"), "CA") + prefixes.Insert(gotrie.Prefix("1825"), "CA") + prefixes.Insert(gotrie.Prefix("1236"), "CA") + prefixes.Insert(gotrie.Prefix("1250"), "CA") + prefixes.Insert(gotrie.Prefix("1604"), "CA") + prefixes.Insert(gotrie.Prefix("1672"), "CA") + prefixes.Insert(gotrie.Prefix("1778"), "CA") + prefixes.Insert(gotrie.Prefix("1204"), "CA") + prefixes.Insert(gotrie.Prefix("1431"), "CA") + prefixes.Insert(gotrie.Prefix("1506"), "CA") + prefixes.Insert(gotrie.Prefix("1709"), "CA") + prefixes.Insert(gotrie.Prefix("1782"), "CA") + prefixes.Insert(gotrie.Prefix("1902"), "CA") + prefixes.Insert(gotrie.Prefix("1548"), "CA") + prefixes.Insert(gotrie.Prefix("1249"), "CA") + prefixes.Insert(gotrie.Prefix("1289"), "CA") + prefixes.Insert(gotrie.Prefix("1343"), "CA") + prefixes.Insert(gotrie.Prefix("1365"), "CA") + prefixes.Insert(gotrie.Prefix("1387"), "CA") + prefixes.Insert(gotrie.Prefix("1416"), "CA") + prefixes.Insert(gotrie.Prefix("1437"), "CA") + prefixes.Insert(gotrie.Prefix("1519"), "CA") + prefixes.Insert(gotrie.Prefix("1613"), "CA") + prefixes.Insert(gotrie.Prefix("1647"), "CA") + prefixes.Insert(gotrie.Prefix("1705"), "CA") + prefixes.Insert(gotrie.Prefix("1742"), "CA") + prefixes.Insert(gotrie.Prefix("1807"), "CA") + prefixes.Insert(gotrie.Prefix("1905"), "CA") + prefixes.Insert(gotrie.Prefix("1782"), "CA") + prefixes.Insert(gotrie.Prefix("1902"), "CA") + prefixes.Insert(gotrie.Prefix("1418"), "CA") + prefixes.Insert(gotrie.Prefix("1438"), "CA") + prefixes.Insert(gotrie.Prefix("1450"), "CA") + prefixes.Insert(gotrie.Prefix("1514"), "CA") + prefixes.Insert(gotrie.Prefix("1579"), "CA") + prefixes.Insert(gotrie.Prefix("1581"), "CA") + prefixes.Insert(gotrie.Prefix("1819"), "CA") + prefixes.Insert(gotrie.Prefix("1873"), "CA") + prefixes.Insert(gotrie.Prefix("1306"), "CA") + prefixes.Insert(gotrie.Prefix("1639"), "CA") + prefixes.Insert(gotrie.Prefix("1867"), "CA") + prefixes.Insert(gotrie.Prefix("1"), "US") + prefixes.Insert(gotrie.Prefix("1242"), "BS") + prefixes.Insert(gotrie.Prefix("1246"), "BB") + prefixes.Insert(gotrie.Prefix("1264"), "AI") + prefixes.Insert(gotrie.Prefix("1268"), "AG") + prefixes.Insert(gotrie.Prefix("1284"), "VG") + prefixes.Insert(gotrie.Prefix("1340"), "VI") + prefixes.Insert(gotrie.Prefix("1345"), "KY") + prefixes.Insert(gotrie.Prefix("1441"), "BM") + prefixes.Insert(gotrie.Prefix("1473"), "GD") + prefixes.Insert(gotrie.Prefix("1649"), "TC") + prefixes.Insert(gotrie.Prefix("1664"), "MS") + prefixes.Insert(gotrie.Prefix("1670"), "MP") + prefixes.Insert(gotrie.Prefix("1671"), "GU") + prefixes.Insert(gotrie.Prefix("1684"), "AS") + prefixes.Insert(gotrie.Prefix("1721"), "SX") + prefixes.Insert(gotrie.Prefix("1758"), "LC") + prefixes.Insert(gotrie.Prefix("1767"), "DM") + prefixes.Insert(gotrie.Prefix("1784"), "VC") + prefixes.Insert(gotrie.Prefix("1787"), "PR") + prefixes.Insert(gotrie.Prefix("1809"), "DO") + prefixes.Insert(gotrie.Prefix("1829"), "DO") + prefixes.Insert(gotrie.Prefix("1849"), "DO") + prefixes.Insert(gotrie.Prefix("1868"), "TT") + prefixes.Insert(gotrie.Prefix("1869"), "KN") + prefixes.Insert(gotrie.Prefix("1876"), "JM") + prefixes.Insert(gotrie.Prefix("1939"), "PR") + prefixes.Insert(gotrie.Prefix("20"), "EG") + prefixes.Insert(gotrie.Prefix("211"), "SS") + prefixes.Insert(gotrie.Prefix("212"), "MA") + prefixes.Insert(gotrie.Prefix("213"), "DZ") + prefixes.Insert(gotrie.Prefix("216"), "TN") + prefixes.Insert(gotrie.Prefix("218"), "LY") + prefixes.Insert(gotrie.Prefix("220"), "GM") + prefixes.Insert(gotrie.Prefix("221"), "SN") + prefixes.Insert(gotrie.Prefix("222"), "MR") + prefixes.Insert(gotrie.Prefix("223"), "ML") + prefixes.Insert(gotrie.Prefix("224"), "GN") + prefixes.Insert(gotrie.Prefix("225"), "CI") + prefixes.Insert(gotrie.Prefix("226"), "BF") + prefixes.Insert(gotrie.Prefix("227"), "NE") + prefixes.Insert(gotrie.Prefix("228"), "TG") + prefixes.Insert(gotrie.Prefix("229"), "BJ") + prefixes.Insert(gotrie.Prefix("230"), "MU") + prefixes.Insert(gotrie.Prefix("231"), "LR") + prefixes.Insert(gotrie.Prefix("232"), "SL") + prefixes.Insert(gotrie.Prefix("233"), "GH") + prefixes.Insert(gotrie.Prefix("234"), "NG") + prefixes.Insert(gotrie.Prefix("235"), "TD") + prefixes.Insert(gotrie.Prefix("236"), "CF") + prefixes.Insert(gotrie.Prefix("237"), "CM") + prefixes.Insert(gotrie.Prefix("238"), "CV") + prefixes.Insert(gotrie.Prefix("239"), "ST") + prefixes.Insert(gotrie.Prefix("240"), "GQ") + prefixes.Insert(gotrie.Prefix("241"), "GA") + prefixes.Insert(gotrie.Prefix("242"), "CG") + prefixes.Insert(gotrie.Prefix("243"), "CD") + prefixes.Insert(gotrie.Prefix("244"), "AO") + prefixes.Insert(gotrie.Prefix("245"), "GW") + prefixes.Insert(gotrie.Prefix("246"), "IO") + prefixes.Insert(gotrie.Prefix("247"), "SH") + prefixes.Insert(gotrie.Prefix("248"), "SC") + prefixes.Insert(gotrie.Prefix("249"), "SD") + prefixes.Insert(gotrie.Prefix("250"), "RW") + prefixes.Insert(gotrie.Prefix("251"), "ET") + prefixes.Insert(gotrie.Prefix("252"), "SO") + prefixes.Insert(gotrie.Prefix("253"), "DJ") + prefixes.Insert(gotrie.Prefix("254"), "KE") + prefixes.Insert(gotrie.Prefix("255"), "TZ") + prefixes.Insert(gotrie.Prefix("256"), "UG") + prefixes.Insert(gotrie.Prefix("257"), "BI") + prefixes.Insert(gotrie.Prefix("258"), "MZ") + prefixes.Insert(gotrie.Prefix("260"), "ZM") + prefixes.Insert(gotrie.Prefix("261"), "MG") + prefixes.Insert(gotrie.Prefix("262"), "RE") + prefixes.Insert(gotrie.Prefix("263"), "ZW") + prefixes.Insert(gotrie.Prefix("264"), "NA") + prefixes.Insert(gotrie.Prefix("265"), "MW") + prefixes.Insert(gotrie.Prefix("266"), "LS") + prefixes.Insert(gotrie.Prefix("267"), "BW") + prefixes.Insert(gotrie.Prefix("268"), "SZ") + prefixes.Insert(gotrie.Prefix("269"), "KM") + prefixes.Insert(gotrie.Prefix("27"), "ZA") + prefixes.Insert(gotrie.Prefix("290"), "SH") + prefixes.Insert(gotrie.Prefix("291"), "ER") + prefixes.Insert(gotrie.Prefix("297"), "AW") + prefixes.Insert(gotrie.Prefix("298"), "FO") + prefixes.Insert(gotrie.Prefix("299"), "GL") + prefixes.Insert(gotrie.Prefix("30"), "GR") + prefixes.Insert(gotrie.Prefix("31"), "NL") + prefixes.Insert(gotrie.Prefix("32"), "BE") + prefixes.Insert(gotrie.Prefix("33"), "FR") + prefixes.Insert(gotrie.Prefix("34"), "ES") + prefixes.Insert(gotrie.Prefix("350"), "GI") + prefixes.Insert(gotrie.Prefix("351"), "PT") + prefixes.Insert(gotrie.Prefix("352"), "LU") + prefixes.Insert(gotrie.Prefix("353"), "IE") + prefixes.Insert(gotrie.Prefix("354"), "IS") + prefixes.Insert(gotrie.Prefix("355"), "AL") + prefixes.Insert(gotrie.Prefix("356"), "MT") + prefixes.Insert(gotrie.Prefix("357"), "CY") + prefixes.Insert(gotrie.Prefix("358"), "FI") + prefixes.Insert(gotrie.Prefix("359"), "BG") + prefixes.Insert(gotrie.Prefix("36"), "HU") + prefixes.Insert(gotrie.Prefix("370"), "LT") + prefixes.Insert(gotrie.Prefix("371"), "LV") + prefixes.Insert(gotrie.Prefix("372"), "EE") + prefixes.Insert(gotrie.Prefix("373"), "MD") + prefixes.Insert(gotrie.Prefix("374"), "AM") + prefixes.Insert(gotrie.Prefix("375"), "BY") + prefixes.Insert(gotrie.Prefix("376"), "AD") + prefixes.Insert(gotrie.Prefix("377"), "MC") + prefixes.Insert(gotrie.Prefix("378"), "SM") + prefixes.Insert(gotrie.Prefix("379"), "VA") + prefixes.Insert(gotrie.Prefix("380"), "UA") + prefixes.Insert(gotrie.Prefix("381"), "RS") + prefixes.Insert(gotrie.Prefix("381"), "XK") + prefixes.Insert(gotrie.Prefix("382"), "ME") + prefixes.Insert(gotrie.Prefix("385"), "HR") + prefixes.Insert(gotrie.Prefix("386"), "SI") + prefixes.Insert(gotrie.Prefix("386"), "XK") + prefixes.Insert(gotrie.Prefix("387"), "BA") + prefixes.Insert(gotrie.Prefix("389"), "MK") + prefixes.Insert(gotrie.Prefix("39"), "IT") + prefixes.Insert(gotrie.Prefix("39066"), "VA") + prefixes.Insert(gotrie.Prefix("40"), "RO") + prefixes.Insert(gotrie.Prefix("41"), "CH") + prefixes.Insert(gotrie.Prefix("420"), "CZ") + prefixes.Insert(gotrie.Prefix("421"), "SK") + prefixes.Insert(gotrie.Prefix("423"), "LI") + prefixes.Insert(gotrie.Prefix("43"), "AT") + prefixes.Insert(gotrie.Prefix("44"), "GB") + prefixes.Insert(gotrie.Prefix("45"), "DK") + prefixes.Insert(gotrie.Prefix("46"), "SE") + prefixes.Insert(gotrie.Prefix("47"), "NO") + prefixes.Insert(gotrie.Prefix("4779"), "SJ") + prefixes.Insert(gotrie.Prefix("48"), "PL") + prefixes.Insert(gotrie.Prefix("49"), "DE") + prefixes.Insert(gotrie.Prefix("500"), "FK") + prefixes.Insert(gotrie.Prefix("501"), "BZ") + prefixes.Insert(gotrie.Prefix("502"), "GT") + prefixes.Insert(gotrie.Prefix("503"), "SV") + prefixes.Insert(gotrie.Prefix("504"), "HN") + prefixes.Insert(gotrie.Prefix("505"), "NI") + prefixes.Insert(gotrie.Prefix("506"), "CR") + prefixes.Insert(gotrie.Prefix("507"), "PA") + prefixes.Insert(gotrie.Prefix("508"), "PM") + prefixes.Insert(gotrie.Prefix("509"), "HT") + prefixes.Insert(gotrie.Prefix("51"), "PE") + prefixes.Insert(gotrie.Prefix("52"), "MX") + prefixes.Insert(gotrie.Prefix("53"), "CU") + prefixes.Insert(gotrie.Prefix("54"), "AR") + prefixes.Insert(gotrie.Prefix("55"), "BR") + prefixes.Insert(gotrie.Prefix("56"), "CL") + prefixes.Insert(gotrie.Prefix("57"), "CO") + prefixes.Insert(gotrie.Prefix("58"), "VE") + prefixes.Insert(gotrie.Prefix("590"), "GP") + prefixes.Insert(gotrie.Prefix("591"), "BO") + prefixes.Insert(gotrie.Prefix("592"), "GY") + prefixes.Insert(gotrie.Prefix("593"), "EC") + prefixes.Insert(gotrie.Prefix("594"), "GF") + prefixes.Insert(gotrie.Prefix("595"), "PY") + prefixes.Insert(gotrie.Prefix("596"), "MQ") + prefixes.Insert(gotrie.Prefix("597"), "SR") + prefixes.Insert(gotrie.Prefix("598"), "UY") + prefixes.Insert(gotrie.Prefix("5997"), "BQ") + prefixes.Insert(gotrie.Prefix("5999"), "CW") + prefixes.Insert(gotrie.Prefix("60"), "MY") + prefixes.Insert(gotrie.Prefix("61"), "AU") + prefixes.Insert(gotrie.Prefix("62"), "ID") + prefixes.Insert(gotrie.Prefix("63"), "PH") + prefixes.Insert(gotrie.Prefix("64"), "NZ") + prefixes.Insert(gotrie.Prefix("65"), "SG") + prefixes.Insert(gotrie.Prefix("66"), "TH") + prefixes.Insert(gotrie.Prefix("670"), "TL") + prefixes.Insert(gotrie.Prefix("672"), "NF") + prefixes.Insert(gotrie.Prefix("673"), "BN") + prefixes.Insert(gotrie.Prefix("674"), "NR") + prefixes.Insert(gotrie.Prefix("675"), "PG") + prefixes.Insert(gotrie.Prefix("676"), "TO") + prefixes.Insert(gotrie.Prefix("677"), "SB") + prefixes.Insert(gotrie.Prefix("678"), "VU") + prefixes.Insert(gotrie.Prefix("679"), "FJ") + prefixes.Insert(gotrie.Prefix("680"), "PW") + prefixes.Insert(gotrie.Prefix("681"), "WF") + prefixes.Insert(gotrie.Prefix("682"), "CK") + prefixes.Insert(gotrie.Prefix("683"), "NU") + prefixes.Insert(gotrie.Prefix("685"), "WS") + prefixes.Insert(gotrie.Prefix("686"), "KI") + prefixes.Insert(gotrie.Prefix("687"), "NC") + prefixes.Insert(gotrie.Prefix("688"), "TV") + prefixes.Insert(gotrie.Prefix("689"), "PF") + prefixes.Insert(gotrie.Prefix("690"), "TK") + prefixes.Insert(gotrie.Prefix("691"), "FM") + prefixes.Insert(gotrie.Prefix("692"), "MH") + prefixes.Insert(gotrie.Prefix("7"), "RU") + prefixes.Insert(gotrie.Prefix("76"), "KZ") + prefixes.Insert(gotrie.Prefix("77"), "KZ") + prefixes.Insert(gotrie.Prefix("81"), "JP") + prefixes.Insert(gotrie.Prefix("82"), "KR") + prefixes.Insert(gotrie.Prefix("84"), "VN") + prefixes.Insert(gotrie.Prefix("850"), "KP") + prefixes.Insert(gotrie.Prefix("852"), "HK") + prefixes.Insert(gotrie.Prefix("853"), "MO") + prefixes.Insert(gotrie.Prefix("855"), "KH") + prefixes.Insert(gotrie.Prefix("856"), "LA") + prefixes.Insert(gotrie.Prefix("86"), "CN") + prefixes.Insert(gotrie.Prefix("880"), "BD") + prefixes.Insert(gotrie.Prefix("886"), "TW") + prefixes.Insert(gotrie.Prefix("90"), "TR") + prefixes.Insert(gotrie.Prefix("91"), "IN") + prefixes.Insert(gotrie.Prefix("92"), "PK") + prefixes.Insert(gotrie.Prefix("93"), "AF") + prefixes.Insert(gotrie.Prefix("94"), "LK") + prefixes.Insert(gotrie.Prefix("95"), "MM") + prefixes.Insert(gotrie.Prefix("960"), "MV") + prefixes.Insert(gotrie.Prefix("961"), "LB") + prefixes.Insert(gotrie.Prefix("962"), "JO") + prefixes.Insert(gotrie.Prefix("963"), "SY") + prefixes.Insert(gotrie.Prefix("964"), "IQ") + prefixes.Insert(gotrie.Prefix("965"), "KW") + prefixes.Insert(gotrie.Prefix("966"), "SA") + prefixes.Insert(gotrie.Prefix("967"), "YE") + prefixes.Insert(gotrie.Prefix("968"), "OM") + prefixes.Insert(gotrie.Prefix("970"), "PS") + prefixes.Insert(gotrie.Prefix("971"), "AE") + prefixes.Insert(gotrie.Prefix("972"), "IL") + prefixes.Insert(gotrie.Prefix("973"), "BH") + prefixes.Insert(gotrie.Prefix("974"), "QA") + prefixes.Insert(gotrie.Prefix("975"), "BT") + prefixes.Insert(gotrie.Prefix("976"), "MN") + prefixes.Insert(gotrie.Prefix("977"), "NP") + prefixes.Insert(gotrie.Prefix("98"), "IR") + prefixes.Insert(gotrie.Prefix("992"), "TJ") + prefixes.Insert(gotrie.Prefix("993"), "TM") + prefixes.Insert(gotrie.Prefix("994"), "AZ") + prefixes.Insert(gotrie.Prefix("995"), "GE") + prefixes.Insert(gotrie.Prefix("996"), "KG") + prefixes.Insert(gotrie.Prefix("998"), "UZ") + + return prefixes +} diff --git a/lib/phonegeocode/data/codegen.go b/lib/phonegeocode/data/codegen.go new file mode 100644 index 000000000..7f73e7fc6 --- /dev/null +++ b/lib/phonegeocode/data/codegen.go @@ -0,0 +1,41 @@ +package main + +import ( + "bufio" + "embed" + "encoding/csv" + "fmt" + "io" + "log" +) + +//go:embed prefixes.csv +var prefixes embed.FS + +func main() { + f, err := prefixes.Open("prefixes.csv") + if err != nil { + log.Fatal(err) + } + in := bufio.NewReader(f) + reader := csv.NewReader(in) + + fmt.Printf("package phonegeocode\n\n") + fmt.Printf("import (\n\tgotrie \"github.com/tchap/go-patricia/patricia\"\n)\n\n") + + fmt.Printf("func initPrefixes() *gotrie.Trie {\n") + fmt.Printf("\tprefixes := gotrie.NewTrie()\n\n") + + for { + row, err := reader.Read() + if err == io.EOF { + break + } + if err != nil { + log.Fatal(err) + } + + fmt.Printf(" prefixes.Insert(gotrie.Prefix(\"%s\"), \"%s\")\n", row[0], row[1]) + } + fmt.Printf("\n\treturn prefixes\n}\n") +} diff --git a/lib/phonegeocode/data/prefixes.csv b/lib/phonegeocode/data/prefixes.csv new file mode 100644 index 000000000..e4ff3bf43 --- /dev/null +++ b/lib/phonegeocode/data/prefixes.csv @@ -0,0 +1,282 @@ +1403,CA +1587,CA +1780,CA +1825,CA +1236,CA +1250,CA +1604,CA +1672,CA +1778,CA +1204,CA +1431,CA +1506,CA +1709,CA +1782,CA +1902,CA +1548,CA +1249,CA +1289,CA +1343,CA +1365,CA +1387,CA +1416,CA +1437,CA +1519,CA +1613,CA +1647,CA +1705,CA +1742,CA +1807,CA +1905,CA +1782,CA +1902,CA +1418,CA +1438,CA +1450,CA +1514,CA +1579,CA +1581,CA +1819,CA +1873,CA +1306,CA +1639,CA +1867,CA +1,US +1242,BS +1246,BB +1264,AI +1268,AG +1284,VG +1340,VI +1345,KY +1441,BM +1473,GD +1649,TC +1664,MS +1670,MP +1671,GU +1684,AS +1721,SX +1758,LC +1767,DM +1784,VC +1787,PR +1809,DO +1829,DO +1849,DO +1868,TT +1869,KN +1876,JM +1939,PR +20,EG +211,SS +212,MA +213,DZ +216,TN +218,LY +220,GM +221,SN +222,MR +223,ML +224,GN +225,CI +226,BF +227,NE +228,TG +229,BJ +230,MU +231,LR +232,SL +233,GH +234,NG +235,TD +236,CF +237,CM +238,CV +239,ST +240,GQ +241,GA +242,CG +243,CD +244,AO +245,GW +246,IO +247,SH +248,SC +249,SD +250,RW +251,ET +252,SO +253,DJ +254,KE +255,TZ +256,UG +257,BI +258,MZ +260,ZM +261,MG +262,RE +263,ZW +264,NA +265,MW +266,LS +267,BW +268,SZ +269,KM +27,ZA +290,SH +291,ER +297,AW +298,FO +299,GL +30,GR +31,NL +32,BE +33,FR +34,ES +350,GI +351,PT +352,LU +353,IE +354,IS +355,AL +356,MT +357,CY +358,FI +359,BG +36,HU +370,LT +371,LV +372,EE +373,MD +374,AM +375,BY +376,AD +377,MC +378,SM +379,VA +380,UA +381,RS +381,XK +382,ME +385,HR +386,SI +386,XK +387,BA +389,MK +39,IT +39066,VA +40,RO +41,CH +420,CZ +421,SK +423,LI +43,AT +44,GB +45,DK +46,SE +47,NO +4779,SJ +48,PL +49,DE +500,FK +501,BZ +502,GT +503,SV +504,HN +505,NI +506,CR +507,PA +508,PM +509,HT +51,PE +52,MX +53,CU +54,AR +55,BR +56,CL +57,CO +58,VE +590,GP +591,BO +592,GY +593,EC +594,GF +595,PY +596,MQ +597,SR +598,UY +5997,BQ +5999,CW +60,MY +61,AU +62,ID +63,PH +64,NZ +65,SG +66,TH +670,TL +672,NF +673,BN +674,NR +675,PG +676,TO +677,SB +678,VU +679,FJ +680,PW +681,WF +682,CK +683,NU +685,WS +686,KI +687,NC +688,TV +689,PF +690,TK +691,FM +692,MH +7,RU +76,KZ +77,KZ +81,JP +82,KR +84,VN +850,KP +852,HK +853,MO +855,KH +856,LA +86,CN +880,BD +886,TW +90,TR +91,IN +92,PK +93,AF +94,LK +95,MM +960,MV +961,LB +962,JO +963,SY +964,IQ +965,KW +966,SA +967,YE +968,OM +970,PS +971,AE +972,IL +973,BH +974,QA +975,BT +976,MN +977,NP +98,IR +992,TJ +993,TM +994,AZ +995,GE +996,KG +998,UZ diff --git a/lib/phonegeocode/phonegeocode.go b/lib/phonegeocode/phonegeocode.go new file mode 100644 index 000000000..bce57d580 --- /dev/null +++ b/lib/phonegeocode/phonegeocode.go @@ -0,0 +1,52 @@ +package phonegeocode + +import ( + "errors" + "strings" + "sync" + + gotrie "github.com/tchap/go-patricia/patricia" +) + +var ErrCountryNotFound = errors.New("could not identify country from phone number") + +var ( + once = &sync.Once{} + tree *trieGeocoder +) + +type Geocoder interface { + Country(number string) (string, error) +} + +// init initialises a new thread-safe geocoder +func init() { + once.Do(func() { + tree = &trieGeocoder{ + data: initPrefixes(), + } + }) +} + +type trieGeocoder struct { + data *gotrie.Trie +} + +// Country tries to identify the country for a phone number - assuming it is provided in i18n format (+nn) +func Country(number string) (cc string, err error) { + number = strings.TrimPrefix(number, "+") + + maxLen := -1 + _ = tree.data.VisitPrefixes(gotrie.Prefix(number), func(prefix gotrie.Prefix, item gotrie.Item) error { + if len(prefix) > maxLen { + cc = item.(string) + } + return nil + }) + + if len(cc) == 0 { + err = ErrCountryNotFound + } + + return +} diff --git a/lib/phonegeocode/phonegeocode_test.go b/lib/phonegeocode/phonegeocode_test.go new file mode 100644 index 000000000..1a874a8b6 --- /dev/null +++ b/lib/phonegeocode/phonegeocode_test.go @@ -0,0 +1,47 @@ +package phonegeocode + +import ( + "testing" +) + +func TestThingsThatAreFound(t *testing.T) { + cases := []struct { + number, country string + }{ + {"+4479991113332", "GB"}, + {"+1807142342342", "CA"}, + {"+1781234555552", "US"}, + {"+3462233455552", "ES"}, + {"+3538523523455", "IE"}, + {"+19425242343333", "US"}, + {"+1786822211132", "US"}, + {"+34634343434443", "ES"}, + {"+190572354235235", "CA"}, + } + + for _, tc := range cases { + cc, err := Country(tc.number) + if err != nil { + t.Errorf("Not expecting number '%s' to yield an error; got %v", tc.number, err) + } + if cc != tc.country { + t.Errorf("Number '%s' did not match expected CC '%s'; got '%s'", tc.number, tc.country, cc) + } + } +} + +func TestThingsThatAreNotFound(t *testing.T) { + cases := []struct { + number string + }{ + {"+9915155235325"}, + {"+898235265"}, + } + + for _, tc := range cases { + _, err := Country(tc.number) + if err == nil { + t.Errorf("Expecting number '%s' to yield an error", tc.number) + } + } +} diff --git a/lib/remote/local_scanner.go b/lib/remote/local_scanner.go index 0584ee819..973ca4a65 100644 --- a/lib/remote/local_scanner.go +++ b/lib/remote/local_scanner.go @@ -1,6 +1,7 @@ package remote import ( + "github.com/nyaruka/phonenumbers" "github.com/sundowndev/phoneinfoga/v2/lib/number" ) @@ -16,6 +17,7 @@ type LocalScannerResponse struct { CountryCode int32 `json:"country_code,omitempty" console:"Country code,omitempty"` Country string `json:"country,omitempty" console:"Country,omitempty"` Carrier string `json:"carrier,omitempty" console:"Carrier,omitempty"` + Valid bool `json:"valid" console:"Is valid"` } func NewLocalScanner() *localScanner { @@ -31,7 +33,13 @@ func (s *localScanner) ShouldRun(_ number.Number) bool { } func (s *localScanner) Scan(n number.Number) (interface{}, error) { + num, err := phonenumbers.Parse(n.E164, n.Country) + if err != nil { + return nil, err + } + data := LocalScannerResponse{ + Valid: phonenumbers.IsValidNumber(num), RawLocal: n.RawLocal, Local: n.Local, E164: n.E164, diff --git a/lib/remote/local_scanner_test.go b/lib/remote/local_scanner_test.go index abb10e7ce..62b98d700 100644 --- a/lib/remote/local_scanner_test.go +++ b/lib/remote/local_scanner_test.go @@ -27,6 +27,8 @@ func TestLocalScanner(t *testing.T) { E164: "+15556661212", International: "15556661212", CountryCode: 1, + Country: "US", + Valid: false, }, }, wantErrors: map[string]error{}, diff --git a/lib/remote/suppliers/ovh.go b/lib/remote/suppliers/ovh.go index e57c5b484..ec9ae313a 100644 --- a/lib/remote/suppliers/ovh.go +++ b/lib/remote/suppliers/ovh.go @@ -41,14 +41,8 @@ func NewOVHSupplier() *OVHSupplier { } func (s *OVHSupplier) Search(num number.Number) (*OVHScannerResponse, error) { - countryCode := strings.ToLower(num.Country) - - if countryCode == "" { - return nil, fmt.Errorf("country code +%d wasn't recognized", num.CountryCode) - } - // Build the request - response, err := http.Get(fmt.Sprintf("https://api.ovh.com/1.0/telephony/number/detailedZones?country=%s", countryCode)) + response, err := http.Get(fmt.Sprintf("https://api.ovh.com/1.0/telephony/number/detailedZones?country=%s", strings.ToLower(num.Country))) if err != nil { return nil, err } diff --git a/lib/remote/suppliers/ovh_test.go b/lib/remote/suppliers/ovh_test.go index e3a4d90b6..379848aaf 100644 --- a/lib/remote/suppliers/ovh_test.go +++ b/lib/remote/suppliers/ovh_test.go @@ -68,15 +68,3 @@ func TestOVHSupplierError(t *testing.T) { Err: dummyError, }, err) } - -func TestOVHSupplierCountryCodeError(t *testing.T) { - defer gock.Off() // Flush pending mocks after test execution - - num, _ := number.NewNumber("15556661212") - - s := NewOVHSupplier() - - got, err := s.Search(*num) - assert.Nil(t, got) - assert.EqualError(t, err, "country code +1 wasn't recognized") -} diff --git a/web/server_test.go b/web/server_test.go index b8ba00eeb..f879cd1ae 100644 --- a/web/server_test.go +++ b/web/server_test.go @@ -105,7 +105,7 @@ func TestApi(t *testing.T) { assert.Equal(t, err, nil) assert.Equal(t, res.Result().StatusCode, 200) - assert.Equal(t, string(body), `{"success":true,"result":{"raw_local":"12345253","local":"12345253","e164":"+3312345253","international":"3312345253","country_code":33,"country":"FR"}}`) + assert.Equal(t, `{"success":true,"result":{"raw_local":"12345253","local":"12345253","e164":"+3312345253","international":"3312345253","country_code":33,"country":"FR","valid":false}}`, string(body)) }) t.Run("invalid number", func(t *testing.T) {