-
Notifications
You must be signed in to change notification settings - Fork 150
/
Copy pathshortnumber_info.go
241 lines (213 loc) · 8.6 KB
/
shortnumber_info.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
package phonenumbers
import (
"golang.org/x/exp/slices"
"github.com/nyaruka/phonenumbers/gen"
"google.golang.org/protobuf/proto"
)
var (
shortNumberRegionToMetadataMap = make(map[string]*PhoneMetadata)
)
func readFromShortNumberRegionToMetadataMap(key string) (*PhoneMetadata, bool) {
v, ok := shortNumberRegionToMetadataMap[key]
return v, ok
}
func writeToShortNumberRegionToMetadataMap(key string, val *PhoneMetadata) {
shortNumberRegionToMetadataMap[key] = val
}
func init() {
err := loadShortNumberMetadataFromFile()
if err != nil {
panic(err)
}
}
var (
currShortNumberMetadataColl *PhoneMetadataCollection
shortNumberReloadMetadata = true
)
func ShortNumberMetadataCollection() (*PhoneMetadataCollection, error) {
if !shortNumberReloadMetadata {
return currShortNumberMetadataColl, nil
}
rawBytes, err := decodeUnzipString(gen.ShortNumberData)
if err != nil {
return nil, err
}
metadataCollection := &PhoneMetadataCollection{}
err = proto.Unmarshal(rawBytes, metadataCollection)
shortNumberReloadMetadata = false
return metadataCollection, err
}
func loadShortNumberMetadataFromFile() error {
metadataCollection, err := ShortNumberMetadataCollection()
if err != nil {
return err
} else if currShortNumberMetadataColl == nil {
currShortNumberMetadataColl = metadataCollection
}
metadataList := metadataCollection.GetMetadata()
if len(metadataList) == 0 {
return ErrEmptyMetadata
}
for _, meta := range metadataList {
region := meta.GetId()
if region == "001" {
// it's a non geographical entity, unused
} else {
writeToShortNumberRegionToMetadataMap(region, meta)
}
}
return nil
}
// Check whether a short number is a possible number. If a country calling code is shared by
// multiple regions, this returns true if it's possible in any of them. This provides a more
// lenient check than #isValidShortNumber.
// See IsPossibleShortNumberForRegion(PhoneNumber, string) for details.
func IsPossibleShortNumber(number *PhoneNumber) bool {
regionsCodes := GetRegionCodesForCountryCode(int(number.GetCountryCode()))
shortNumberLength := len(GetNationalSignificantNumber(number))
for _, region := range regionsCodes {
phoneMetadata := getShortNumberMetadataForRegion(region)
if phoneMetadata == nil {
continue
}
if phoneMetadata.GeneralDesc.hasPossibleLength(int32(shortNumberLength)) {
return true
}
}
return false
}
// Check whether a short number is a possible number when dialed from the given region. This
// provides a more lenient check than IsValidShortNumberForRegion.
func IsPossibleShortNumberForRegion(number *PhoneNumber, regionDialingFrom string) bool {
if !regionDialingFromMatchesNumber(number, regionDialingFrom) {
return false
}
phoneMetadata := getShortNumberMetadataForRegion(regionDialingFrom)
if phoneMetadata == nil {
return false
}
numberLength := len(GetNationalSignificantNumber(number))
return phoneMetadata.GeneralDesc.hasPossibleLength(int32(numberLength))
}
// Tests whether a short number matches a valid pattern. If a country calling code is shared by
// multiple regions, this returns true if it's valid in any of them. Note that this doesn't verify
// the number is actually in use, which is impossible to tell by just looking at the number
// itself. See IsValidShortNumberForRegion(PhoneNumber, String) for details.
func IsValidShortNumber(number *PhoneNumber) bool {
regionCodes := GetRegionCodesForCountryCode(int(number.GetCountryCode()))
regionCode := getRegionCodeForShortNumberFromRegionList(number, regionCodes)
if len(regionCodes) > 1 && regionCode != "" {
// If a matching region had been found for the phone number from among two or more regions,
// then we have already implicitly verified its validity for that region.
return true
}
return IsValidShortNumberForRegion(number, regionCode)
}
// Tests whether a short number matches a valid pattern in a region. Note that this doesn't verify
// the number is actually in use, which is impossible to tell by just looking at the number itself.
func IsValidShortNumberForRegion(number *PhoneNumber, regionDialingFrom string) bool {
if !regionDialingFromMatchesNumber(number, regionDialingFrom) {
return false
}
phoneMetadata := getShortNumberMetadataForRegion(regionDialingFrom)
if phoneMetadata == nil {
return false
}
shortNumber := GetNationalSignificantNumber(number)
generalDesc := phoneMetadata.GeneralDesc
if !matchesPossibleNumberAndNationalNumber(shortNumber, generalDesc) {
return false
}
shortNumberDesc := phoneMetadata.GetShortCode()
return matchesPossibleNumberAndNationalNumber(shortNumber, shortNumberDesc)
}
func getShortNumberMetadataForRegion(regionCode string) *PhoneMetadata {
val, _ := readFromShortNumberRegionToMetadataMap(regionCode)
return val
}
func getRegionCodeForShortNumberFromRegionList(number *PhoneNumber, regionCodes []string) string {
if len(regionCodes) == 0 {
return ""
}
if len(regionCodes) == 1 {
return regionCodes[0]
}
nationalNumber := GetNationalSignificantNumber(number)
for _, regionCode := range regionCodes {
phoneMetadata := getShortNumberMetadataForRegion(regionCode)
if phoneMetadata != nil && matchesPossibleNumberAndNationalNumber(nationalNumber, phoneMetadata.GetShortCode()) {
// The number is valid for this region.
return regionCode
}
}
return ""
}
// Helper method to check that the country calling code of the number matches the region it's
// being dialed from.
func regionDialingFromMatchesNumber(number *PhoneNumber, regionDialingFrom string) bool {
regionCodes := GetRegionCodesForCountryCode(int(number.GetCountryCode()))
for _, region := range regionCodes {
if region == regionDialingFrom {
return true
}
}
return false
}
// TODO: Once we have benchmarked ShortNumberInfo, consider if it is worth keeping
// this performance optimization.
func matchesPossibleNumberAndNationalNumber(number string, numberDesc *PhoneNumberDesc) bool {
if numberDesc == nil {
return false
}
if len(numberDesc.PossibleLength) > 0 && !numberDesc.hasPossibleLength(int32(len(number))) {
return false
}
return MatchNationalNumber(number, *numberDesc, false)
}
// In these countries, if extra digits are added to an emergency number, it no longer connects
// to the emergency service.
var REGIONS_WHERE_EMERGENCY_NUMBERS_MUST_BE_EXACT = []string{"BR", "CL", "NI"}
func matchesEmergencyNumber(number string, regionCode string, allowPrefixMatch bool) bool {
possibleNumber := extractPossibleNumber(number)
// Returns false if the number starts with a plus sign. We don't believe dialing the country
// code before emergency numbers (e.g. +1911) works, but later, if that proves to work, we can
// add additional logic here to handle it.
if PLUS_CHARS_PATTERN.MatchString(possibleNumber) {
return false
}
phoneMetadata := getShortNumberMetadataForRegion(regionCode)
if phoneMetadata == nil || phoneMetadata.GetEmergency() == nil {
return false
}
normalizedNumber := NormalizeDigitsOnly(possibleNumber)
allowPrefixMatchForRegion := allowPrefixMatch && !slices.Contains(REGIONS_WHERE_EMERGENCY_NUMBERS_MUST_BE_EXACT, regionCode)
return MatchNationalNumber(normalizedNumber, *phoneMetadata.GetEmergency(), allowPrefixMatchForRegion)
}
// Returns true if the given number exactly matches an emergency service number in the given
// region.
//
// This method takes into account cases where the number might contain formatting, but doesn't
// allow additional digits to be appended. Note that isEmergencyNumber(number, region)
// implies connectsToEmergencyNumber(number, region).
//
// number: the phone number to test
// regionCode: the region where the phone number is being dialed
// return: whether the number exactly matches an emergency services number in the given region
func IsEmergencyNumber(number string, regionCode string) bool {
return matchesEmergencyNumber(number, regionCode, false)
}
// Returns true if the given number, exactly as dialed, might be used to connect to an emergency
// service in the given region.
//
// This method accepts a string, rather than a PhoneNumber, because it needs to distinguish
// cases such as "+1 911" and "911", where the former may not connect to an emergency service in
// all cases but the latter would. This method takes into account cases where the number might
// contain formatting, or might have additional digits appended (when it is okay to do that in
// the specified region).
//
// number: the phone number to test
// regionCode: the region where the phone number is being dialed
// return: whether the number might be used to connect to an emergency service in the given region
func ConnectsToEmergencyNumber(number string, regionCode string) bool {
return matchesEmergencyNumber(number, regionCode, true)
}