diff --git a/appview/cache/cache.go b/appview/cache/cache.go
--- a/appview/cache/cache.go
+++ b/appview/cache/cache.go
@@ -1,6 +1,20 @@
package cache
-import "github.com/redis/go-redis/v9"
+import (
+ "context"
+ "fmt"
+ "time"
+
+ "github.com/bluesky-social/indigo/atproto/syntax"
+ "github.com/redis/go-redis/v9"
+ "tangled.org/core/appview/db"
+)
+
+const (
+ PreferredHandleByDid = "preferred_handle:did:%s"
+ PreferredHandleByHandle = "preferred_handle:handle:%s"
+ PreferredHandleTTL = 24 * time.Hour
+)
type Cache struct {
*redis.Client
@@ -12,3 +26,52 @@ Addr: addr,
})
return &Cache{rdb}
}
+
+func LookupPreferredHandle(ctx context.Context, rdb *Cache, e db.Execer, did string) string {
+ if rdb != nil {
+ if h, err := rdb.Get(ctx, fmt.Sprintf(PreferredHandleByDid, did)).Result(); err == nil {
+ return h
+ }
+ }
+
+ var handle string
+ if h, err := db.GetPreferredHandle(e, did); err == nil {
+ handle = string(h)
+ }
+
+ if rdb != nil {
+ pipe := rdb.Pipeline()
+ pipe.Set(ctx, fmt.Sprintf(PreferredHandleByDid, did), handle, PreferredHandleTTL)
+ if handle != "" {
+ pipe.Set(ctx, fmt.Sprintf(PreferredHandleByHandle, handle), did, PreferredHandleTTL)
+ }
+ pipe.Exec(ctx)
+ }
+
+ return handle
+}
+
+func LookupDidByPreferredHandle(ctx context.Context, rdb *Cache, e db.Execer, handle syntax.Handle) string {
+ handleStr := string(handle)
+ if rdb != nil {
+ if d, err := rdb.Get(ctx, fmt.Sprintf(PreferredHandleByHandle, handleStr)).Result(); err == nil {
+ return d
+ }
+ }
+
+ var did string
+ if d, err := db.GetDidByPreferredHandle(e, handle); err == nil {
+ did = string(d)
+ }
+
+ if rdb != nil {
+ pipe := rdb.Pipeline()
+ pipe.Set(ctx, fmt.Sprintf(PreferredHandleByHandle, handleStr), did, PreferredHandleTTL)
+ if did != "" {
+ pipe.Set(ctx, fmt.Sprintf(PreferredHandleByDid, did), handleStr, PreferredHandleTTL)
+ }
+ pipe.Exec(ctx)
+ }
+
+ return did
+}
diff --git a/appview/db/profile.go b/appview/db/profile.go
--- a/appview/db/profile.go
+++ b/appview/db/profile.go
@@ -354,6 +354,21 @@
return profileMap, nil
}
+func GetPreferredHandle(e Execer, did string) (syntax.Handle, error) {
+ var h sql.Null[string]
+ err := e.QueryRow(
+ `select preferred_handle from profile where did = ?`,
+ did,
+ ).Scan(&h)
+ if err != nil {
+ return "", err
+ }
+ if !h.Valid || h.V == "" {
+ return "", sql.ErrNoRows
+ }
+ return syntax.Handle(h.V), nil
+}
+
func GetDidByPreferredHandle(e Execer, handle syntax.Handle) (syntax.DID, error) {
var did string
err := e.QueryRow(
diff --git a/appview/ingester.go b/appview/ingester.go
--- a/appview/ingester.go
+++ b/appview/ingester.go
@@ -23,6 +23,7 @@ "github.com/go-git/go-git/v5/plumbing"
"github.com/ipfs/go-cid"
"golang.org/x/sync/errgroup"
"tangled.org/core/api/tangled"
+ "tangled.org/core/appview/cache"
"tangled.org/core/appview/config"
"tangled.org/core/appview/db"
"tangled.org/core/appview/models"
@@ -37,6 +38,7 @@ type Ingester struct {
Db db.DbWrapper
Enforcer *rbac.Enforcer
IdResolver *idresolver.Resolver
+ Cache *cache.Cache
Config *config.Config
Logger *slog.Logger
Validator *validator.Validator
@@ -434,6 +436,19 @@ return fmt.Errorf("invalid profile record")
}
err = db.UpsertProfile(tx, &profile)
+ if err == nil && i.Cache != nil {
+ pipe := i.Cache.Pipeline()
+ didKey := fmt.Sprintf(cache.PreferredHandleByDid, did)
+ if preferredHandle != "" {
+ pipe.Set(ctx, didKey, string(preferredHandle), cache.PreferredHandleTTL)
+ pipe.Set(ctx, fmt.Sprintf(cache.PreferredHandleByHandle, string(preferredHandle)), did, cache.PreferredHandleTTL)
+ } else {
+ pipe.Del(ctx, didKey)
+ }
+ if _, execErr := pipe.Exec(ctx); execErr != nil {
+ l.Warn("failed to update preferred handle cache", "err", execErr)
+ }
+ }
case jmodels.CommitOperationDelete:
err = db.DeleteArtifact(i.Db, orm.FilterEq("did", did), orm.FilterEq("rkey", e.Commit.RKey))
}
diff --git a/appview/issues/opengraph.go b/appview/issues/opengraph.go
--- a/appview/issues/opengraph.go
+++ b/appview/issues/opengraph.go
@@ -67,15 +67,9 @@ })
}
}
- var ownerHandle string
- owner, err := rp.idResolver.ResolveIdent(context.Background(), f.Did)
- if err != nil {
- ownerHandle = f.Did
- } else {
- ownerHandle = owner.Handle.String()
- }
+ ownerHandle := rp.pages.DisplayHandle(r.Context(), f.Did)
- avatarUrl := rp.pages.AvatarUrl(ownerHandle, "256")
+ avatarUrl := rp.pages.AvatarUrl(f.Did, "256")
status := "closed"
if issue.Open {
diff --git a/appview/middleware/middleware.go b/appview/middleware/middleware.go
--- a/appview/middleware/middleware.go
+++ b/appview/middleware/middleware.go
@@ -13,6 +13,7 @@
"github.com/bluesky-social/indigo/atproto/identity"
"github.com/bluesky-social/indigo/atproto/syntax"
"github.com/go-chi/chi/v5"
+ "tangled.org/core/appview/cache"
"tangled.org/core/appview/db"
"tangled.org/core/appview/oauth"
"tangled.org/core/appview/pages"
@@ -31,10 +32,11 @@ enforcer *rbac.Enforcer
repoResolver *reporesolver.RepoResolver
idResolver *idresolver.Resolver
pages *pages.Pages
+ rdb *cache.Cache
logger *slog.Logger
}
-func New(oauth *oauth.OAuth, db *db.DB, enforcer *rbac.Enforcer, repoResolver *reporesolver.RepoResolver, idResolver *idresolver.Resolver, pages *pages.Pages, logger *slog.Logger) Middleware {
+func New(oauth *oauth.OAuth, db *db.DB, enforcer *rbac.Enforcer, repoResolver *reporesolver.RepoResolver, idResolver *idresolver.Resolver, pages *pages.Pages, rdb *cache.Cache, logger *slog.Logger) Middleware {
return Middleware{
oauth: oauth,
db: db,
@@ -42,6 +44,7 @@ enforcer: enforcer,
repoResolver: repoResolver,
idResolver: idResolver,
pages: pages,
+ rdb: rdb,
logger: logger,
}
}
@@ -184,19 +187,19 @@ excluded := []string{"favicon.ico"}
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, "@")
+ origSeg := chi.URLParam(req, "user")
+ didOrHandle := strings.TrimPrefix(origSeg, "@")
if slices.Contains(excluded, didOrHandle) {
next.ServeHTTP(w, req)
return
}
- id, err := mw.idResolver.ResolveIdent(req.Context(), didOrHandle)
+ id, err := mw.idResolver.ResolveAtIdentifier(req.Context(), didOrHandle)
if err != nil {
if h, parseErr := syntax.ParseHandle(didOrHandle); parseErr == nil {
- if did, lookupErr := db.GetDidByPreferredHandle(mw.db, h); lookupErr == nil {
- id, err = mw.idResolver.ResolveIdent(req.Context(), string(did))
+ if did := cache.LookupDidByPreferredHandle(req.Context(), mw.rdb, mw.db, h); did != "" {
+ id, err = mw.idResolver.ResolveAtIdentifier(req.Context(), did)
}
}
}
@@ -204,6 +207,18 @@ if err != nil {
mw.logger.Error("failed to resolve did/handle", "didOrHandle", didOrHandle, "err", err)
mw.pages.Error404(w)
return
+ }
+
+ if req.Method == http.MethodGet && !userutil.IsDid(didOrHandle) {
+ if pref := cache.LookupPreferredHandle(req.Context(), mw.rdb, mw.db, id.DID.String()); pref != "" && didOrHandle != pref {
+ rest := strings.TrimPrefix(req.URL.Path, "/"+origSeg)
+ target := "/" + pref + rest
+ if req.URL.RawQuery != "" {
+ target += "?" + req.URL.RawQuery
+ }
+ http.Redirect(w, req, target, http.StatusFound)
+ return
+ }
}
ctx := context.WithValue(req.Context(), "resolvedId", *id)
diff --git a/appview/pages/funcmap.go b/appview/pages/funcmap.go
--- a/appview/pages/funcmap.go
+++ b/appview/pages/funcmap.go
@@ -27,11 +27,13 @@ "github.com/dustin/go-humanize"
"github.com/go-enry/go-enry/v2"
"github.com/yuin/goldmark"
emoji "github.com/yuin/goldmark-emoji"
+ "tangled.org/core/appview/cache"
"tangled.org/core/appview/db"
"tangled.org/core/appview/models"
"tangled.org/core/appview/oauth"
"tangled.org/core/appview/pages/markup"
"tangled.org/core/crypto"
+ "tangled.org/core/idresolver"
)
type tab map[string]string
@@ -65,21 +67,10 @@ keyValue := reflect.ValueOf(key)
return mapValue.MapIndex(keyValue).IsValid()
},
"resolve": func(s string) string {
- profile, err := db.GetProfile(p.db, s)
- if err == nil && profile != nil && profile.PreferredHandle != "" {
- return string(profile.PreferredHandle)
- }
-
- identity, err := p.resolver.ResolveIdent(context.Background(), s)
- if err != nil {
- return s
- }
-
- if identity.Handle.IsInvalidHandle() {
- return "handle.invalid"
- }
-
- return identity.Handle.String()
+ return p.DisplayHandle(context.Background(), s)
+ },
+ "primaryHandle": func(s string) string {
+ return primaryHandle(p.resolver, s)
},
"resolvePds": func(s string) string {
identity, err := p.resolver.ResolveIdent(context.Background(), s)
@@ -512,6 +503,26 @@ "PdsUserDomain": p.pdsCfg.UserDomain,
}
},
}
+}
+
+func primaryHandle(r *idresolver.Resolver, s string) string {
+ identity, err := r.ResolveIdent(context.Background(), s)
+ if err != nil || identity.Handle.IsInvalidHandle() {
+ return "handle.invalid"
+ }
+ return identity.Handle.String()
+}
+
+func (p *Pages) DisplayHandle(ctx context.Context, did string) string {
+ if p.db != nil {
+ if h := cache.LookupPreferredHandle(ctx, p.rdb, p.db, did); h != "" {
+ return h
+ }
+ }
+ if id, err := p.resolver.ResolveIdent(ctx, did); err == nil && !id.Handle.IsInvalidHandle() {
+ return id.Handle.String()
+ }
+ return did
}
func (p *Pages) AvatarUrl(actor, size string) string {
diff --git a/appview/pages/funcmap_test.go b/appview/pages/funcmap_test.go
--- a/appview/pages/funcmap_test.go
+++ b/appview/pages/funcmap_test.go
@@ -22,7 +22,7 @@ // TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- p := NewPages(tt.config, tt.res, nil, tt.l)
+ p := NewPages(tt.config, tt.res, nil, nil, tt.l)
got := p.funcMap()
// TODO: update the condition below to compare got with tt.want.
if true {
diff --git a/appview/pages/pages.go b/appview/pages/pages.go
--- a/appview/pages/pages.go
+++ b/appview/pages/pages.go
@@ -17,6 +17,7 @@ "sync"
"time"
"tangled.org/core/api/tangled"
+ "tangled.org/core/appview/cache"
"tangled.org/core/appview/commitverify"
"tangled.org/core/appview/config"
"tangled.org/core/appview/db"
@@ -45,6 +46,7 @@ avatar config.AvatarConfig
pdsCfg config.PdsConfig
resolver *idresolver.Resolver
db *db.DB
+ rdb *cache.Cache
dev bool
embedFS fs.FS
templateDir string // Path to templates on disk for dev mode
@@ -52,7 +54,7 @@ rctx *markup.RenderContext
logger *slog.Logger
}
-func NewPages(config *config.Config, res *idresolver.Resolver, database *db.DB, logger *slog.Logger) *Pages {
+func NewPages(config *config.Config, res *idresolver.Resolver, database *db.DB, rdb *cache.Cache, logger *slog.Logger) *Pages {
// initialized with safe defaults, can be overridden per use
rctx := &markup.RenderContext{
IsDev: config.Core.Dev,
@@ -72,6 +74,7 @@ pdsCfg: config.Pds,
rctx: rctx,
resolver: res,
db: database,
+ rdb: rdb,
templateDir: "appview/pages",
logger: logger,
}
diff --git a/appview/pages/templates/user/fragments/profileCard.html b/appview/pages/templates/user/fragments/profileCard.html
--- a/appview/pages/templates/user/fragments/profileCard.html
+++ b/appview/pages/templates/user/fragments/profileCard.html
@@ -61,7 +61,7 @@ {{ end }}
{{ if .IncludeBluesky }}
{{ end }}
{{ range $link := .Links }}
diff --git a/appview/pulls/opengraph.go b/appview/pulls/opengraph.go
--- a/appview/pulls/opengraph.go
+++ b/appview/pulls/opengraph.go
@@ -1,7 +1,6 @@
package pulls
import (
- "context"
"log"
"net/http"
"time"
@@ -26,15 +25,9 @@ http.Error(w, "pull not found", http.StatusNotFound)
return
}
- var ownerHandle string
- owner, err := s.idResolver.ResolveIdent(context.Background(), f.Did)
- if err != nil {
- ownerHandle = f.Did
- } else {
- ownerHandle = owner.Handle.String()
- }
+ ownerHandle := s.pages.DisplayHandle(r.Context(), f.Did)
- avatarUrl := s.pages.AvatarUrl(ownerHandle, "256")
+ avatarUrl := s.pages.AvatarUrl(f.Did, "256")
var status string
if pull.State.IsOpen() {
diff --git a/appview/repo/opengraph.go b/appview/repo/opengraph.go
--- a/appview/repo/opengraph.go
+++ b/appview/repo/opengraph.go
@@ -1,7 +1,6 @@
package repo
import (
- "context"
"log"
"net/http"
"sort"
@@ -21,15 +20,9 @@ log.Println("failed to get repo and knot", err)
return
}
- var ownerHandle string
- owner, err := rp.idResolver.ResolveIdent(context.Background(), f.Did)
- if err != nil {
- ownerHandle = f.Did
- } else {
- ownerHandle = owner.Handle.String()
- }
+ ownerHandle := rp.pages.DisplayHandle(r.Context(), f.Did)
- avatarUrl := rp.pages.AvatarUrl(ownerHandle, "256")
+ avatarUrl := rp.pages.AvatarUrl(f.Did, "256")
var languageStats []types.RepoLanguageDetails
langs, err := db.GetRepoLanguages(
diff --git a/appview/reporesolver/resolver.go b/appview/reporesolver/resolver.go
--- a/appview/reporesolver/resolver.go
+++ b/appview/reporesolver/resolver.go
@@ -10,6 +10,7 @@ "strings"
"github.com/bluesky-social/indigo/atproto/identity"
"github.com/go-chi/chi/v5"
+ "tangled.org/core/appview/cache"
"tangled.org/core/appview/config"
"tangled.org/core/appview/db"
"tangled.org/core/appview/models"
@@ -27,10 +28,11 @@ type RepoResolver struct {
config *config.Config
enforcer *rbac.Enforcer
execer db.Execer
+ rdb *cache.Cache
}
-func New(config *config.Config, enforcer *rbac.Enforcer, execer db.Execer) *RepoResolver {
- return &RepoResolver{config: config, enforcer: enforcer, execer: execer}
+func New(config *config.Config, enforcer *rbac.Enforcer, execer db.Execer, rdb *cache.Cache) *RepoResolver {
+ return &RepoResolver{config: config, enforcer: enforcer, execer: execer, rdb: rdb}
}
// NOTE: this... should not even be here. the entire package will be removed in future refactor
@@ -116,10 +118,15 @@ log.Println("failed to get source repo", err)
}
}
+ ownerHandle := ownerId.Handle.String()
+ if h := cache.LookupPreferredHandle(r.Context(), rr.rdb, rr.execer, ownerId.DID.String()); h != "" {
+ ownerHandle = h
+ }
+
repoInfo := repoinfo.RepoInfo{
// this is basically a models.Repo
OwnerDid: ownerId.DID.String(),
- OwnerHandle: ownerId.Handle.String(),
+ OwnerHandle: ownerHandle,
Name: repo.Name,
Rkey: repo.Rkey,
Description: repo.Description,
diff --git a/appview/state/login.go b/appview/state/login.go
--- a/appview/state/login.go
+++ b/appview/state/login.go
@@ -8,8 +8,6 @@ "strings"
"time"
comatproto "github.com/bluesky-social/indigo/api/atproto"
- "github.com/bluesky-social/indigo/atproto/identity"
- "github.com/bluesky-social/indigo/atproto/syntax"
"github.com/bluesky-social/indigo/xrpc"
"tangled.org/core/appview/pages"
)
@@ -59,14 +57,7 @@ )
return
}
- ident, err := s.idResolver.ResolveIdent(r.Context(), handle)
- if err != nil && errors.Is(err, identity.ErrHandleMismatch) {
- if h, parseErr := syntax.ParseHandle(handle); parseErr == nil {
- if did, resolveErr := s.idResolver.ResolveHandle(r.Context(), h); resolveErr == nil {
- ident, err = s.idResolver.ResolveIdent(r.Context(), did.String())
- }
- }
- }
+ ident, err := s.idResolver.ResolveAtIdentifier(r.Context(), handle)
if err != nil {
l.Warn("handle resolution failed", "handle", handle, "err", err)
s.pages.Notice(w, "login-msg", fmt.Sprintf("Could not resolve handle \"%s\". The account may not exist.", handle))
diff --git a/appview/state/profile.go b/appview/state/profile.go
--- a/appview/state/profile.go
+++ b/appview/state/profile.go
@@ -15,6 +15,7 @@ lexutil "github.com/bluesky-social/indigo/lex/util"
"github.com/go-chi/chi/v5"
"github.com/gorilla/feeds"
"tangled.org/core/api/tangled"
+ "tangled.org/core/appview/cache"
"tangled.org/core/appview/db"
"tangled.org/core/appview/middleware"
"tangled.org/core/appview/models"
@@ -91,11 +92,13 @@ }
loggedInUser := s.oauth.GetMultiAccountUser(r)
followStatus := models.IsNotFollowing
+ var loggedInDid string
if loggedInUser != nil {
followStatus = db.GetFollowStatus(s.db, loggedInUser.Did, did)
+ loggedInDid = loggedInUser.Did
}
- showPunchcard := s.shouldShowPunchcard(did, loggedInUser.Did)
+ showPunchcard := s.shouldShowPunchcard(did, loggedInDid)
var punchcard *models.Punchcard
if showPunchcard {
@@ -745,12 +748,6 @@
func (s *State) updateProfile(profile *models.Profile, w http.ResponseWriter, r *http.Request) {
l := s.logger.With("handler", "updateProfile")
user := s.oauth.GetMultiAccountUser(r)
- tx, err := s.db.BeginTx(r.Context(), nil)
- if err != nil {
- l.Error("failed to start transaction", "err", err)
- s.pages.Notice(w, "update-profile", "Failed to update profile, try again later.")
- return
- }
client, err := s.oauth.AuthorizedClient(r)
if err != nil {
@@ -805,11 +802,32 @@ s.pages.Notice(w, "update-profile", "Failed to update PDS, try again later.")
return
}
- err = db.UpsertProfile(tx, profile)
+ tx, err := s.db.BeginTx(r.Context(), nil)
if err != nil {
+ l.Error("failed to start transaction", "err", err)
+ s.pages.Notice(w, "update-profile", "Failed to update profile, try again later.")
+ return
+ }
+
+ if err := db.UpsertProfile(tx, profile); err != nil {
l.Error("failed to update profile in DB", "err", err)
s.pages.Notice(w, "update-profile", "Failed to update profile, try again later.")
return
+ }
+
+ if s.rdb != nil {
+ ctx := r.Context()
+ pipe := s.rdb.Pipeline()
+ didKey := fmt.Sprintf(cache.PreferredHandleByDid, profile.Did)
+ if profile.PreferredHandle != "" {
+ pipe.Set(ctx, didKey, string(profile.PreferredHandle), cache.PreferredHandleTTL)
+ pipe.Set(ctx, fmt.Sprintf(cache.PreferredHandleByHandle, string(profile.PreferredHandle)), profile.Did, cache.PreferredHandleTTL)
+ } else {
+ pipe.Del(ctx, didKey)
+ }
+ if _, execErr := pipe.Exec(ctx); execErr != nil {
+ l.Warn("failed to update preferred handle cache", "err", execErr)
+ }
}
s.notifier.UpdateProfile(r.Context(), profile)
diff --git a/appview/state/router.go b/appview/state/router.go
--- a/appview/state/router.go
+++ b/appview/state/router.go
@@ -33,6 +33,7 @@ s.enforcer,
s.repoResolver,
s.idResolver,
s.pages,
+ s.rdb,
s.logger,
)
diff --git a/appview/state/state.go b/appview/state/state.go
--- a/appview/state/state.go
+++ b/appview/state/state.go
@@ -13,6 +13,7 @@
"tangled.org/core/api/tangled"
"tangled.org/core/appview"
"tangled.org/core/appview/bsky"
+ "tangled.org/core/appview/cache"
"tangled.org/core/appview/cloudflare"
"tangled.org/core/appview/config"
"tangled.org/core/appview/db"
@@ -57,6 +58,7 @@ oauth *oauth.OAuth
enforcer *rbac.Enforcer
pages *pages.Pages
idResolver *idresolver.Resolver
+ rdb *cache.Cache
mentionsResolver *mentions.Resolver
posthog posthog.Client
jc *jetstream.JetstreamClient
@@ -92,6 +94,11 @@ res, err := idresolver.RedisResolver(config.Redis.ToURL(), config.Plc.PLCURL)
if err != nil {
logger.Error("failed to create redis resolver", "err", err)
res = idresolver.DefaultResolver(config.Plc.PLCURL)
+ }
+
+ var rdb *cache.Cache
+ if config.Redis.Addr != "" {
+ rdb = cache.New(config.Redis.Addr)
}
posthog, err := posthog.NewWithConfig(config.Posthog.ApiKey, posthog.Config{Endpoint: config.Posthog.Endpoint})
@@ -99,14 +106,14 @@ if err != nil {
return nil, fmt.Errorf("failed to create posthog client: %w", err)
}
- pages := pages.NewPages(config, res, d, log.SubLogger(logger, "pages"))
+ pages := pages.NewPages(config, res, d, rdb, log.SubLogger(logger, "pages"))
oauth, err := oauth.New(config, posthog, d, enforcer, res, log.SubLogger(logger, "oauth"))
if err != nil {
return nil, fmt.Errorf("failed to start oauth handler: %w", err)
}
validator := validator.New(d, res, enforcer)
- repoResolver := reporesolver.New(config, enforcer, d)
+ repoResolver := reporesolver.New(config, enforcer, d, rdb)
mentionsResolver := mentions.New(config, res, d, log.SubLogger(logger, "mentionsResolver"))
@@ -152,6 +159,7 @@ ingester := appview.Ingester{
Db: wrapper,
Enforcer: enforcer,
IdResolver: res,
+ Cache: rdb,
Config: config,
Logger: log.SubLogger(logger, "ingester"),
Validator: validator,
@@ -206,6 +214,7 @@ oauth: oauth,
enforcer: enforcer,
pages: pages,
idResolver: res,
+ rdb: rdb,
mentionsResolver: mentionsResolver,
posthog: posthog,
jc: jc,
@@ -631,10 +640,12 @@
aturi = ""
s.notifier.NewRepo(r.Context(), repo)
- if repoDid != "" {
+ switch {
+ case repoDid != "":
s.pages.HxLocation(w, fmt.Sprintf("/%s", repoDid))
- } else {
- s.pages.HxLocation(w, fmt.Sprintf("/%s/%s", user.Did, repoName))
+ default:
+ handle := s.pages.DisplayHandle(r.Context(), user.Did)
+ s.pages.HxLocation(w, fmt.Sprintf("/%s/%s", handle, repoName))
}
}
}
diff --git a/cmd/blog/main.go b/cmd/blog/main.go
--- a/cmd/blog/main.go
+++ b/cmd/blog/main.go
@@ -54,7 +54,7 @@ }
func makePages(ctx context.Context, cfg *config.Config, logger *slog.Logger) (*pages.Pages, error) {
resolver := idresolver.DefaultResolver(cfg.Plc.PLCURL)
- return pages.NewPages(cfg, resolver, nil, logger), nil
+ return pages.NewPages(cfg, resolver, nil, nil, logger), nil
}
func runBuild(ctx context.Context, logger *slog.Logger) error {
diff --git a/idresolver/resolver.go b/idresolver/resolver.go
--- a/idresolver/resolver.go
+++ b/idresolver/resolver.go
@@ -2,8 +2,11 @@ package idresolver
import (
"context"
+ "fmt"
"net"
"net/http"
+ "slices"
+ "strings"
"sync"
"time"
@@ -71,8 +74,39 @@ base: base,
}, nil
}
+type handleResolver interface {
+ ResolveHandle(ctx context.Context, h syntax.Handle) (syntax.DID, error)
+}
+
func (r *Resolver) ResolveHandle(ctx context.Context, handle syntax.Handle) (syntax.DID, error) {
+ if hr, ok := r.directory.(handleResolver); ok {
+ return hr.ResolveHandle(ctx, handle)
+ }
return r.base.ResolveHandle(ctx, handle)
+}
+
+func (r *Resolver) ResolveAtIdentifier(ctx context.Context, input string) (*identity.Identity, error) {
+ if did, err := syntax.ParseDID(input); err == nil {
+ return r.directory.LookupDID(ctx, did)
+ }
+ handle, err := syntax.ParseHandle(input)
+ if err != nil {
+ return nil, fmt.Errorf("not a did or handle: %w", err)
+ }
+ handle = handle.Normalize()
+ did, err := r.base.ResolveHandle(ctx, handle)
+ if err != nil {
+ return nil, fmt.Errorf("resolve handle %q: %w", handle, err)
+ }
+ ident, err := r.directory.LookupDID(ctx, did)
+ if err != nil {
+ return nil, fmt.Errorf("lookup did for %q: %w", handle, err)
+ }
+ aka := "at://" + handle.String()
+ if !slices.ContainsFunc(ident.AlsoKnownAs, func(s string) bool { return strings.EqualFold(s, aka) }) {
+ return nil, fmt.Errorf("handle %q not declared in alsoKnownAs for %s", handle, did)
+ }
+ return ident, nil
}
func (r *Resolver) ResolveIdent(ctx context.Context, arg string) (*identity.Identity, error) {
diff --git a/knotserver/ingester.go b/knotserver/ingester.go
--- a/knotserver/ingester.go
+++ b/knotserver/ingester.go
@@ -471,24 +471,31 @@ return nil
}
func (h *Knot) processMessages(ctx context.Context, event *jmodels.Event) error {
- if event.Kind != jmodels.EventKindCommit {
+ var err error
+ switch event.Kind {
+ case jmodels.EventKindIdentity:
+ err = h.resolver.InvalidateIdent(ctx, event.Did)
+ case jmodels.EventKindCommit:
+ switch event.Commit.Collection {
+ case tangled.PublicKeyNSID:
+ err = h.processPublicKey(ctx, event)
+ case tangled.KnotMemberNSID:
+ err = h.processKnotMember(ctx, event)
+ case tangled.RepoPullNSID:
+ err = h.processPull(ctx, event)
+ case tangled.RepoCollaboratorNSID:
+ err = h.processCollaborator(ctx, event)
+ }
+ default:
return nil
}
- var err error
- switch event.Commit.Collection {
- case tangled.PublicKeyNSID:
- err = h.processPublicKey(ctx, event)
- case tangled.KnotMemberNSID:
- err = h.processKnotMember(ctx, event)
- case tangled.RepoPullNSID:
- err = h.processPull(ctx, event)
- case tangled.RepoCollaboratorNSID:
- err = h.processCollaborator(ctx, event)
- }
-
if err != nil {
- h.l.Warn("failed to process event, skipping", "nsid", event.Commit.Collection, "err", err)
+ args := []any{"kind", event.Kind, "err", err}
+ if event.Kind == jmodels.EventKindCommit {
+ args = append(args, "nsid", event.Commit.Collection)
+ }
+ h.l.Warn("failed to process event, skipping", args...)
}
lastTimeUs := event.TimeUS + 1
diff --git a/knotserver/internal.go b/knotserver/internal.go
--- a/knotserver/internal.go
+++ b/knotserver/internal.go
@@ -115,17 +115,16 @@ diskRelative = rel
case len(components) == 2:
repoOwner := components[0]
- resolver := idresolver.DefaultResolver(h.c.Server.PlcUrl)
- repoOwnerIdent, resolveErr := resolver.ResolveIdent(r.Context(), repoOwner)
- if resolveErr != nil || repoOwnerIdent.Handle.IsInvalidHandle() {
- l.Error("Error resolving handle", "handle", repoOwner, "err", resolveErr)
+ ownerIdent, resolveErr := h.res.ResolveAtIdentifier(r.Context(), repoOwner)
+ if resolveErr != nil {
+ l.Error("error resolving owner", "owner", repoOwner, "err", resolveErr)
w.WriteHeader(http.StatusInternalServerError)
- fmt.Fprintf(w, "error resolving handle: invalid handle\n")
+ fmt.Fprintf(w, "error resolving owner: invalid did or handle\n")
return
}
- ownerDid := repoOwnerIdent.DID.String()
+ ownerDid := ownerIdent.DID
repoName := components[1]
- repoDid, didErr := h.db.GetRepoDid(ownerDid, repoName)
+ repoDid, didErr := h.db.GetRepoDid(ownerDid.String(), repoName)
var repoPath string
if didErr == nil {
var lookupErr error
@@ -138,7 +137,7 @@ return
}
rbacResource = repoDid
} else {
- legacyPath, joinErr := securejoin.SecureJoin(h.c.Repo.ScanPath, filepath.Join(ownerDid, repoName))
+ legacyPath, joinErr := securejoin.SecureJoin(h.c.Repo.ScanPath, filepath.Join(ownerDid.String(), repoName))
if joinErr != nil {
w.WriteHeader(http.StatusNotFound)
fmt.Fprintln(w, "repo not found")
@@ -151,7 +150,7 @@ fmt.Fprintln(w, "repo not found")
return
}
repoPath = legacyPath
- rbacResource = ownerDid + "/" + repoName
+ rbacResource = ownerDid.String() + "/" + repoName
}
rel, relErr := filepath.Rel(h.c.Repo.ScanPath, repoPath)
if relErr != nil {
@@ -476,19 +475,18 @@ *clientMsgs = append(*clientMsgs, ZWS)
return nil
}
-func Internal(ctx context.Context, c *config.Config, db *db.DB, e *rbac.Enforcer, n *notifier.Notifier) http.Handler {
+func Internal(ctx context.Context, c *config.Config, db *db.DB, e *rbac.Enforcer, n *notifier.Notifier, res *idresolver.Resolver) http.Handler {
r := chi.NewRouter()
l := log.FromContext(ctx)
l = log.SubLogger(l, "internal")
- res := idresolver.DefaultResolver(c.Server.PlcUrl)
h := InternalHandle{
- db,
- c,
- e,
- l,
- n,
- res,
+ db: db,
+ c: c,
+ e: e,
+ l: l,
+ n: n,
+ res: res,
}
r.Get("/push-allowed", h.PushAllowed)
diff --git a/knotserver/router.go b/knotserver/router.go
--- a/knotserver/router.go
+++ b/knotserver/router.go
@@ -36,7 +36,7 @@ motd []byte
motdMu sync.RWMutex
}
-func Setup(ctx context.Context, c *config.Config, db *db.DB, e *rbac.Enforcer, jc *jetstream.JetstreamClient, n *notifier.Notifier) (http.Handler, error) {
+func Setup(ctx context.Context, c *config.Config, db *db.DB, e *rbac.Enforcer, jc *jetstream.JetstreamClient, n *notifier.Notifier, resolver *idresolver.Resolver) (http.Handler, error) {
h := Knot{
c: c,
db: db,
@@ -44,7 +44,7 @@ e: e,
l: log.FromContext(ctx),
jc: jc,
n: n,
- resolver: idresolver.DefaultResolver(c.Server.PlcUrl),
+ resolver: resolver,
motd: defaultMotd,
}
diff --git a/knotserver/server.go b/knotserver/server.go
--- a/knotserver/server.go
+++ b/knotserver/server.go
@@ -9,6 +9,7 @@ "github.com/bluesky-social/indigo/xrpc"
"github.com/urfave/cli/v3"
"tangled.org/core/api/tangled"
"tangled.org/core/hook"
+ "tangled.org/core/idresolver"
"tangled.org/core/jetstream"
"tangled.org/core/knotserver/config"
"tangled.org/core/knotserver/db"
@@ -89,14 +90,16 @@ }
notifier := notifier.New()
+ resolver := idresolver.DefaultResolver(c.Server.PlcUrl)
+
go migrateReposOnStartup(ctx, c, db, e, ¬ifier, log.SubLogger(logger, "migrate"))
- mux, err := Setup(ctx, c, db, e, jc, ¬ifier)
+ mux, err := Setup(ctx, c, db, e, jc, ¬ifier, resolver)
if err != nil {
return fmt.Errorf("failed to setup server: %w", err)
}
- imux := Internal(ctx, c, db, e, ¬ifier)
+ imux := Internal(ctx, c, db, e, ¬ifier, resolver)
logger.Info("starting internal server", "address", c.Server.InternalListenAddr)
go http.ListenAndServe(c.Server.InternalListenAddr, imux)