diff --git a/appview/ingester.go b/appview/ingester.go --- a/appview/ingester.go +++ b/appview/ingester.go @@ -23,6 +23,7 @@ "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 @@ 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 @@ } 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/idresolver/resolver.go b/idresolver/resolver.go --- a/idresolver/resolver.go +++ b/idresolver/resolver.go @@ -2,8 +2,11 @@ import ( "context" + "fmt" "net" "net/http" + "slices" + "strings" "sync" "time" @@ -71,8 +74,39 @@ }, 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 @@ } 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 @@ 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 @@ } 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 @@ 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 @@ 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 @@ 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 @@ 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/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) 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 @@ -11,4 +25,53 @@ 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/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 @@ 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 @@ repoResolver: repoResolver, idResolver: idResolver, pages: pages, + rdb: rdb, logger: logger, } } @@ -184,19 +187,19 @@ 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 @@ 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/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 @@ 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 @@ } }, } +} + +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 @@ } 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 @@ "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 @@ 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 @@ 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 @@ rctx: rctx, resolver: res, db: database, + rdb: rdb, templateDir: "appview/pages", logger: logger, } 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 @@ 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 @@ 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 @@ "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 @@ 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 @@ } } + 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 @@ "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 @@ "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 @@ 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.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 @@ enforcer *rbac.Enforcer pages *pages.Pages idResolver *idresolver.Resolver + rdb *cache.Cache mentionsResolver *mentions.Resolver posthog posthog.Client jc *jetstream.JetstreamClient @@ -94,19 +96,24 @@ 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}) 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 @@ Db: wrapper, Enforcer: enforcer, IdResolver: res, + Cache: rdb, Config: config, Logger: log.SubLogger(logger, "ingester"), Validator: validator, @@ -206,6 +214,7 @@ 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/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 @@ {{ if .IncludeBluesky }}
{{ template "user/fragments/bluesky" "w-4 h-4 text-black dark:text-white" }} - {{ $userIdent }} + {{ primaryHandle $.UserDid }}
{{ end }} {{ range $link := .Links }}