diff --git a/internal/user/model.go b/internal/user/model.go index 18565f8..e3cfc73 100644 --- a/internal/user/model.go +++ b/internal/user/model.go @@ -42,8 +42,9 @@ var ( ErrUsernameTaken = errors.New("username taken") ) -// Username rules: 1-30 chars, [a-z0-9_-], lowercase, must start with alnum. -var usernameRE = regexp.MustCompile(`^[a-z0-9][a-z0-9_-]{0,29}$`) +// Username rules: 1-30 chars, [a-z0-9_.-], lowercase, must start with alnum. +// Dot is allowed per NIP-05 local-part spec. +var usernameRE = regexp.MustCompile(`^[a-z0-9][a-z0-9_.-]{0,29}$`) func ValidateUsername(name string, reserved []string) error { name = strings.ToLower(strings.TrimSpace(name)) diff --git a/internal/user/model_test.go b/internal/user/model_test.go index 8ed52d2..c6f24b1 100644 --- a/internal/user/model_test.go +++ b/internal/user/model_test.go @@ -10,9 +10,12 @@ func TestValidateUsername(t *testing.T) { {"alice", true}, {"al-ice_42", true}, {"a", true}, + {"alice.bob", true}, + {"alice.smith.42", true}, {"", false}, {"-alice", false}, {"_alice", false}, + {".alice", false}, {"thisusernameiswaytoolongtobevalid12345", false}, {"admin", false}, } diff --git a/internal/user/nostr_sync.go b/internal/user/nostr_sync.go index e448d00..a409cca 100644 --- a/internal/user/nostr_sync.go +++ b/internal/user/nostr_sync.go @@ -5,7 +5,7 @@ import ( ) // SanitizeForUsername coerces an arbitrary profile string into a candidate -// that matches usernameRE: lowercase ASCII alphanumerics, `_`, and `-`, +// that matches usernameRE: lowercase ASCII alphanumerics, `_`, `-`, and `.`, // length <= 30, with an alphanumeric first character. Returns "" when no // usable handle can be derived. func SanitizeForUsername(s string) string { @@ -22,7 +22,7 @@ func SanitizeForUsername(s string) string { case (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9'): b.WriteRune(r) prevSep = false - case r == '-' || r == '_': + case r == '-' || r == '_' || r == '.': if b.Len() == 0 { continue } @@ -42,9 +42,9 @@ func SanitizeForUsername(s string) string { prevSep = true } } - out := strings.TrimRight(b.String(), "_-") + out := strings.TrimRight(b.String(), "_-.") if len(out) > 30 { - out = strings.TrimRight(out[:30], "_-") + out = strings.TrimRight(out[:30], "_-.") } return out } diff --git a/internal/user/nostr_sync_test.go b/internal/user/nostr_sync_test.go index 1ad0236..562367c 100644 --- a/internal/user/nostr_sync_test.go +++ b/internal/user/nostr_sync_test.go @@ -17,6 +17,10 @@ func TestSanitizeForUsername(t *testing.T) { {"alice_", "alice"}, {"alice--bob", "alice-bob"}, {"alice__bob", "alice_bob"}, + {"alice.bob", "alice.bob"}, + {"alice..bob", "alice.bob"}, + {".alice", "alice"}, + {"alice.", "alice"}, {" ", ""}, {"", ""}, {"!!!", ""}, @@ -59,6 +63,11 @@ func TestCandidateFromMetadataPrecedence(t *testing.T) { name: "alice", nip05: "preferred@azzamo.net", dom: domain, want: "preferred", }, + { + desc: "nip05 local part with dot is preserved", + name: "alice", nip05: "alice.smith@azzamo.net", dom: domain, + want: "alice.smith", + }, { desc: "nip05 ignored when domain differs", name: "alice", nip05: "preferred@other.example", dom: domain,