From 3d47ba6ee3f62b2ea703609a8855e047a7020511 Mon Sep 17 00:00:00 2001
From: oppiliappan
Date: Tue, 14 Oct 2025 13:46:13 +0000
Subject: [PATCH] appview: remove `@` from URLs and interface
old URLs that refer to users with the `@` are redirected to the version
without `@`. the leading motivation for this change is that valid
atproto handles do not contain the prefix. it is purely stylistic.
Signed-off-by: oppiliappan
---
appview/middleware/middleware.go | 4 ++--
appview/pages/funcmap.go | 7 ++++---
appview/state/router.go | 50 ++++++++++++++++++++++++++------------------------
appview/state/state.go | 5 +++--
appview/pages/repoinfo/repoinfo.go | 12 +++++-------
appview/state/userutil/userutil.go | 12 ++++++------
appview/pages/templates/layouts/repobase.html | 4 ++--
appview/pages/templates/repo/fragments/cloneDropdown.html | 4 ++--
8 file(s) changed, 50 insertion(s)(+), 48 deletion(s)(-)
diff --git a/appview/middleware/middleware.go b/appview/middleware/middleware.go
--- a/appview/middleware/middleware.go
+++ b/appview/middleware/middleware.go
@@ -180,12 +180,12 @@
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
didOrHandle := chi.URLParam(req, "user")
+ didOrHandle = strings.TrimPrefix(didOrHandle, "@")
+
if slices.Contains(excluded, didOrHandle) {
next.ServeHTTP(w, req)
return
}
-
- didOrHandle = strings.TrimPrefix(didOrHandle, "@")
id, err := mw.idResolver.ResolveIdent(req.Context(), didOrHandle)
if err != nil {
diff --git a/appview/pages/funcmap.go b/appview/pages/funcmap.go
--- a/appview/pages/funcmap.go
+++ b/appview/pages/funcmap.go
@@ -17,6 +17,7 @@
"strings"
"time"
+ "github.com/bluesky-social/indigo/atproto/syntax"
"github.com/dustin/go-humanize"
"github.com/go-enry/go-enry/v2"
"tangled.org/core/appview/filetree"
@@ -63,7 +64,7 @@
return "handle.invalid"
}
- return "@" + identity.Handle.String()
+ return identity.Handle.String()
},
"truncateAt30": func(s string) string {
if len(s) <= 30 {
@@ -123,8 +124,8 @@
return b
},
"didOrHandle": func(did, handle string) string {
- if handle != "" {
- return fmt.Sprintf("@%s", handle)
+ if handle != "" && handle != syntax.HandleInvalid.String() {
+ return handle
} else {
return did
}
diff --git a/appview/state/router.go b/appview/state/router.go
--- a/appview/state/router.go
+++ b/appview/state/router.go
@@ -42,32 +42,34 @@
router.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) {
pat := chi.URLParam(r, "*")
- if strings.HasPrefix(pat, "did:") || strings.HasPrefix(pat, "@") {
- userRouter.ServeHTTP(w, r)
- } else {
- // Check if the first path element is a valid handle without '@' or a flattened DID
- pathParts := strings.SplitN(pat, "/", 2)
- if len(pathParts) > 0 {
- if userutil.IsHandleNoAt(pathParts[0]) {
- // Redirect to the same path but with '@' prefixed to the handle
- redirectPath := "@" + pat
- http.Redirect(w, r, "/"+redirectPath, http.StatusFound)
- return
- } else if userutil.IsFlattenedDid(pathParts[0]) {
- // Redirect to the unflattened DID version
- unflattenedDid := userutil.UnflattenDid(pathParts[0])
- var redirectPath string
- if len(pathParts) > 1 {
- redirectPath = unflattenedDid + "/" + pathParts[1]
- } else {
- redirectPath = unflattenedDid
- }
- http.Redirect(w, r, "/"+redirectPath, http.StatusFound)
- return
- }
+ pathParts := strings.SplitN(pat, "/", 2)
+
+ if len(pathParts) > 0 {
+ firstPart := pathParts[0]
+
+ // if using a DID or handle, just continue as per usual
+ if userutil.IsDid(firstPart) || userutil.IsHandle(firstPart) {
+ userRouter.ServeHTTP(w, r)
+ return
}
- standardRouter.ServeHTTP(w, r)
+
+ // if using a flattened DID (like you would in go modules), unflatten
+ if userutil.IsFlattenedDid(firstPart) {
+ unflattenedDid := userutil.UnflattenDid(firstPart)
+ redirectPath := strings.Join(append([]string{unflattenedDid}, pathParts[1:]...), "/")
+ http.Redirect(w, r, "/"+redirectPath, http.StatusFound)
+ return
+ }
+
+ // if using a handle with @, rewrite to work without @
+ if normalized := strings.TrimPrefix(firstPart, "@"); userutil.IsHandle(normalized) {
+ redirectPath := strings.Join(append([]string{normalized}, pathParts[1:]...), "/")
+ http.Redirect(w, r, "/"+redirectPath, http.StatusFound)
+ return
+ }
}
+
+ standardRouter.ServeHTTP(w, r)
})
return router
diff --git a/appview/state/state.go b/appview/state/state.go
--- a/appview/state/state.go
+++ b/appview/state/state.go
@@ -386,12 +386,13 @@
pubKeys, err := db.GetPublicKeysForDid(s.db, id.DID.String())
if err != nil {
- w.WriteHeader(http.StatusNotFound)
+ s.logger.Error("failed to get public keys", "err", err)
+ http.Error(w, "failed to get public keys", http.StatusInternalServerError)
return
}
if len(pubKeys) == 0 {
- w.WriteHeader(http.StatusNotFound)
+ w.WriteHeader(http.StatusNoContent)
return
}
diff --git a/appview/pages/repoinfo/repoinfo.go b/appview/pages/repoinfo/repoinfo.go
--- a/appview/pages/repoinfo/repoinfo.go
+++ b/appview/pages/repoinfo/repoinfo.go
@@ -1,31 +1,29 @@
package repoinfo
import (
- "fmt"
"path"
"slices"
- "strings"
"github.com/bluesky-social/indigo/atproto/syntax"
"tangled.org/core/appview/models"
"tangled.org/core/appview/state/userutil"
)
-func (r RepoInfo) OwnerWithAt() string {
+func (r RepoInfo) Owner() string {
if r.OwnerHandle != "" {
- return fmt.Sprintf("@%s", r.OwnerHandle)
+ return r.OwnerHandle
} else {
return r.OwnerDid
}
}
func (r RepoInfo) FullName() string {
- return path.Join(r.OwnerWithAt(), r.Name)
+ return path.Join(r.Owner(), r.Name)
}
func (r RepoInfo) OwnerWithoutAt() string {
- if after, ok := strings.CutPrefix(r.OwnerWithAt(), "@"); ok {
- return after
+ if r.OwnerHandle != "" {
+ return r.OwnerHandle
} else {
return userutil.FlattenDid(r.OwnerDid)
}
diff --git a/appview/state/userutil/userutil.go b/appview/state/userutil/userutil.go
--- a/appview/state/userutil/userutil.go
+++ b/appview/state/userutil/userutil.go
@@ -10,9 +10,14 @@
didRegex = regexp.MustCompile(`^did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-]$`)
)
-func IsHandleNoAt(s string) bool {
+func IsHandle(s string) bool {
// ref: https://atproto.com/specs/handle
return handleRegex.MatchString(s)
+}
+
+// IsDid checks if the given string is a standard DID.
+func IsDid(s string) bool {
+ return didRegex.MatchString(s)
}
func UnflattenDid(s string) string {
@@ -45,11 +50,6 @@
return strings.Replace(s, ":", "-", 2)
}
return s
-}
-
-// IsDid checks if the given string is a standard DID.
-func IsDid(s string) bool {
- return didRegex.MatchString(s)
}
var subdomainRegex = regexp.MustCompile(`^[a-z0-9]([a-z0-9-]{2,61}[a-z0-9])?$`)
diff --git a/appview/pages/templates/layouts/repobase.html b/appview/pages/templates/layouts/repobase.html
--- a/appview/pages/templates/layouts/repobase.html
+++ b/appview/pages/templates/layouts/repobase.html
@@ -13,8 +13,8 @@
{{ end }}
-
-
{{ .RepoInfo.OwnerWithAt }}
+
diff --git a/appview/pages/templates/repo/fragments/cloneDropdown.html b/appview/pages/templates/repo/fragments/cloneDropdown.html
--- a/appview/pages/templates/repo/fragments/cloneDropdown.html
+++ b/appview/pages/templates/repo/fragments/cloneDropdown.html
@@ -29,8 +29,8 @@
https://tangled.org/{{ .RepoInfo.OwnerWithAt }}/{{ .RepoInfo.Name }}
+ data-url="https://tangled.org/{{ resolve .RepoInfo.OwnerDid }}/{{ .RepoInfo.Name }}"
+ >https://tangled.org/{{ resolve .RepoInfo.OwnerDid }}/{{ .RepoInfo.Name }}