···2233import (
44 "net/http"
55- "regexp"
65 "strings"
7687 "github.com/go-chi/chi/v5"
88+ "github.com/sotangled/tangled/appview/state/userutil"
99)
10101111func (s *State) Router() http.Handler {
···1919 // Check if the first path element is a valid handle without '@' or a flattened DID
2020 pathParts := strings.SplitN(pat, "/", 2)
2121 if len(pathParts) > 0 {
2222- if isHandleNoAt(pathParts[0]) {
2222+ if userutil.IsHandleNoAt(pathParts[0]) {
2323 // Redirect to the same path but with '@' prefixed to the handle
2424 redirectPath := "@" + pat
2525 http.Redirect(w, r, "/"+redirectPath, http.StatusFound)
2626 return
2727- } else if isFlattenedDid(pathParts[0]) {
2727+ } else if userutil.IsFlattenedDid(pathParts[0]) {
2828 // Redirect to the unflattened DID version
2929- unflattenedDid := unflattenDid(pathParts[0])
2929+ unflattenedDid := userutil.UnflattenDid(pathParts[0])
3030 var redirectPath string
3131 if len(pathParts) > 1 {
3232 redirectPath = unflattenedDid + "/" + pathParts[1]
···4242 })
43434444 return router
4545-}
4646-4747-func isHandleNoAt(s string) bool {
4848- // ref: https://atproto.com/specs/handle
4949- re := 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])?$`)
5050- return re.MatchString(s)
5151-}
5252-5353-func unflattenDid(s string) string {
5454- if !isFlattenedDid(s) {
5555- return s
5656- }
5757-5858- parts := strings.SplitN(s[4:], "-", 2) // Skip "did-" prefix and split on first "-"
5959- if len(parts) != 2 {
6060- return s
6161- }
6262-6363- return "did:" + parts[0] + ":" + parts[1]
6464-}
6565-6666-// isFlattenedDid checks if the given string is a flattened DID.
6767-// A flattened DID is a DID with the :s swapped to -s to satisfy certain
6868-// application requirements, such as Go module naming conventions.
6969-func isFlattenedDid(s string) bool {
7070- // Check if the string starts with "did-"
7171- if !strings.HasPrefix(s, "did-") {
7272- return false
7373- }
7474-7575- // Split the string to extract method and identifier
7676- parts := strings.SplitN(s[4:], "-", 2) // Skip "did-" prefix and split on first "-"
7777- if len(parts) != 2 {
7878- return false
7979- }
8080-8181- // Reconstruct as a standard DID format
8282- // Example: "did-plc-xyz-abc" becomes "did:plc:xyz-abc"
8383- reconstructed := "did:" + parts[0] + ":" + parts[1]
8484- re := regexp.MustCompile(`^did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-]$`)
8585-8686- return re.MatchString(reconstructed)
8745}
88468947func (s *State) UserRouter() http.Handler {
···11-package state
11+package userutil
2233import "testing"
44···36363737 for _, tc := range tests {
3838 t.Run(tc.name, func(t *testing.T) {
3939- result := unflattenDid(tc.input)
3939+ result := UnflattenDid(tc.input)
4040 if result != tc.expected {
4141 t.Errorf("unflattenDid(%q) = %q, want %q", tc.input, result, tc.expected)
4242 }
···105105func TestIsFlattenedDid(t *testing.T) {
106106 for _, tc := range isFlattenedDidTests {
107107 t.Run(tc.name, func(t *testing.T) {
108108- result := isFlattenedDid(tc.input)
108108+ result := IsFlattenedDid(tc.input)
109109 if result != tc.expected {
110110 t.Errorf("isFlattenedDid(%q) = %v, want %v", tc.input, result, tc.expected)
111111 }
+62
appview/state/userutil/userutil.go
···11+package userutil
22+33+import (
44+ "regexp"
55+ "strings"
66+)
77+88+func IsHandleNoAt(s string) bool {
99+ // ref: https://atproto.com/specs/handle
1010+ re := 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])?$`)
1111+ return re.MatchString(s)
1212+}
1313+1414+func UnflattenDid(s string) string {
1515+ if !IsFlattenedDid(s) {
1616+ return s
1717+ }
1818+1919+ parts := strings.SplitN(s[4:], "-", 2) // Skip "did-" prefix and split on first "-"
2020+ if len(parts) != 2 {
2121+ return s
2222+ }
2323+2424+ return "did:" + parts[0] + ":" + parts[1]
2525+}
2626+2727+// IsFlattenedDid checks if the given string is a flattened DID.
2828+func IsFlattenedDid(s string) bool {
2929+ // Check if the string starts with "did-"
3030+ if !strings.HasPrefix(s, "did-") {
3131+ return false
3232+ }
3333+3434+ // Split the string to extract method and identifier
3535+ parts := strings.SplitN(s[4:], "-", 2) // Skip "did-" prefix and split on first "-"
3636+ if len(parts) != 2 {
3737+ return false
3838+ }
3939+4040+ // Reconstruct as a standard DID format
4141+ // Example: "did-plc-xyz-abc" becomes "did:plc:xyz-abc"
4242+ reconstructed := "did:" + parts[0] + ":" + parts[1]
4343+ re := regexp.MustCompile(`^did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-]$`)
4444+4545+ return re.MatchString(reconstructed)
4646+}
4747+4848+// FlattenDid converts a DID to a flattened format.
4949+// A flattened DID is a DID with the :s swapped to -s to satisfy certain
5050+// application requirements, such as Go module naming conventions.
5151+func FlattenDid(s string) string {
5252+ if !IsFlattenedDid(s) {
5353+ return s
5454+ }
5555+5656+ parts := strings.SplitN(s[4:], ":", 2) // Skip "did:" prefix and split on first ":"
5757+ if len(parts) != 2 {
5858+ return s
5959+ }
6060+6161+ return "did-" + parts[0] + "-" + parts[1]
6262+}