···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 DID2020 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 handle2424 redirectPath := "@" + pat2525 http.Redirect(w, r, "/"+redirectPath, http.StatusFound)2626 return2727- } else if isFlattenedDid(pathParts[0]) {2727+ } else if userutil.IsFlattenedDid(pathParts[0]) {2828 // Redirect to the unflattened DID version2929- unflattenedDid := unflattenDid(pathParts[0])2929+ unflattenedDid := userutil.UnflattenDid(pathParts[0])3030 var redirectPath string3131 if len(pathParts) > 1 {3232 redirectPath = unflattenedDid + "/" + pathParts[1]···4242 })43434444 return router4545-}4646-4747-func isHandleNoAt(s string) bool {4848- // ref: https://atproto.com/specs/handle4949- 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 s5656- }5757-5858- parts := strings.SplitN(s[4:], "-", 2) // Skip "did-" prefix and split on first "-"5959- if len(parts) != 2 {6060- return s6161- }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 certain6868-// 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 false7373- }7474-7575- // Split the string to extract method and identifier7676- parts := strings.SplitN(s[4:], "-", 2) // Skip "did-" prefix and split on first "-"7777- if len(parts) != 2 {7878- return false7979- }8080-8181- // Reconstruct as a standard DID format8282- // 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 state11+package userutil2233import "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 userutil22+33+import (44+ "regexp"55+ "strings"66+)77+88+func IsHandleNoAt(s string) bool {99+ // ref: https://atproto.com/specs/handle1010+ 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 s1717+ }1818+1919+ parts := strings.SplitN(s[4:], "-", 2) // Skip "did-" prefix and split on first "-"2020+ if len(parts) != 2 {2121+ return s2222+ }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 false3232+ }3333+3434+ // Split the string to extract method and identifier3535+ parts := strings.SplitN(s[4:], "-", 2) // Skip "did-" prefix and split on first "-"3636+ if len(parts) != 2 {3737+ return false3838+ }3939+4040+ // Reconstruct as a standard DID format4141+ // 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 certain5050+// application requirements, such as Go module naming conventions.5151+func FlattenDid(s string) string {5252+ if !IsFlattenedDid(s) {5353+ return s5454+ }5555+5656+ parts := strings.SplitN(s[4:], ":", 2) // Skip "did:" prefix and split on first ":"5757+ if len(parts) != 2 {5858+ return s5959+ }6060+6161+ return "did-" + parts[0] + "-" + parts[1]6262+}