Monorepo for Tangled
0

Configure Feed

Select the types of activity you want to include in your feed.

appview: state/userutil: init new pkg for user handle/did utils

Anirudh Oppiliappan (Mar 24, 2025, 1:26 PM +0200) bdc676d6 58349244

+205 -161
+14 -1
appview/pages/pages.go
··· 22 22 "github.com/microcosm-cc/bluemonday" 23 23 "github.com/sotangled/tangled/appview/auth" 24 24 "github.com/sotangled/tangled/appview/db" 25 + "github.com/sotangled/tangled/appview/state/userutil" 25 26 "github.com/sotangled/tangled/types" 26 27 ) 27 28 ··· 252 253 return path.Join(r.OwnerWithAt(), r.Name) 253 254 } 254 255 256 + func (r RepoInfo) OwnerWithoutAt() string { 257 + if strings.HasPrefix(r.OwnerWithAt(), "@") { 258 + return strings.TrimPrefix(r.OwnerWithAt(), "@") 259 + } else { 260 + return userutil.FlattenDid(r.OwnerDid) 261 + } 262 + } 263 + 264 + func (r RepoInfo) FullNameWithoutAt() string { 265 + return path.Join(r.OwnerWithoutAt(), r.Name) 266 + } 267 + 255 268 func (r RepoInfo) GetTabs() [][]string { 256 269 tabs := [][]string{ 257 270 {"overview", "/"}, ··· 328 341 LoggedInUser *auth.User 329 342 RepoInfo RepoInfo 330 343 types.RepoLogResponse 331 - Active string 344 + Active string 332 345 EmailToDidOrHandle map[string]string 333 346 } 334 347
+4 -46
appview/state/router.go
··· 2 2 3 3 import ( 4 4 "net/http" 5 - "regexp" 6 5 "strings" 7 6 8 7 "github.com/go-chi/chi/v5" 8 + "github.com/sotangled/tangled/appview/state/userutil" 9 9 ) 10 10 11 11 func (s *State) Router() http.Handler { ··· 19 19 // Check if the first path element is a valid handle without '@' or a flattened DID 20 20 pathParts := strings.SplitN(pat, "/", 2) 21 21 if len(pathParts) > 0 { 22 - if isHandleNoAt(pathParts[0]) { 22 + if userutil.IsHandleNoAt(pathParts[0]) { 23 23 // Redirect to the same path but with '@' prefixed to the handle 24 24 redirectPath := "@" + pat 25 25 http.Redirect(w, r, "/"+redirectPath, http.StatusFound) 26 26 return 27 - } else if isFlattenedDid(pathParts[0]) { 27 + } else if userutil.IsFlattenedDid(pathParts[0]) { 28 28 // Redirect to the unflattened DID version 29 - unflattenedDid := unflattenDid(pathParts[0]) 29 + unflattenedDid := userutil.UnflattenDid(pathParts[0]) 30 30 var redirectPath string 31 31 if len(pathParts) > 1 { 32 32 redirectPath = unflattenedDid + "/" + pathParts[1] ··· 42 42 }) 43 43 44 44 return router 45 - } 46 - 47 - func isHandleNoAt(s string) bool { 48 - // ref: https://atproto.com/specs/handle 49 - 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])?$`) 50 - return re.MatchString(s) 51 - } 52 - 53 - func unflattenDid(s string) string { 54 - if !isFlattenedDid(s) { 55 - return s 56 - } 57 - 58 - parts := strings.SplitN(s[4:], "-", 2) // Skip "did-" prefix and split on first "-" 59 - if len(parts) != 2 { 60 - return s 61 - } 62 - 63 - return "did:" + parts[0] + ":" + parts[1] 64 - } 65 - 66 - // isFlattenedDid checks if the given string is a flattened DID. 67 - // A flattened DID is a DID with the :s swapped to -s to satisfy certain 68 - // application requirements, such as Go module naming conventions. 69 - func isFlattenedDid(s string) bool { 70 - // Check if the string starts with "did-" 71 - if !strings.HasPrefix(s, "did-") { 72 - return false 73 - } 74 - 75 - // Split the string to extract method and identifier 76 - parts := strings.SplitN(s[4:], "-", 2) // Skip "did-" prefix and split on first "-" 77 - if len(parts) != 2 { 78 - return false 79 - } 80 - 81 - // Reconstruct as a standard DID format 82 - // Example: "did-plc-xyz-abc" becomes "did:plc:xyz-abc" 83 - reconstructed := "did:" + parts[0] + ":" + parts[1] 84 - re := regexp.MustCompile(`^did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-]$`) 85 - 86 - return re.MatchString(reconstructed) 87 45 } 88 46 89 47 func (s *State) UserRouter() http.Handler {
-114
appview/state/router_test.go
··· 1 - package state 2 - 3 - import "testing" 4 - 5 - func TestUnflattenDid(t *testing.T) { 6 - unflattenedMap := map[string]string{ 7 - "did-plc-abcdefghijklmnopqrstuvwxyz": "did:plc:abcdefghijklmnopqrstuvwxyz", 8 - "did-plc-1234567890": "did:plc:1234567890", 9 - "did-key-z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK", 10 - "did-plc-abcdefghi-jklmnopqr-stuvwxyz": "did:plc:abcdefghi-jklmnopqr-stuvwxyz", 11 - "plc-abcdefghijklmnopqrstuvwxyz": "plc-abcdefghijklmnopqrstuvwxyz", 12 - "didplc-abcdefghijklmnopqrstuvwxyz": "didplc-abcdefghijklmnopqrstuvwxyz", 13 - "": "", 14 - "did-": "did-", 15 - "did:plc:abcdefghijklmnopqrstuvwxyz": "did:plc:abcdefghijklmnopqrstuvwxyz", 16 - "did-invalid$format:something": "did-invalid$format:something", 17 - } 18 - 19 - tests := []struct { 20 - name string 21 - input string 22 - expected string 23 - }{} 24 - 25 - for _, tc := range isFlattenedDidTests { 26 - tests = append(tests, struct { 27 - name string 28 - input string 29 - expected string 30 - }{ 31 - name: tc.name, 32 - input: tc.input, 33 - expected: unflattenedMap[tc.input], 34 - }) 35 - } 36 - 37 - for _, tc := range tests { 38 - t.Run(tc.name, func(t *testing.T) { 39 - result := unflattenDid(tc.input) 40 - if result != tc.expected { 41 - t.Errorf("unflattenDid(%q) = %q, want %q", tc.input, result, tc.expected) 42 - } 43 - }) 44 - } 45 - } 46 - 47 - var isFlattenedDidTests = []struct { 48 - name string 49 - input string 50 - expected bool 51 - }{ 52 - { 53 - name: "valid flattened DID", 54 - input: "did-plc-abcdefghijklmnopqrstuvwxyz", 55 - expected: true, 56 - }, 57 - { 58 - name: "valid flattened DID with numbers", 59 - input: "did-plc-1234567890", 60 - expected: true, 61 - }, 62 - { 63 - name: "valid flattened DID with special characters", 64 - input: "did-key-z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK", 65 - expected: true, 66 - }, 67 - { 68 - name: "valid flattened DID with dashes", 69 - input: "did-plc-abcdefghi-jklmnopqr-stuvwxyz", 70 - expected: true, 71 - }, 72 - 73 - { 74 - name: "doesn't start with did-", 75 - input: "plc-abcdefghijklmnopqrstuvwxyz", 76 - expected: false, 77 - }, 78 - { 79 - name: "no hyphen after did", 80 - input: "didplc-abcdefghijklmnopqrstuvwxyz", 81 - expected: false, 82 - }, 83 - { 84 - name: "empty string", 85 - input: "", 86 - expected: false, 87 - }, 88 - { 89 - name: "only did-", 90 - input: "did-", 91 - expected: false, 92 - }, 93 - { 94 - name: "standard DID format, not flattened", 95 - input: "did:plc:abcdefghijklmnopqrstuvwxyz", 96 - expected: false, 97 - }, 98 - { 99 - name: "invalid reconstructed DID format", 100 - input: "did-invalid$format:something", 101 - expected: false, 102 - }, 103 - } 104 - 105 - func TestIsFlattenedDid(t *testing.T) { 106 - for _, tc := range isFlattenedDidTests { 107 - t.Run(tc.name, func(t *testing.T) { 108 - result := isFlattenedDid(tc.input) 109 - if result != tc.expected { 110 - t.Errorf("isFlattenedDid(%q) = %v, want %v", tc.input, result, tc.expected) 111 - } 112 - }) 113 - } 114 - }
+62
appview/state/userutil/userutil.go
··· 1 + package userutil 2 + 3 + import ( 4 + "regexp" 5 + "strings" 6 + ) 7 + 8 + func IsHandleNoAt(s string) bool { 9 + // ref: https://atproto.com/specs/handle 10 + 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])?$`) 11 + return re.MatchString(s) 12 + } 13 + 14 + func UnflattenDid(s string) string { 15 + if !IsFlattenedDid(s) { 16 + return s 17 + } 18 + 19 + parts := strings.SplitN(s[4:], "-", 2) // Skip "did-" prefix and split on first "-" 20 + if len(parts) != 2 { 21 + return s 22 + } 23 + 24 + return "did:" + parts[0] + ":" + parts[1] 25 + } 26 + 27 + // IsFlattenedDid checks if the given string is a flattened DID. 28 + func IsFlattenedDid(s string) bool { 29 + // Check if the string starts with "did-" 30 + if !strings.HasPrefix(s, "did-") { 31 + return false 32 + } 33 + 34 + // Split the string to extract method and identifier 35 + parts := strings.SplitN(s[4:], "-", 2) // Skip "did-" prefix and split on first "-" 36 + if len(parts) != 2 { 37 + return false 38 + } 39 + 40 + // Reconstruct as a standard DID format 41 + // Example: "did-plc-xyz-abc" becomes "did:plc:xyz-abc" 42 + reconstructed := "did:" + parts[0] + ":" + parts[1] 43 + re := regexp.MustCompile(`^did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-]$`) 44 + 45 + return re.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. 51 + func FlattenDid(s string) string { 52 + if !IsFlattenedDid(s) { 53 + return s 54 + } 55 + 56 + parts := strings.SplitN(s[4:], ":", 2) // Skip "did:" prefix and split on first ":" 57 + if len(parts) != 2 { 58 + return s 59 + } 60 + 61 + return "did-" + parts[0] + "-" + parts[1] 62 + }
+114
appview/state/userutil/userutil_test.go
··· 1 + package userutil 2 + 3 + import "testing" 4 + 5 + func TestUnflattenDid(t *testing.T) { 6 + unflattenedMap := map[string]string{ 7 + "did-plc-abcdefghijklmnopqrstuvwxyz": "did:plc:abcdefghijklmnopqrstuvwxyz", 8 + "did-plc-1234567890": "did:plc:1234567890", 9 + "did-key-z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK", 10 + "did-plc-abcdefghi-jklmnopqr-stuvwxyz": "did:plc:abcdefghi-jklmnopqr-stuvwxyz", 11 + "plc-abcdefghijklmnopqrstuvwxyz": "plc-abcdefghijklmnopqrstuvwxyz", 12 + "didplc-abcdefghijklmnopqrstuvwxyz": "didplc-abcdefghijklmnopqrstuvwxyz", 13 + "": "", 14 + "did-": "did-", 15 + "did:plc:abcdefghijklmnopqrstuvwxyz": "did:plc:abcdefghijklmnopqrstuvwxyz", 16 + "did-invalid$format:something": "did-invalid$format:something", 17 + } 18 + 19 + tests := []struct { 20 + name string 21 + input string 22 + expected string 23 + }{} 24 + 25 + for _, tc := range isFlattenedDidTests { 26 + tests = append(tests, struct { 27 + name string 28 + input string 29 + expected string 30 + }{ 31 + name: tc.name, 32 + input: tc.input, 33 + expected: unflattenedMap[tc.input], 34 + }) 35 + } 36 + 37 + for _, tc := range tests { 38 + t.Run(tc.name, func(t *testing.T) { 39 + result := UnflattenDid(tc.input) 40 + if result != tc.expected { 41 + t.Errorf("unflattenDid(%q) = %q, want %q", tc.input, result, tc.expected) 42 + } 43 + }) 44 + } 45 + } 46 + 47 + var isFlattenedDidTests = []struct { 48 + name string 49 + input string 50 + expected bool 51 + }{ 52 + { 53 + name: "valid flattened DID", 54 + input: "did-plc-abcdefghijklmnopqrstuvwxyz", 55 + expected: true, 56 + }, 57 + { 58 + name: "valid flattened DID with numbers", 59 + input: "did-plc-1234567890", 60 + expected: true, 61 + }, 62 + { 63 + name: "valid flattened DID with special characters", 64 + input: "did-key-z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK", 65 + expected: true, 66 + }, 67 + { 68 + name: "valid flattened DID with dashes", 69 + input: "did-plc-abcdefghi-jklmnopqr-stuvwxyz", 70 + expected: true, 71 + }, 72 + 73 + { 74 + name: "doesn't start with did-", 75 + input: "plc-abcdefghijklmnopqrstuvwxyz", 76 + expected: false, 77 + }, 78 + { 79 + name: "no hyphen after did", 80 + input: "didplc-abcdefghijklmnopqrstuvwxyz", 81 + expected: false, 82 + }, 83 + { 84 + name: "empty string", 85 + input: "", 86 + expected: false, 87 + }, 88 + { 89 + name: "only did-", 90 + input: "did-", 91 + expected: false, 92 + }, 93 + { 94 + name: "standard DID format, not flattened", 95 + input: "did:plc:abcdefghijklmnopqrstuvwxyz", 96 + expected: false, 97 + }, 98 + { 99 + name: "invalid reconstructed DID format", 100 + input: "did-invalid$format:something", 101 + expected: false, 102 + }, 103 + } 104 + 105 + func TestIsFlattenedDid(t *testing.T) { 106 + for _, tc := range isFlattenedDidTests { 107 + t.Run(tc.name, func(t *testing.T) { 108 + result := IsFlattenedDid(tc.input) 109 + if result != tc.expected { 110 + t.Errorf("isFlattenedDid(%q) = %v, want %v", tc.input, result, tc.expected) 111 + } 112 + }) 113 + } 114 + }
+2
appview/pages/templates/layouts/base.html
··· 9 9 /> 10 10 <script src="/static/htmx.min.js"></script> 11 11 <link href="/static/tw.css" rel="stylesheet" type="text/css" /> 12 + 12 13 <title>{{ block "title" . }}{{ end }} · tangled</title> 14 + {{ block "extrameta" . }}{{ end }} 13 15 </head> 14 16 <body class="bg-slate-100"> 15 17 <div class="container mx-auto px-1 pt-4 min-h-screen flex flex-col">
+9
appview/pages/templates/layouts/repobase.html
··· 1 1 {{ define "title" }}{{ .RepoInfo.FullName }}{{ end }} 2 2 3 + {{ define "extrameta" }} 4 + <meta name="vcs:clone" content="https://tangled.sh/{{ .RepoInfo.FullName }}"/> 5 + <meta name="forge:summary" content="https://tangled.sh/{{ .RepoInfo.FullName }}"> 6 + <meta name="forge:dir" content="https://tangled.sh/{{ .RepoInfo.FullName }}/tree/{ref}/{path}"> 7 + <meta name="forge:file" content="https://tangled.sh/{{ .RepoInfo.FullName }}/blob/{ref}/{path}"> 8 + <meta name="forge:line" content="https://tangled.sh/{{ .RepoInfo.FullName }}/blob/{ref}/{path}#L{line}"> 9 + <meta name="go-import" content="tangled.sh/{{ .RepoInfo.FullNameWithoutAt }} git https://tangled.sh/{{ .RepoInfo.FullName }}"> 10 + {{ end }} 11 + 3 12 {{ define "content" }} 4 13 <section id="repo-header" class="mb-4 py-2 px-6"> 5 14 <p class="text-lg">