fix: canonicalize phone numbers as E.164

This commit is contained in:
richjohnrambo
2026-08-18 01:23:18 +08:00
parent bf562c7d56
commit e063e7879e
7 changed files with 103 additions and 6 deletions
+15 -1
View File
@@ -1684,7 +1684,12 @@ func (a *adapter) UserCreate(user *t.User) error {
}
var phone any
if strings.TrimSpace(user.Phone) != "" {
phone = strings.TrimSpace(user.Phone)
canonicalPhone, err := normalizeUserPhone(user.Phone)
if err != nil {
return err
}
user.Phone = canonicalPhone
phone = canonicalPhone
}
if _, err = tx.Exec("INSERT INTO im_users(id,createdat,updatedat,state,access,phone,public,trusted) VALUES(?,?,?,?,?,?,?,?)",
decoded_uid,
@@ -3891,6 +3896,15 @@ func (a *adapter) DeviceDelete(uid t.Uid, deviceID string) error {
// 2.4 Insert new credential record.
func (a *adapter) CredUpsert(cred *t.Credential) (bool, error) {
var err error
if cred == nil {
return false, t.ErrMalformed
}
if cred.Method == "tel" {
cred.Value, err = normalizeUserPhone(cred.Value)
if err != nil {
return false, err
}
}
ctx, cancel := a.getContextForTx()
if cancel != nil {
+23
View File
@@ -3,6 +3,29 @@
package mysql
import (
"strings"
"github.com/nyaruka/phonenumbers"
t "github.com/tinode/chat/server/store/types"
)
func normalizeUserPhone(value string) (string, error) {
value = strings.TrimSpace(strings.TrimPrefix(strings.TrimPrefix(value, "tel:"), "basic:"))
if value == "" {
return "", t.ErrMalformed
}
number, err := phonenumbers.Parse(value, "CN")
if err != nil || !phonenumbers.IsValidNumber(number) {
return "", t.ErrMalformed
}
numberType := phonenumbers.GetNumberType(number)
if numberType != phonenumbers.MOBILE && numberType != phonenumbers.FIXED_LINE_OR_MOBILE {
return "", t.ErrMalformed
}
return phonenumbers.Format(number, phonenumbers.E164), nil
}
func withoutUserTags(update map[string]any) map[string]any {
if update == nil {
return nil
+25 -3
View File
@@ -144,15 +144,37 @@ func backfillUserPhones(ctx context.Context, conn *sqlx.Conn, scoped bool) error
if scoped {
prefix = "tenant_id,"
}
if err = conn.SelectContext(ctx, &existing, "SELECT "+prefix+"id AS user_id,phone AS value FROM im_users WHERE phone IS NOT NULL AND phone<>''"); err != nil {
orderBy := "id"
if scoped {
orderBy = "tenant_id,id"
}
if err = conn.SelectContext(ctx, &existing, "SELECT "+prefix+"id AS user_id,phone AS value FROM im_users WHERE phone IS NOT NULL AND phone<>'' ORDER BY "+orderBy); err != nil {
return err
}
for _, item := range existing {
seen[identityUniqueKey(item.TenantID, item.Value, scoped)] = true
canonical, err := normalizeUserPhone(item.Value)
if err != nil {
return fmt.Errorf("normalize existing phone for user %d: %w", item.UserID, err)
}
if canonical != item.Value {
query := "UPDATE im_users SET phone=? WHERE id=?"
args := []any{canonical, item.UserID}
if scoped {
query = "UPDATE im_users SET phone=? WHERE tenant_id=? AND id=?"
args = []any{canonical, item.TenantID, item.UserID}
}
if _, err = conn.ExecContext(ctx, query, args...); err != nil {
return err
}
}
seen[identityUniqueKey(item.TenantID, canonical, scoped)] = true
}
updatedUsers := make(map[string]bool)
for _, item := range candidates {
item.Value = strings.TrimSpace(strings.TrimPrefix(strings.TrimPrefix(item.Value, "tel:"), "basic:"))
item.Value, err = normalizeUserPhone(item.Value)
if err != nil {
return fmt.Errorf("normalize legacy phone for user %d: %w", item.UserID, err)
}
userKey := identityUserKey(item.TenantID, item.UserID, scoped)
valueKey := identityUniqueKey(item.TenantID, item.Value, scoped)
if item.Value == "" || updatedUsers[userKey] || seen[valueKey] {
+25
View File
@@ -0,0 +1,25 @@
//go:build mysql
// +build mysql
package mysql
import "testing"
func TestNormalizeUserPhone(t *testing.T) {
for input, want := range map[string]string{
"13800138000": "+8613800138000",
"+8613800138000": "+8613800138000",
"tel:+17025550001": "+17025550001",
} {
got, err := normalizeUserPhone(input)
if err != nil {
t.Fatalf("normalize %q: %v", input, err)
}
if got != want {
t.Fatalf("normalize %q: got %q, want %q", input, got, want)
}
}
if _, err := normalizeUserPhone("12345"); err == nil {
t.Fatal("invalid phone unexpectedly accepted")
}
}
+1 -1
View File
@@ -129,7 +129,7 @@ func (*validator) PreCheck(cred string, params map[string]any) (string, error) {
}
countryCode, ok := params["countryCode"].(string)
if !ok {
countryCode = "US"
countryCode = "CN"
}
number, err := phonenumbers.Parse(cred, countryCode)
if err != nil {
+13
View File
@@ -0,0 +1,13 @@
package tel
import "testing"
func TestPreCheckDefaultsToChina(t *testing.T) {
got, err := (&validator{}).PreCheck("13800138000", map[string]any{})
if err != nil {
t.Fatal(err)
}
if got != "tel:+8613800138000" {
t.Fatalf("got %q, want tel:+8613800138000", got)
}
}