This repository has no description
1package userutil
2
3import (
4 "bufio"
5 "log/slog"
6 "os"
7 "regexp"
8 "strings"
9)
10
11var (
12 handleRegex = regexp.MustCompile(`^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$`)
13 didRegex = regexp.MustCompile(`^did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-]$`)
14)
15
16func IsHandle(s string) bool {
17 // ref: https://atproto.com/specs/handle
18 return handleRegex.MatchString(s)
19}
20
21// IsDid checks if the given string is a standard DID.
22func IsDid(s string) bool {
23 return didRegex.MatchString(s)
24}
25
26func UnflattenDid(s string) string {
27 if !IsFlattenedDid(s) {
28 return s
29 }
30
31 return strings.Replace(s, "-", ":", 2)
32}
33
34// IsFlattenedDid checks if the given string is a flattened DID.
35func IsFlattenedDid(s string) bool {
36 // Check if the string starts with "did-"
37 if !strings.HasPrefix(s, "did-") {
38 return false
39 }
40
41 // Reconstruct as a standard DID format using Replace
42 // Example: "did-plc-xyz-abc" becomes "did:plc:xyz-abc"
43 reconstructed := strings.Replace(s, "-", ":", 2)
44
45 return didRegex.MatchString(reconstructed)
46}
47
48// FlattenDid converts a DID to a flattened format.
49// A flattened DID is a DID with the :s swapped to -s to satisfy certain
50// application requirements, such as Go module naming conventions.
51func FlattenDid(s string) string {
52 if IsDid(s) {
53 return strings.Replace(s, ":", "-", 2)
54 }
55 return s
56}
57
58var subdomainRegex = regexp.MustCompile(`^[a-z0-9]([a-z0-9-]{2,61}[a-z0-9])?$`)
59
60func IsValidSubdomain(name string) bool {
61 return len(name) >= 4 && len(name) <= 63 && subdomainRegex.MatchString(name)
62}
63
64// LoadDisallowedNicknames reads a newline-separated list of reserved nicknames
65// from filepath (blank lines and #-comments ignored). An empty filepath or a
66// read error yields an empty set. Invalid entries are logged and skipped.
67func LoadDisallowedNicknames(filepath string, logger *slog.Logger) map[string]bool {
68 disallowed := make(map[string]bool)
69
70 if filepath == "" {
71 logger.Warn("no disallowed nicknames file configured")
72 return disallowed
73 }
74
75 file, err := os.Open(filepath)
76 if err != nil {
77 logger.Warn("failed to open disallowed nicknames file", "file", filepath, "error", err)
78 return disallowed
79 }
80 defer file.Close()
81
82 scanner := bufio.NewScanner(file)
83 lineNum := 0
84 for scanner.Scan() {
85 lineNum++
86 line := strings.TrimSpace(scanner.Text())
87 if line == "" || strings.HasPrefix(line, "#") {
88 continue
89 }
90
91 nickname := strings.ToLower(line)
92 if IsValidSubdomain(nickname) {
93 disallowed[nickname] = true
94 } else {
95 logger.Warn("invalid nickname format in disallowed nicknames file",
96 "file", filepath, "line", lineNum, "nickname", nickname)
97 }
98 }
99
100 if err := scanner.Err(); err != nil {
101 logger.Error("error reading disallowed nicknames file", "file", filepath, "error", err)
102 }
103
104 logger.Info("loaded disallowed nicknames", "count", len(disallowed), "file", filepath)
105 return disallowed
106}