-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
64e21d5
commit fb6f575
Showing
7 changed files
with
411 additions
and
661 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
package urns | ||
|
||
import ( | ||
"strconv" | ||
"strings" | ||
|
||
"github.com/nyaruka/phonenumbers" | ||
"github.com/pkg/errors" | ||
) | ||
|
||
// FromLocalPhone returns a validated tel URN | ||
func FromLocalPhone(number string, country string) (URN, error) { | ||
path, err := ParsePhone(number, country) | ||
if err != nil { | ||
return NilURN, err | ||
} | ||
|
||
return NewURNFromParts(Phone, path, "", "") | ||
} | ||
|
||
// ToLocalPhone converts a phone URN to a local number in the given country | ||
func ToLocalPhone(u URN, country string) string { | ||
_, path, _, _ := u.ToParts() | ||
|
||
parsed, err := phonenumbers.Parse(path, country) | ||
if err == nil { | ||
return strconv.FormatUint(parsed.GetNationalNumber(), 10) | ||
} | ||
return path | ||
} | ||
|
||
// ParsePhone tries to parse the given string as a phone number and if successful returns it as E164 | ||
func ParsePhone(s, country string) (string, error) { | ||
parsed, err := phonenumbers.Parse(s, country) | ||
if err != nil { | ||
return "", errors.Wrap(err, "unable to parse number") | ||
} | ||
|
||
if phonenumbers.IsPossibleNumberWithReason(parsed) != phonenumbers.IS_POSSIBLE { | ||
// if it's not a possible number, try adding a + and parsing again | ||
if !strings.HasPrefix(s, "+") { | ||
return ParsePhone("+"+s, country) | ||
} | ||
|
||
return "", errors.New("not a possible number") | ||
} | ||
|
||
return phonenumbers.Format(parsed, phonenumbers.E164), nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
package urns_test | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/nyaruka/gocommon/urns" | ||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func TestFromLocalPhone(t *testing.T) { | ||
testCases := []struct { | ||
number string | ||
country string | ||
expected urns.URN | ||
hasError bool | ||
}{ | ||
{"tel:0788383383", "RW", "tel:+250788383383", false}, | ||
{"tel: +250788383383 ", "KE", "tel:+250788383383", false}, // already has country code | ||
{"tel:(917)992-5253", "US", "tel:+19179925253", false}, | ||
{"tel:800-CABBAGE", "US", "tel:+18002222243", false}, | ||
{"tel:+62877747666", "ID", "tel:+62877747666", false}, | ||
{"tel:0877747666", "ID", "tel:+62877747666", false}, | ||
{"tel:07531669965", "GB", "tel:+447531669965", false}, | ||
{"tel:263780821000", "ZW", "tel:+263780821000", false}, | ||
|
||
{"0788383383", "ZZ", urns.NilURN, true}, // invalid country code | ||
{"1", "RW", urns.NilURN, true}, | ||
{"123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890", "RW", urns.NilURN, true}, | ||
} | ||
|
||
for i, tc := range testCases { | ||
urn, err := urns.FromLocalPhone(tc.number, tc.country) | ||
|
||
if tc.hasError { | ||
assert.Error(t, err, "%d: expected error for %s, %s", i, tc.number, tc.country) | ||
} else { | ||
assert.NoError(t, err, "%d: unexpected error for %s, %s", i, tc.number, tc.country) | ||
assert.Equal(t, tc.expected, urn, "%d: created URN mismatch for %s, %s", i, tc.number, tc.country) | ||
} | ||
} | ||
} | ||
|
||
func TestParsePhone(t *testing.T) { | ||
tcs := []struct { | ||
input string | ||
country string | ||
parsed string | ||
}{ | ||
{"+250788123123", "", "+250788123123"}, // international number fine without country | ||
{"+250 788 123-123", "", "+250788123123"}, // fine if not E164 formatted | ||
{"0788123123", "RW", "+250788123123"}, | ||
{"206 555 1212", "US", "+12065551212"}, | ||
{"12065551212", "US", "+12065551212"}, // country code but no + | ||
{"5912705", "US", ""}, // is only possible as a local number so ignored | ||
{"10000", "US", ""}, | ||
} | ||
|
||
for _, tc := range tcs { | ||
if tc.parsed != "" { | ||
parsed, err := urns.ParsePhone(tc.input, tc.country) | ||
assert.NoError(t, err, "unexpected error for '%s'", tc.input) | ||
assert.Equal(t, parsed, tc.parsed, "result mismatch for '%s'", tc.input) | ||
} else { | ||
_, err := urns.ParsePhone(tc.input, tc.country) | ||
assert.Error(t, err, "expected error for '%s'", tc.input) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,185 @@ | ||
package urns | ||
|
||
import ( | ||
"regexp" | ||
"strings" | ||
|
||
"github.com/nyaruka/phonenumbers" | ||
) | ||
|
||
var allDigitsRegex = regexp.MustCompile(`^[0-9]+$`) | ||
var nonTelCharsRegex = regexp.MustCompile(`[^0-9A-Z]`) | ||
|
||
var emailRegex = regexp.MustCompile(`^[^\s@]+@[^\s@]+$`) | ||
var freshchatRegex = regexp.MustCompile(`^[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}/[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}$`) | ||
var viberRegex = regexp.MustCompile(`^[a-zA-Z0-9_=/+]{1,24}$`) | ||
var lineRegex = regexp.MustCompile(`^[a-zA-Z0-9_]{1,36}$`) | ||
var telRegex = regexp.MustCompile(`^\+?[a-zA-Z0-9]{1,64}$`) | ||
var twitterHandleRegex = regexp.MustCompile(`^[a-zA-Z0-9_]{1,15}$`) | ||
var webchatRegex = regexp.MustCompile(`^[a-zA-Z0-9]{24}(:[^\s@]+@[^\s@]+)?$`) | ||
|
||
const ( | ||
// FacebookRefPrefix is prefix used for facebook referral URNs | ||
FacebookRefPrefix string = "ref:" | ||
) | ||
|
||
func init() { | ||
register(Discord) | ||
register(Email) | ||
register(External) | ||
register(Facebook) | ||
register(Firebase) | ||
register(FreshChat) | ||
register(Instagram) | ||
register(JioChat) | ||
register(Line) | ||
register(Phone) | ||
register(RocketChat) | ||
register(Slack) | ||
register(Telegram) | ||
register(Twitter) | ||
register(TwitterID) | ||
register(Viber) | ||
register(VK) | ||
register(WebChat) | ||
register(WeChat) | ||
register(WhatsApp) | ||
} | ||
|
||
var schemes = map[string]*Scheme{} | ||
|
||
func register(s *Scheme) { | ||
schemes[s.Prefix] = s | ||
} | ||
|
||
type Scheme struct { | ||
Prefix string | ||
Normalize func(string) string | ||
Validate func(string) bool | ||
Format func(string) string | ||
} | ||
|
||
var Discord = &Scheme{ | ||
Prefix: "discord", | ||
Validate: func(path string) bool { return allDigitsRegex.MatchString(path) }, | ||
} | ||
|
||
var Email = &Scheme{ | ||
Prefix: "mailto", | ||
Normalize: func(path string) string { return strings.ToLower(path) }, | ||
Validate: func(path string) bool { return emailRegex.MatchString(path) }, | ||
} | ||
|
||
var External = &Scheme{ | ||
Prefix: "ext", | ||
} | ||
|
||
var Facebook = &Scheme{ | ||
Prefix: "facebook", | ||
Validate: func(path string) bool { | ||
// we don't validate facebook refs since they come from the outside | ||
if strings.HasPrefix(path, FacebookRefPrefix) { | ||
return true | ||
} | ||
// otherwise, this should be an int | ||
return allDigitsRegex.MatchString(path) | ||
}, | ||
} | ||
|
||
var Firebase = &Scheme{ | ||
Prefix: "fcm", | ||
} | ||
|
||
var FreshChat = &Scheme{ | ||
Prefix: "freshchat", | ||
Validate: func(path string) bool { return freshchatRegex.MatchString(path) }, | ||
} | ||
|
||
var Instagram = &Scheme{ | ||
Prefix: "instagram", | ||
Validate: func(path string) bool { return allDigitsRegex.MatchString(path) }, | ||
} | ||
|
||
var JioChat = &Scheme{ | ||
Prefix: "jiochat", | ||
Validate: func(path string) bool { return allDigitsRegex.MatchString(path) }, | ||
} | ||
|
||
var Line = &Scheme{ | ||
Prefix: "line", | ||
Validate: func(path string) bool { return lineRegex.MatchString(path) }, | ||
} | ||
|
||
var Phone = &Scheme{ | ||
Prefix: "tel", | ||
Normalize: func(path string) string { | ||
e164, err := ParsePhone(path, "") | ||
if err != nil { | ||
// could be a short code so uppercase and remove non alphanumeric characters | ||
return nonTelCharsRegex.ReplaceAllString(strings.ToUpper(path), "") | ||
} | ||
|
||
return e164 | ||
}, | ||
Validate: func(path string) bool { return telRegex.MatchString(path) }, | ||
Format: func(path string) string { | ||
parsed, err := phonenumbers.Parse(path, "") | ||
if err != nil { | ||
return path | ||
} | ||
return phonenumbers.Format(parsed, phonenumbers.NATIONAL) | ||
}, | ||
} | ||
|
||
var RocketChat = &Scheme{ | ||
Prefix: "rocketchat", | ||
} | ||
|
||
var Slack = &Scheme{ | ||
Prefix: "slack", | ||
} | ||
|
||
var Telegram = &Scheme{ | ||
Prefix: "telegram", | ||
Validate: func(path string) bool { return allDigitsRegex.MatchString(path) }, | ||
} | ||
|
||
var Twitter = &Scheme{ | ||
Prefix: "twitter", | ||
Normalize: func(path string) string { | ||
// handles are case-insensitive, so we always store as lowercase | ||
path = strings.ToLower(path) | ||
|
||
// strip @ prefix if provided | ||
return strings.TrimPrefix(path, "@") | ||
}, | ||
Validate: func(path string) bool { return twitterHandleRegex.MatchString(path) }, | ||
} | ||
|
||
var TwitterID = &Scheme{ | ||
Prefix: "twitterid", | ||
Validate: func(path string) bool { return allDigitsRegex.MatchString(path) }, | ||
} | ||
|
||
var Viber = &Scheme{ | ||
Prefix: "viber", | ||
Validate: func(path string) bool { return viberRegex.MatchString(path) }, | ||
} | ||
|
||
var VK = &Scheme{ | ||
Prefix: "vk", | ||
} | ||
|
||
var WebChat = &Scheme{ | ||
Prefix: "webchat", | ||
Validate: func(path string) bool { return webchatRegex.MatchString(path) }, | ||
} | ||
|
||
var WeChat = &Scheme{ | ||
Prefix: "wechat", | ||
} | ||
|
||
var WhatsApp = &Scheme{ | ||
Prefix: "whatsapp", | ||
Validate: func(path string) bool { return allDigitsRegex.MatchString(path) }, | ||
} |
Oops, something went wrong.