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
··· 180 return func(next http.Handler) http.Handler { 181 return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 182 didOrHandle := chi.URLParam(req, "user") 183 if slices.Contains(excluded, didOrHandle) { 184 next.ServeHTTP(w, req) 185 return 186 } 187 188 - didOrHandle = strings.TrimPrefix(didOrHandle, "@") 189 - 190 id, err := mw.idResolver.ResolveIdent(req.Context(), didOrHandle) 191 if err != nil { 192 // invalid did or handle
··· 180 return func(next http.Handler) http.Handler { 181 return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 182 didOrHandle := chi.URLParam(req, "user") 183 + didOrHandle = strings.TrimPrefix(didOrHandle, "@") 184 + 185 if slices.Contains(excluded, didOrHandle) { 186 next.ServeHTTP(w, req) 187 return 188 } 189 190 id, err := mw.idResolver.ResolveIdent(req.Context(), didOrHandle) 191 if err != nil { 192 // invalid did or handle
+4 -3
appview/pages/funcmap.go
··· 17 "strings" 18 "time" 19 20 "github.com/dustin/go-humanize" 21 "github.com/go-enry/go-enry/v2" 22 "tangled.org/core/appview/filetree" ··· 57 return "handle.invalid" 58 } 59 60 - return "@" + identity.Handle.String() 61 }, 62 "truncateAt30": func(s string) string { 63 if len(s) <= 30 { ··· 117 return b 118 }, 119 "didOrHandle": func(did, handle string) string { 120 - if handle != "" { 121 - return fmt.Sprintf("@%s", handle) 122 } else { 123 return did 124 }
··· 17 "strings" 18 "time" 19 20 + "github.com/bluesky-social/indigo/atproto/syntax" 21 "github.com/dustin/go-humanize" 22 "github.com/go-enry/go-enry/v2" 23 "tangled.org/core/appview/filetree" ··· 58 return "handle.invalid" 59 } 60 61 + return identity.Handle.String() 62 }, 63 "truncateAt30": func(s string) string { 64 if len(s) <= 30 { ··· 118 return b 119 }, 120 "didOrHandle": func(did, handle string) string { 121 + if handle != "" && handle != syntax.HandleInvalid.String() { 122 + return handle 123 } else { 124 return did 125 }
+5 -7
appview/pages/repoinfo/repoinfo.go
··· 1 package repoinfo 2 3 import ( 4 - "fmt" 5 "path" 6 "slices" 7 - "strings" 8 9 "github.com/bluesky-social/indigo/atproto/syntax" 10 "tangled.org/core/appview/models" 11 "tangled.org/core/appview/state/userutil" 12 ) 13 14 - func (r RepoInfo) OwnerWithAt() string { 15 if r.OwnerHandle != "" { 16 - return fmt.Sprintf("@%s", r.OwnerHandle) 17 } else { 18 return r.OwnerDid 19 } 20 } 21 22 func (r RepoInfo) FullName() string { 23 - return path.Join(r.OwnerWithAt(), r.Name) 24 } 25 26 func (r RepoInfo) OwnerWithoutAt() string { 27 - if after, ok := strings.CutPrefix(r.OwnerWithAt(), "@"); ok { 28 - return after 29 } else { 30 return userutil.FlattenDid(r.OwnerDid) 31 }
··· 1 package repoinfo 2 3 import ( 4 "path" 5 "slices" 6 7 "github.com/bluesky-social/indigo/atproto/syntax" 8 "tangled.org/core/appview/models" 9 "tangled.org/core/appview/state/userutil" 10 ) 11 12 + func (r RepoInfo) Owner() string { 13 if r.OwnerHandle != "" { 14 + return r.OwnerHandle 15 } else { 16 return r.OwnerDid 17 } 18 } 19 20 func (r RepoInfo) FullName() string { 21 + return path.Join(r.Owner(), r.Name) 22 } 23 24 func (r RepoInfo) OwnerWithoutAt() string { 25 + if r.OwnerHandle != "" { 26 + return r.OwnerHandle 27 } else { 28 return userutil.FlattenDid(r.OwnerDid) 29 }
+2 -2
appview/pages/templates/layouts/repobase.html
··· 13 </p> 14 {{ end }} 15 <div class="text-lg flex items-center justify-between"> 16 - <div> 17 - <a href="/{{ .RepoInfo.OwnerWithAt }}">{{ .RepoInfo.OwnerWithAt }}</a> 18 <span class="select-none">/</span> 19 <a href="/{{ .RepoInfo.FullName }}" class="font-bold">{{ .RepoInfo.Name }}</a> 20 </div>
··· 13 </p> 14 {{ end }} 15 <div class="text-lg flex items-center justify-between"> 16 + <div class="flex items-center gap-2 flex-wrap"> 17 + {{ template "user/fragments/picHandleLink" .RepoInfo.OwnerDid }} 18 <span class="select-none">/</span> 19 <a href="/{{ .RepoInfo.FullName }}" class="font-bold">{{ .RepoInfo.Name }}</a> 20 </div>
+2 -2
appview/pages/templates/repo/fragments/cloneDropdown.html
··· 29 <code 30 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" 31 onclick="window.getSelection().selectAllChildren(this)" 32 - data-url="https://tangled.org/{{ .RepoInfo.OwnerWithAt }}/{{ .RepoInfo.Name }}" 33 - >https://tangled.org/{{ .RepoInfo.OwnerWithAt }}/{{ .RepoInfo.Name }}</code> 34 <button 35 onclick="copyToClipboard(this, this.previousElementSibling.getAttribute('data-url'))" 36 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"
··· 29 <code 30 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" 31 onclick="window.getSelection().selectAllChildren(this)" 32 + data-url="https://tangled.org/{{ resolve .RepoInfo.OwnerDid }}/{{ .RepoInfo.Name }}" 33 + >https://tangled.org/{{ resolve .RepoInfo.OwnerDid }}/{{ .RepoInfo.Name }}</code> 34 <button 35 onclick="copyToClipboard(this, this.previousElementSibling.getAttribute('data-url'))" 36 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
··· 42 43 router.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) { 44 pat := chi.URLParam(r, "*") 45 - if strings.HasPrefix(pat, "did:") || strings.HasPrefix(pat, "@") { 46 - userRouter.ServeHTTP(w, r) 47 - } else { 48 - // Check if the first path element is a valid handle without '@' or a flattened DID 49 - pathParts := strings.SplitN(pat, "/", 2) 50 - if len(pathParts) > 0 { 51 - if userutil.IsHandleNoAt(pathParts[0]) { 52 - // Redirect to the same path but with '@' prefixed to the handle 53 - redirectPath := "@" + pat 54 - http.Redirect(w, r, "/"+redirectPath, http.StatusFound) 55 - return 56 - } else if userutil.IsFlattenedDid(pathParts[0]) { 57 - // Redirect to the unflattened DID version 58 - unflattenedDid := userutil.UnflattenDid(pathParts[0]) 59 - var redirectPath string 60 - if len(pathParts) > 1 { 61 - redirectPath = unflattenedDid + "/" + pathParts[1] 62 - } else { 63 - redirectPath = unflattenedDid 64 - } 65 - http.Redirect(w, r, "/"+redirectPath, http.StatusFound) 66 - return 67 - } 68 } 69 - standardRouter.ServeHTTP(w, r) 70 } 71 }) 72 73 return router
··· 42 43 router.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) { 44 pat := chi.URLParam(r, "*") 45 + pathParts := strings.SplitN(pat, "/", 2) 46 + 47 + if len(pathParts) > 0 { 48 + firstPart := pathParts[0] 49 + 50 + // if using a DID or handle, just continue as per usual 51 + if userutil.IsDid(firstPart) || userutil.IsHandle(firstPart) { 52 + userRouter.ServeHTTP(w, r) 53 + return 54 + } 55 + 56 + // if using a flattened DID (like you would in go modules), unflatten 57 + if userutil.IsFlattenedDid(firstPart) { 58 + unflattenedDid := userutil.UnflattenDid(firstPart) 59 + redirectPath := strings.Join(append([]string{unflattenedDid}, pathParts[1:]...), "/") 60 + http.Redirect(w, r, "/"+redirectPath, http.StatusFound) 61 + return 62 + } 63 + 64 + // if using a handle with @, rewrite to work without @ 65 + if normalized := strings.TrimPrefix(firstPart, "@"); userutil.IsHandle(normalized) { 66 + redirectPath := strings.Join(append([]string{normalized}, pathParts[1:]...), "/") 67 + http.Redirect(w, r, "/"+redirectPath, http.StatusFound) 68 + return 69 } 70 } 71 + 72 + standardRouter.ServeHTTP(w, r) 73 }) 74 75 return router
+3 -2
appview/state/state.go
··· 376 377 pubKeys, err := db.GetPublicKeysForDid(s.db, id.DID.String()) 378 if err != nil { 379 - w.WriteHeader(http.StatusNotFound) 380 return 381 } 382 383 if len(pubKeys) == 0 { 384 - w.WriteHeader(http.StatusNotFound) 385 return 386 } 387
··· 376 377 pubKeys, err := db.GetPublicKeysForDid(s.db, id.DID.String()) 378 if err != nil { 379 + s.logger.Error("failed to get public keys", "err", err) 380 + http.Error(w, "failed to get public keys", http.StatusInternalServerError) 381 return 382 } 383 384 if len(pubKeys) == 0 { 385 + w.WriteHeader(http.StatusNoContent) 386 return 387 } 388
+6 -6
appview/state/userutil/userutil.go
··· 10 didRegex = regexp.MustCompile(`^did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-]$`) 11 ) 12 13 - func IsHandleNoAt(s string) bool { 14 // ref: https://atproto.com/specs/handle 15 return handleRegex.MatchString(s) 16 } 17 18 func UnflattenDid(s string) string { 19 if !IsFlattenedDid(s) { 20 return s ··· 47 return s 48 } 49 50 - // IsDid checks if the given string is a standard DID. 51 - func IsDid(s string) bool { 52 - return didRegex.MatchString(s) 53 - } 54 - 55 var subdomainRegex = regexp.MustCompile(`^[a-z0-9]([a-z0-9-]{2,61}[a-z0-9])?$`) 56 57 func IsValidSubdomain(name string) bool {
··· 10 didRegex = regexp.MustCompile(`^did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-]$`) 11 ) 12 13 + func IsHandle(s string) bool { 14 // ref: https://atproto.com/specs/handle 15 return handleRegex.MatchString(s) 16 } 17 18 + // IsDid checks if the given string is a standard DID. 19 + func IsDid(s string) bool { 20 + return didRegex.MatchString(s) 21 + } 22 + 23 func UnflattenDid(s string) string { 24 if !IsFlattenedDid(s) { 25 return s ··· 52 return s 53 } 54 55 var subdomainRegex = regexp.MustCompile(`^[a-z0-9]([a-z0-9-]{2,61}[a-z0-9])?$`) 56 57 func IsValidSubdomain(name string) bool {