+14
-1
appview/pages/pages.go
+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
+2
appview/pages/templates/layouts/base.html
+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
+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">
+4
-46
appview/state/router.go
+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 {
+3
-3
appview/state/router_test.go
appview/state/userutil/userutil_test.go
+3
-3
appview/state/router_test.go
appview/state/userutil/userutil_test.go
···
1
-
package state
1
+
package userutil
2
2
3
3
import "testing"
4
4
···
36
36
37
37
for _, tc := range tests {
38
38
t.Run(tc.name, func(t *testing.T) {
39
-
result := unflattenDid(tc.input)
39
+
result := UnflattenDid(tc.input)
40
40
if result != tc.expected {
41
41
t.Errorf("unflattenDid(%q) = %q, want %q", tc.input, result, tc.expected)
42
42
}
···
105
105
func TestIsFlattenedDid(t *testing.T) {
106
106
for _, tc := range isFlattenedDidTests {
107
107
t.Run(tc.name, func(t *testing.T) {
108
-
result := isFlattenedDid(tc.input)
108
+
result := IsFlattenedDid(tc.input)
109
109
if result != tc.expected {
110
110
t.Errorf("isFlattenedDid(%q) = %v, want %v", tc.input, result, tc.expected)
111
111
}
+62
appview/state/userutil/userutil.go
+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
+
}