first commit
This commit is contained in:
101
internal/user/nostr_sync.go
Normal file
101
internal/user/nostr_sync.go
Normal file
@@ -0,0 +1,101 @@
|
||||
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 ""
|
||||
}
|
||||
Reference in New Issue
Block a user