appview: remove @ from URLs and interface #666

merged
opened by oppi.li targeting master from push-ytvszwnnmymo

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 me@oppi.li

Changed files
+50 -48
appview
middleware
pages
repoinfo
templates
layouts
repo
fragments
state
+2 -2
appview/middleware/middleware.go
···
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
didOrHandle := chi.URLParam(req, "user")
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 {
// invalid did or handle
···
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
}
id, err := mw.idResolver.ResolveIdent(req.Context(), didOrHandle)
if err != nil {
// invalid did or handle
+4 -3
appview/pages/funcmap.go
···
"strings"
"time"
"github.com/dustin/go-humanize"
"github.com/go-enry/go-enry/v2"
"tangled.org/core/appview/filetree"
···
return "handle.invalid"
}
-
return "@" + identity.Handle.String()
},
"truncateAt30": func(s string) string {
if len(s) <= 30 {
···
return b
},
"didOrHandle": func(did, handle string) string {
-
if handle != "" {
-
return fmt.Sprintf("@%s", handle)
} else {
return did
}
···
"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"
···
return "handle.invalid"
}
+
return identity.Handle.String()
},
"truncateAt30": func(s string) string {
if len(s) <= 30 {
···
return b
},
"didOrHandle": func(did, handle string) string {
+
if handle != "" && handle != syntax.HandleInvalid.String() {
+
return handle
} else {
return did
}
+5 -7
appview/pages/repoinfo/repoinfo.go
···
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 {
if r.OwnerHandle != "" {
-
return fmt.Sprintf("@%s", r.OwnerHandle)
} else {
return r.OwnerDid
}
}
func (r RepoInfo) FullName() string {
-
return path.Join(r.OwnerWithAt(), r.Name)
}
func (r RepoInfo) OwnerWithoutAt() string {
-
if after, ok := strings.CutPrefix(r.OwnerWithAt(), "@"); ok {
-
return after
} else {
return userutil.FlattenDid(r.OwnerDid)
}
···
package repoinfo
import (
"path"
"slices"
"github.com/bluesky-social/indigo/atproto/syntax"
"tangled.org/core/appview/models"
"tangled.org/core/appview/state/userutil"
)
+
func (r RepoInfo) Owner() string {
if r.OwnerHandle != "" {
+
return r.OwnerHandle
} else {
return r.OwnerDid
}
}
func (r RepoInfo) FullName() string {
+
return path.Join(r.Owner(), r.Name)
}
func (r RepoInfo) OwnerWithoutAt() string {
+
if r.OwnerHandle != "" {
+
return r.OwnerHandle
} else {
return userutil.FlattenDid(r.OwnerDid)
}
+2 -2
appview/pages/templates/layouts/repobase.html
···
</p>
{{ end }}
<div class="text-lg flex items-center justify-between">
-
<div>
-
<a href="/{{ .RepoInfo.OwnerWithAt }}">{{ .RepoInfo.OwnerWithAt }}</a>
<span class="select-none">/</span>
<a href="/{{ .RepoInfo.FullName }}" class="font-bold">{{ .RepoInfo.Name }}</a>
</div>
···
</p>
{{ end }}
<div class="text-lg flex items-center justify-between">
+
<div class="flex items-center gap-2 flex-wrap">
+
{{ template "user/fragments/picHandleLink" .RepoInfo.OwnerDid }}
<span class="select-none">/</span>
<a href="/{{ .RepoInfo.FullName }}" class="font-bold">{{ .RepoInfo.Name }}</a>
</div>
+2 -2
appview/pages/templates/repo/fragments/cloneDropdown.html
···
<code
class="flex-1 px-3 py-2 text-sm bg-gray-50 dark:bg-gray-700 text-gray-900 dark:text-gray-100 rounded-l select-all cursor-pointer whitespace-nowrap overflow-x-auto"
onclick="window.getSelection().selectAllChildren(this)"
-
data-url="https://tangled.org/{{ .RepoInfo.OwnerWithAt }}/{{ .RepoInfo.Name }}"
-
>https://tangled.org/{{ .RepoInfo.OwnerWithAt }}/{{ .RepoInfo.Name }}</code>
<button
onclick="copyToClipboard(this, this.previousElementSibling.getAttribute('data-url'))"
class="px-3 py-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 border-l border-gray-300 dark:border-gray-600"
···
<code
class="flex-1 px-3 py-2 text-sm bg-gray-50 dark:bg-gray-700 text-gray-900 dark:text-gray-100 rounded-l select-all cursor-pointer whitespace-nowrap overflow-x-auto"
onclick="window.getSelection().selectAllChildren(this)"
+
data-url="https://tangled.org/{{ resolve .RepoInfo.OwnerDid }}/{{ .RepoInfo.Name }}"
+
>https://tangled.org/{{ resolve .RepoInfo.OwnerDid }}/{{ .RepoInfo.Name }}</code>
<button
onclick="copyToClipboard(this, this.previousElementSibling.getAttribute('data-url'))"
class="px-3 py-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 border-l border-gray-300 dark:border-gray-600"
+26 -24
appview/state/router.go
···
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
-
}
}
-
standardRouter.ServeHTTP(w, r)
}
})
return router
···
router.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) {
pat := chi.URLParam(r, "*")
+
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
+
}
+
+
// 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
+3 -2
appview/state/state.go
···
pubKeys, err := db.GetPublicKeysForDid(s.db, id.DID.String())
if err != nil {
-
w.WriteHeader(http.StatusNotFound)
return
}
if len(pubKeys) == 0 {
-
w.WriteHeader(http.StatusNotFound)
return
}
···
pubKeys, err := db.GetPublicKeysForDid(s.db, id.DID.String())
if err != nil {
+
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.StatusNoContent)
return
}
+6 -6
appview/state/userutil/userutil.go
···
didRegex = regexp.MustCompile(`^did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-]$`)
)
-
func IsHandleNoAt(s string) bool {
// ref: https://atproto.com/specs/handle
return handleRegex.MatchString(s)
}
func UnflattenDid(s string) string {
if !IsFlattenedDid(s) {
return s
···
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])?$`)
func IsValidSubdomain(name string) bool {
···
didRegex = regexp.MustCompile(`^did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-]$`)
)
+
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 {
if !IsFlattenedDid(s) {
return s
···
return s
}
var subdomainRegex = regexp.MustCompile(`^[a-z0-9]([a-z0-9-]{2,61}[a-z0-9])?$`)
func IsValidSubdomain(name string) bool {