Files
Nip-05-api/internal/user/nostr_sync.go
2026-04-29 02:35:00 +00:00

102 lines
2.4 KiB
Go

package user
import (
"strings"
)
// SanitizeForUsername coerces an arbitrary profile string into a candidate
// 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 {
s = strings.ToLower(strings.TrimSpace(s))
if s == "" {
return ""
}
var b strings.Builder
b.Grow(len(s))
prevSep := false
for _, r := range s {
switch {
case (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9'):
b.WriteRune(r)
prevSep = false
case r == '-' || r == '_':
if b.Len() == 0 {
continue
}
if prevSep {
continue
}
b.WriteRune(r)
prevSep = true
default:
if b.Len() == 0 {
continue
}
if prevSep {
continue
}
b.WriteRune('_')
prevSep = true
}
}
out := strings.TrimRight(b.String(), "_-")
if len(out) > 30 {
out = strings.TrimRight(out[:30], "_-")
}
return out
}
// nip05LocalPart returns the local part of a NIP-05 identifier (`local@domain`)
// when its domain matches `serviceDomain` (case-insensitive). The bare form
// `local` (no `@`) per NIP-05 implies `_@domain`, which we ignore for sync
// since we cannot prove the domain match.
func nip05LocalPart(nip05, serviceDomain string) string {
nip05 = strings.TrimSpace(nip05)
if nip05 == "" || serviceDomain == "" {
return ""
}
at := strings.LastIndex(nip05, "@")
if at <= 0 || at == len(nip05)-1 {
return ""
}
local := nip05[:at]
domain := nip05[at+1:]
if !strings.EqualFold(domain, serviceDomain) {
return ""
}
return local
}
// CandidateFromMetadata returns a sanitized, validated username derived from a
// kind:0 profile, or "" if no field yields a usable handle. Precedence:
// 1. NIP-05 local part when its domain matches serviceDomain.
// 2. `name` (NIP-01).
// 3. `display_name` / `displayName` (NIP-24).
// 4. Deprecated `username` field.
func CandidateFromMetadata(name, displayName, displayNameAlt, username, nip05, serviceDomain string, reserved []string) string {
tryFields := []string{
nip05LocalPart(nip05, serviceDomain),
name,
displayName,
displayNameAlt,
username,
}
for _, raw := range tryFields {
if raw == "" {
continue
}
c := SanitizeForUsername(raw)
if c == "" {
continue
}
if err := ValidateUsername(c, reserved); err != nil {
continue
}
return c
}
return ""
}