Monorepo for Tangled
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

appview,knotserver: pref-handle display + identity ingest

Lewis: May this revision serve well! <lewis@tangled.org>

authored by

Lewis and committed by
Tangled
98ff5a1b 00b32691

+285 -113
+64 -1
appview/cache/cache.go
··· 1 1 package cache 2 2 3 - import "github.com/redis/go-redis/v9" 3 + import ( 4 + "context" 5 + "fmt" 6 + "time" 7 + 8 + "github.com/bluesky-social/indigo/atproto/syntax" 9 + "github.com/redis/go-redis/v9" 10 + "tangled.org/core/appview/db" 11 + ) 12 + 13 + const ( 14 + PreferredHandleByDid = "preferred_handle:did:%s" 15 + PreferredHandleByHandle = "preferred_handle:handle:%s" 16 + PreferredHandleTTL = 24 * time.Hour 17 + ) 4 18 5 19 type Cache struct { 6 20 *redis.Client ··· 12 26 }) 13 27 return &Cache{rdb} 14 28 } 29 + 30 + func LookupPreferredHandle(ctx context.Context, rdb *Cache, e db.Execer, did string) string { 31 + if rdb != nil { 32 + if h, err := rdb.Get(ctx, fmt.Sprintf(PreferredHandleByDid, did)).Result(); err == nil { 33 + return h 34 + } 35 + } 36 + 37 + var handle string 38 + if h, err := db.GetPreferredHandle(e, did); err == nil { 39 + handle = string(h) 40 + } 41 + 42 + if rdb != nil { 43 + pipe := rdb.Pipeline() 44 + pipe.Set(ctx, fmt.Sprintf(PreferredHandleByDid, did), handle, PreferredHandleTTL) 45 + if handle != "" { 46 + pipe.Set(ctx, fmt.Sprintf(PreferredHandleByHandle, handle), did, PreferredHandleTTL) 47 + } 48 + pipe.Exec(ctx) 49 + } 50 + 51 + return handle 52 + } 53 + 54 + func LookupDidByPreferredHandle(ctx context.Context, rdb *Cache, e db.Execer, handle syntax.Handle) string { 55 + handleStr := string(handle) 56 + if rdb != nil { 57 + if d, err := rdb.Get(ctx, fmt.Sprintf(PreferredHandleByHandle, handleStr)).Result(); err == nil { 58 + return d 59 + } 60 + } 61 + 62 + var did string 63 + if d, err := db.GetDidByPreferredHandle(e, handle); err == nil { 64 + did = string(d) 65 + } 66 + 67 + if rdb != nil { 68 + pipe := rdb.Pipeline() 69 + pipe.Set(ctx, fmt.Sprintf(PreferredHandleByHandle, handleStr), did, PreferredHandleTTL) 70 + if did != "" { 71 + pipe.Set(ctx, fmt.Sprintf(PreferredHandleByDid, did), handleStr, PreferredHandleTTL) 72 + } 73 + pipe.Exec(ctx) 74 + } 75 + 76 + return did 77 + }
+15
appview/db/profile.go
··· 354 354 return profileMap, nil 355 355 } 356 356 357 + func GetPreferredHandle(e Execer, did string) (syntax.Handle, error) { 358 + var h sql.Null[string] 359 + err := e.QueryRow( 360 + `select preferred_handle from profile where did = ?`, 361 + did, 362 + ).Scan(&h) 363 + if err != nil { 364 + return "", err 365 + } 366 + if !h.Valid || h.V == "" { 367 + return "", sql.ErrNoRows 368 + } 369 + return syntax.Handle(h.V), nil 370 + } 371 + 357 372 func GetDidByPreferredHandle(e Execer, handle syntax.Handle) (syntax.DID, error) { 358 373 var did string 359 374 err := e.QueryRow(
+15
appview/ingester.go
··· 23 23 "github.com/ipfs/go-cid" 24 24 "golang.org/x/sync/errgroup" 25 25 "tangled.org/core/api/tangled" 26 + "tangled.org/core/appview/cache" 26 27 "tangled.org/core/appview/config" 27 28 "tangled.org/core/appview/db" 28 29 "tangled.org/core/appview/models" ··· 37 38 Db db.DbWrapper 38 39 Enforcer *rbac.Enforcer 39 40 IdResolver *idresolver.Resolver 41 + Cache *cache.Cache 40 42 Config *config.Config 41 43 Logger *slog.Logger 42 44 Validator *validator.Validator ··· 434 436 } 435 437 436 438 err = db.UpsertProfile(tx, &profile) 439 + if err == nil && i.Cache != nil { 440 + pipe := i.Cache.Pipeline() 441 + didKey := fmt.Sprintf(cache.PreferredHandleByDid, did) 442 + if preferredHandle != "" { 443 + pipe.Set(ctx, didKey, string(preferredHandle), cache.PreferredHandleTTL) 444 + pipe.Set(ctx, fmt.Sprintf(cache.PreferredHandleByHandle, string(preferredHandle)), did, cache.PreferredHandleTTL) 445 + } else { 446 + pipe.Del(ctx, didKey) 447 + } 448 + if _, execErr := pipe.Exec(ctx); execErr != nil { 449 + l.Warn("failed to update preferred handle cache", "err", execErr) 450 + } 451 + } 437 452 case jmodels.CommitOperationDelete: 438 453 err = db.DeleteArtifact(i.Db, orm.FilterEq("did", did), orm.FilterEq("rkey", e.Commit.RKey)) 439 454 }
+2 -8
appview/issues/opengraph.go
··· 67 67 } 68 68 } 69 69 70 - var ownerHandle string 71 - owner, err := rp.idResolver.ResolveIdent(context.Background(), f.Did) 72 - if err != nil { 73 - ownerHandle = f.Did 74 - } else { 75 - ownerHandle = owner.Handle.String() 76 - } 70 + ownerHandle := rp.pages.DisplayHandle(r.Context(), f.Did) 77 71 78 - avatarUrl := rp.pages.AvatarUrl(ownerHandle, "256") 72 + avatarUrl := rp.pages.AvatarUrl(f.Did, "256") 79 73 80 74 status := "closed" 81 75 if issue.Open {
+21 -6
appview/middleware/middleware.go
··· 13 13 "github.com/bluesky-social/indigo/atproto/identity" 14 14 "github.com/bluesky-social/indigo/atproto/syntax" 15 15 "github.com/go-chi/chi/v5" 16 + "tangled.org/core/appview/cache" 16 17 "tangled.org/core/appview/db" 17 18 "tangled.org/core/appview/oauth" 18 19 "tangled.org/core/appview/pages" ··· 31 32 repoResolver *reporesolver.RepoResolver 32 33 idResolver *idresolver.Resolver 33 34 pages *pages.Pages 35 + rdb *cache.Cache 34 36 logger *slog.Logger 35 37 } 36 38 37 - func New(oauth *oauth.OAuth, db *db.DB, enforcer *rbac.Enforcer, repoResolver *reporesolver.RepoResolver, idResolver *idresolver.Resolver, pages *pages.Pages, logger *slog.Logger) Middleware { 39 + 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 { 38 40 return Middleware{ 39 41 oauth: oauth, 40 42 db: db, ··· 42 44 repoResolver: repoResolver, 43 45 idResolver: idResolver, 44 46 pages: pages, 47 + rdb: rdb, 45 48 logger: logger, 46 49 } 47 50 } ··· 184 187 185 188 return func(next http.Handler) http.Handler { 186 189 return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 187 - didOrHandle := chi.URLParam(req, "user") 188 - didOrHandle = strings.TrimPrefix(didOrHandle, "@") 190 + origSeg := chi.URLParam(req, "user") 191 + didOrHandle := strings.TrimPrefix(origSeg, "@") 189 192 190 193 if slices.Contains(excluded, didOrHandle) { 191 194 next.ServeHTTP(w, req) 192 195 return 193 196 } 194 197 195 - id, err := mw.idResolver.ResolveIdent(req.Context(), didOrHandle) 198 + id, err := mw.idResolver.ResolveAtIdentifier(req.Context(), didOrHandle) 196 199 if err != nil { 197 200 if h, parseErr := syntax.ParseHandle(didOrHandle); parseErr == nil { 198 - if did, lookupErr := db.GetDidByPreferredHandle(mw.db, h); lookupErr == nil { 199 - id, err = mw.idResolver.ResolveIdent(req.Context(), string(did)) 201 + if did := cache.LookupDidByPreferredHandle(req.Context(), mw.rdb, mw.db, h); did != "" { 202 + id, err = mw.idResolver.ResolveAtIdentifier(req.Context(), did) 200 203 } 201 204 } 202 205 } ··· 204 207 mw.logger.Error("failed to resolve did/handle", "didOrHandle", didOrHandle, "err", err) 205 208 mw.pages.Error404(w) 206 209 return 210 + } 211 + 212 + if req.Method == http.MethodGet && !userutil.IsDid(didOrHandle) { 213 + if pref := cache.LookupPreferredHandle(req.Context(), mw.rdb, mw.db, id.DID.String()); pref != "" && didOrHandle != pref { 214 + rest := strings.TrimPrefix(req.URL.Path, "/"+origSeg) 215 + target := "/" + pref + rest 216 + if req.URL.RawQuery != "" { 217 + target += "?" + req.URL.RawQuery 218 + } 219 + http.Redirect(w, req, target, http.StatusFound) 220 + return 221 + } 207 222 } 208 223 209 224 ctx := context.WithValue(req.Context(), "resolvedId", *id)
+26 -15
appview/pages/funcmap.go
··· 27 27 "github.com/go-enry/go-enry/v2" 28 28 "github.com/yuin/goldmark" 29 29 emoji "github.com/yuin/goldmark-emoji" 30 + "tangled.org/core/appview/cache" 30 31 "tangled.org/core/appview/db" 31 32 "tangled.org/core/appview/models" 32 33 "tangled.org/core/appview/oauth" 33 34 "tangled.org/core/appview/pages/markup" 34 35 "tangled.org/core/crypto" 36 + "tangled.org/core/idresolver" 35 37 ) 36 38 37 39 type tab map[string]string ··· 65 67 return mapValue.MapIndex(keyValue).IsValid() 66 68 }, 67 69 "resolve": func(s string) string { 68 - profile, err := db.GetProfile(p.db, s) 69 - if err == nil && profile != nil && profile.PreferredHandle != "" { 70 - return string(profile.PreferredHandle) 71 - } 72 - 73 - identity, err := p.resolver.ResolveIdent(context.Background(), s) 74 - if err != nil { 75 - return s 76 - } 77 - 78 - if identity.Handle.IsInvalidHandle() { 79 - return "handle.invalid" 80 - } 81 - 82 - return identity.Handle.String() 70 + return p.DisplayHandle(context.Background(), s) 71 + }, 72 + "primaryHandle": func(s string) string { 73 + return primaryHandle(p.resolver, s) 83 74 }, 84 75 "resolvePds": func(s string) string { 85 76 identity, err := p.resolver.ResolveIdent(context.Background(), s) ··· 512 503 } 513 504 }, 514 505 } 506 + } 507 + 508 + func primaryHandle(r *idresolver.Resolver, s string) string { 509 + identity, err := r.ResolveIdent(context.Background(), s) 510 + if err != nil || identity.Handle.IsInvalidHandle() { 511 + return "handle.invalid" 512 + } 513 + return identity.Handle.String() 514 + } 515 + 516 + func (p *Pages) DisplayHandle(ctx context.Context, did string) string { 517 + if p.db != nil { 518 + if h := cache.LookupPreferredHandle(ctx, p.rdb, p.db, did); h != "" { 519 + return h 520 + } 521 + } 522 + if id, err := p.resolver.ResolveIdent(ctx, did); err == nil && !id.Handle.IsInvalidHandle() { 523 + return id.Handle.String() 524 + } 525 + return did 515 526 } 516 527 517 528 func (p *Pages) AvatarUrl(actor, size string) string {
+1 -1
appview/pages/funcmap_test.go
··· 22 22 } 23 23 for _, tt := range tests { 24 24 t.Run(tt.name, func(t *testing.T) { 25 - p := NewPages(tt.config, tt.res, nil, tt.l) 25 + p := NewPages(tt.config, tt.res, nil, nil, tt.l) 26 26 got := p.funcMap() 27 27 // TODO: update the condition below to compare got with tt.want. 28 28 if true {
+4 -1
appview/pages/pages.go
··· 17 17 "time" 18 18 19 19 "tangled.org/core/api/tangled" 20 + "tangled.org/core/appview/cache" 20 21 "tangled.org/core/appview/commitverify" 21 22 "tangled.org/core/appview/config" 22 23 "tangled.org/core/appview/db" ··· 45 46 pdsCfg config.PdsConfig 46 47 resolver *idresolver.Resolver 47 48 db *db.DB 49 + rdb *cache.Cache 48 50 dev bool 49 51 embedFS fs.FS 50 52 templateDir string // Path to templates on disk for dev mode ··· 52 54 logger *slog.Logger 53 55 } 54 56 55 - func NewPages(config *config.Config, res *idresolver.Resolver, database *db.DB, logger *slog.Logger) *Pages { 57 + func NewPages(config *config.Config, res *idresolver.Resolver, database *db.DB, rdb *cache.Cache, logger *slog.Logger) *Pages { 56 58 // initialized with safe defaults, can be overridden per use 57 59 rctx := &markup.RenderContext{ 58 60 IsDev: config.Core.Dev, ··· 72 74 rctx: rctx, 73 75 resolver: res, 74 76 db: database, 77 + rdb: rdb, 75 78 templateDir: "appview/pages", 76 79 logger: logger, 77 80 }
+1 -1
appview/pages/templates/user/fragments/profileCard.html
··· 61 61 {{ if .IncludeBluesky }} 62 62 <div class="flex items-center gap-2"> 63 63 <span class="flex-shrink-0">{{ template "user/fragments/bluesky" "w-4 h-4 text-black dark:text-white" }}</span> 64 - <a id="bluesky-link" href="https://bsky.app/profile/{{ $.UserDid }}">{{ $userIdent }}</a> 64 + <a id="bluesky-link" href="https://bsky.app/profile/{{ $.UserDid }}">{{ primaryHandle $.UserDid }}</a> 65 65 </div> 66 66 {{ end }} 67 67 {{ range $link := .Links }}
+2 -9
appview/pulls/opengraph.go
··· 1 1 package pulls 2 2 3 3 import ( 4 - "context" 5 4 "log" 6 5 "net/http" 7 6 "time" ··· 26 25 return 27 26 } 28 27 29 - var ownerHandle string 30 - owner, err := s.idResolver.ResolveIdent(context.Background(), f.Did) 31 - if err != nil { 32 - ownerHandle = f.Did 33 - } else { 34 - ownerHandle = owner.Handle.String() 35 - } 28 + ownerHandle := s.pages.DisplayHandle(r.Context(), f.Did) 36 29 37 - avatarUrl := s.pages.AvatarUrl(ownerHandle, "256") 30 + avatarUrl := s.pages.AvatarUrl(f.Did, "256") 38 31 39 32 var status string 40 33 if pull.State.IsOpen() {
+2 -9
appview/repo/opengraph.go
··· 1 1 package repo 2 2 3 3 import ( 4 - "context" 5 4 "log" 6 5 "net/http" 7 6 "sort" ··· 21 20 return 22 21 } 23 22 24 - var ownerHandle string 25 - owner, err := rp.idResolver.ResolveIdent(context.Background(), f.Did) 26 - if err != nil { 27 - ownerHandle = f.Did 28 - } else { 29 - ownerHandle = owner.Handle.String() 30 - } 23 + ownerHandle := rp.pages.DisplayHandle(r.Context(), f.Did) 31 24 32 - avatarUrl := rp.pages.AvatarUrl(ownerHandle, "256") 25 + avatarUrl := rp.pages.AvatarUrl(f.Did, "256") 33 26 34 27 var languageStats []types.RepoLanguageDetails 35 28 langs, err := db.GetRepoLanguages(
+10 -3
appview/reporesolver/resolver.go
··· 10 10 11 11 "github.com/bluesky-social/indigo/atproto/identity" 12 12 "github.com/go-chi/chi/v5" 13 + "tangled.org/core/appview/cache" 13 14 "tangled.org/core/appview/config" 14 15 "tangled.org/core/appview/db" 15 16 "tangled.org/core/appview/models" ··· 27 28 config *config.Config 28 29 enforcer *rbac.Enforcer 29 30 execer db.Execer 31 + rdb *cache.Cache 30 32 } 31 33 32 - func New(config *config.Config, enforcer *rbac.Enforcer, execer db.Execer) *RepoResolver { 33 - return &RepoResolver{config: config, enforcer: enforcer, execer: execer} 34 + func New(config *config.Config, enforcer *rbac.Enforcer, execer db.Execer, rdb *cache.Cache) *RepoResolver { 35 + return &RepoResolver{config: config, enforcer: enforcer, execer: execer, rdb: rdb} 34 36 } 35 37 36 38 // NOTE: this... should not even be here. the entire package will be removed in future refactor ··· 116 118 } 117 119 } 118 120 121 + ownerHandle := ownerId.Handle.String() 122 + if h := cache.LookupPreferredHandle(r.Context(), rr.rdb, rr.execer, ownerId.DID.String()); h != "" { 123 + ownerHandle = h 124 + } 125 + 119 126 repoInfo := repoinfo.RepoInfo{ 120 127 // this is basically a models.Repo 121 128 OwnerDid: ownerId.DID.String(), 122 - OwnerHandle: ownerId.Handle.String(), 129 + OwnerHandle: ownerHandle, 123 130 Name: repo.Name, 124 131 Rkey: repo.Rkey, 125 132 Description: repo.Description,
+1 -10
appview/state/login.go
··· 8 8 "time" 9 9 10 10 comatproto "github.com/bluesky-social/indigo/api/atproto" 11 - "github.com/bluesky-social/indigo/atproto/identity" 12 - "github.com/bluesky-social/indigo/atproto/syntax" 13 11 "github.com/bluesky-social/indigo/xrpc" 14 12 "tangled.org/core/appview/pages" 15 13 ) ··· 59 57 return 60 58 } 61 59 62 - ident, err := s.idResolver.ResolveIdent(r.Context(), handle) 63 - if err != nil && errors.Is(err, identity.ErrHandleMismatch) { 64 - if h, parseErr := syntax.ParseHandle(handle); parseErr == nil { 65 - if did, resolveErr := s.idResolver.ResolveHandle(r.Context(), h); resolveErr == nil { 66 - ident, err = s.idResolver.ResolveIdent(r.Context(), did.String()) 67 - } 68 - } 69 - } 60 + ident, err := s.idResolver.ResolveAtIdentifier(r.Context(), handle) 70 61 if err != nil { 71 62 l.Warn("handle resolution failed", "handle", handle, "err", err) 72 63 s.pages.Notice(w, "login-msg", fmt.Sprintf("Could not resolve handle \"%s\". The account may not exist.", handle))
+26 -8
appview/state/profile.go
··· 15 15 "github.com/go-chi/chi/v5" 16 16 "github.com/gorilla/feeds" 17 17 "tangled.org/core/api/tangled" 18 + "tangled.org/core/appview/cache" 18 19 "tangled.org/core/appview/db" 19 20 "tangled.org/core/appview/middleware" 20 21 "tangled.org/core/appview/models" ··· 91 92 92 93 loggedInUser := s.oauth.GetMultiAccountUser(r) 93 94 followStatus := models.IsNotFollowing 95 + var loggedInDid string 94 96 if loggedInUser != nil { 95 97 followStatus = db.GetFollowStatus(s.db, loggedInUser.Did, did) 98 + loggedInDid = loggedInUser.Did 96 99 } 97 100 98 - showPunchcard := s.shouldShowPunchcard(did, loggedInUser.Did) 101 + showPunchcard := s.shouldShowPunchcard(did, loggedInDid) 99 102 100 103 var punchcard *models.Punchcard 101 104 if showPunchcard { ··· 745 748 func (s *State) updateProfile(profile *models.Profile, w http.ResponseWriter, r *http.Request) { 746 749 l := s.logger.With("handler", "updateProfile") 747 750 user := s.oauth.GetMultiAccountUser(r) 748 - tx, err := s.db.BeginTx(r.Context(), nil) 749 - if err != nil { 750 - l.Error("failed to start transaction", "err", err) 751 - s.pages.Notice(w, "update-profile", "Failed to update profile, try again later.") 752 - return 753 - } 754 751 755 752 client, err := s.oauth.AuthorizedClient(r) 756 753 if err != nil { ··· 805 802 return 806 803 } 807 804 808 - err = db.UpsertProfile(tx, profile) 805 + tx, err := s.db.BeginTx(r.Context(), nil) 809 806 if err != nil { 807 + l.Error("failed to start transaction", "err", err) 808 + s.pages.Notice(w, "update-profile", "Failed to update profile, try again later.") 809 + return 810 + } 811 + 812 + if err := db.UpsertProfile(tx, profile); err != nil { 810 813 l.Error("failed to update profile in DB", "err", err) 811 814 s.pages.Notice(w, "update-profile", "Failed to update profile, try again later.") 812 815 return 816 + } 817 + 818 + if s.rdb != nil { 819 + ctx := r.Context() 820 + pipe := s.rdb.Pipeline() 821 + didKey := fmt.Sprintf(cache.PreferredHandleByDid, profile.Did) 822 + if profile.PreferredHandle != "" { 823 + pipe.Set(ctx, didKey, string(profile.PreferredHandle), cache.PreferredHandleTTL) 824 + pipe.Set(ctx, fmt.Sprintf(cache.PreferredHandleByHandle, string(profile.PreferredHandle)), profile.Did, cache.PreferredHandleTTL) 825 + } else { 826 + pipe.Del(ctx, didKey) 827 + } 828 + if _, execErr := pipe.Exec(ctx); execErr != nil { 829 + l.Warn("failed to update preferred handle cache", "err", execErr) 830 + } 813 831 } 814 832 815 833 s.notifier.UpdateProfile(r.Context(), profile)
+1
appview/state/router.go
··· 33 33 s.repoResolver, 34 34 s.idResolver, 35 35 s.pages, 36 + s.rdb, 36 37 s.logger, 37 38 ) 38 39
+16 -5
appview/state/state.go
··· 13 13 "tangled.org/core/api/tangled" 14 14 "tangled.org/core/appview" 15 15 "tangled.org/core/appview/bsky" 16 + "tangled.org/core/appview/cache" 16 17 "tangled.org/core/appview/cloudflare" 17 18 "tangled.org/core/appview/config" 18 19 "tangled.org/core/appview/db" ··· 57 58 enforcer *rbac.Enforcer 58 59 pages *pages.Pages 59 60 idResolver *idresolver.Resolver 61 + rdb *cache.Cache 60 62 mentionsResolver *mentions.Resolver 61 63 posthog posthog.Client 62 64 jc *jetstream.JetstreamClient ··· 92 94 if err != nil { 93 95 logger.Error("failed to create redis resolver", "err", err) 94 96 res = idresolver.DefaultResolver(config.Plc.PLCURL) 97 + } 98 + 99 + var rdb *cache.Cache 100 + if config.Redis.Addr != "" { 101 + rdb = cache.New(config.Redis.Addr) 95 102 } 96 103 97 104 posthog, err := posthog.NewWithConfig(config.Posthog.ApiKey, posthog.Config{Endpoint: config.Posthog.Endpoint}) ··· 99 106 return nil, fmt.Errorf("failed to create posthog client: %w", err) 100 107 } 101 108 102 - pages := pages.NewPages(config, res, d, log.SubLogger(logger, "pages")) 109 + pages := pages.NewPages(config, res, d, rdb, log.SubLogger(logger, "pages")) 103 110 oauth, err := oauth.New(config, posthog, d, enforcer, res, log.SubLogger(logger, "oauth")) 104 111 if err != nil { 105 112 return nil, fmt.Errorf("failed to start oauth handler: %w", err) 106 113 } 107 114 validator := validator.New(d, res, enforcer) 108 115 109 - repoResolver := reporesolver.New(config, enforcer, d) 116 + repoResolver := reporesolver.New(config, enforcer, d, rdb) 110 117 111 118 mentionsResolver := mentions.New(config, res, d, log.SubLogger(logger, "mentionsResolver")) 112 119 ··· 152 159 Db: wrapper, 153 160 Enforcer: enforcer, 154 161 IdResolver: res, 162 + Cache: rdb, 155 163 Config: config, 156 164 Logger: log.SubLogger(logger, "ingester"), 157 165 Validator: validator, ··· 206 214 enforcer: enforcer, 207 215 pages: pages, 208 216 idResolver: res, 217 + rdb: rdb, 209 218 mentionsResolver: mentionsResolver, 210 219 posthog: posthog, 211 220 jc: jc, ··· 631 640 aturi = "" 632 641 633 642 s.notifier.NewRepo(r.Context(), repo) 634 - if repoDid != "" { 643 + switch { 644 + case repoDid != "": 635 645 s.pages.HxLocation(w, fmt.Sprintf("/%s", repoDid)) 636 - } else { 637 - s.pages.HxLocation(w, fmt.Sprintf("/%s/%s", user.Did, repoName)) 646 + default: 647 + handle := s.pages.DisplayHandle(r.Context(), user.Did) 648 + s.pages.HxLocation(w, fmt.Sprintf("/%s/%s", handle, repoName)) 638 649 } 639 650 } 640 651 }
+1 -1
cmd/blog/main.go
··· 54 54 55 55 func makePages(ctx context.Context, cfg *config.Config, logger *slog.Logger) (*pages.Pages, error) { 56 56 resolver := idresolver.DefaultResolver(cfg.Plc.PLCURL) 57 - return pages.NewPages(cfg, resolver, nil, logger), nil 57 + return pages.NewPages(cfg, resolver, nil, nil, logger), nil 58 58 } 59 59 60 60 func runBuild(ctx context.Context, logger *slog.Logger) error {
+34
idresolver/resolver.go
··· 2 2 3 3 import ( 4 4 "context" 5 + "fmt" 5 6 "net" 6 7 "net/http" 8 + "slices" 9 + "strings" 7 10 "sync" 8 11 "time" 9 12 ··· 71 74 }, nil 72 75 } 73 76 77 + type handleResolver interface { 78 + ResolveHandle(ctx context.Context, h syntax.Handle) (syntax.DID, error) 79 + } 80 + 74 81 func (r *Resolver) ResolveHandle(ctx context.Context, handle syntax.Handle) (syntax.DID, error) { 82 + if hr, ok := r.directory.(handleResolver); ok { 83 + return hr.ResolveHandle(ctx, handle) 84 + } 75 85 return r.base.ResolveHandle(ctx, handle) 86 + } 87 + 88 + func (r *Resolver) ResolveAtIdentifier(ctx context.Context, input string) (*identity.Identity, error) { 89 + if did, err := syntax.ParseDID(input); err == nil { 90 + return r.directory.LookupDID(ctx, did) 91 + } 92 + handle, err := syntax.ParseHandle(input) 93 + if err != nil { 94 + return nil, fmt.Errorf("not a did or handle: %w", err) 95 + } 96 + handle = handle.Normalize() 97 + did, err := r.base.ResolveHandle(ctx, handle) 98 + if err != nil { 99 + return nil, fmt.Errorf("resolve handle %q: %w", handle, err) 100 + } 101 + ident, err := r.directory.LookupDID(ctx, did) 102 + if err != nil { 103 + return nil, fmt.Errorf("lookup did for %q: %w", handle, err) 104 + } 105 + aka := "at://" + handle.String() 106 + if !slices.ContainsFunc(ident.AlsoKnownAs, func(s string) bool { return strings.EqualFold(s, aka) }) { 107 + return nil, fmt.Errorf("handle %q not declared in alsoKnownAs for %s", handle, did) 108 + } 109 + return ident, nil 76 110 } 77 111 78 112 func (r *Resolver) ResolveIdent(ctx context.Context, arg string) (*identity.Identity, error) {
+21 -14
knotserver/ingester.go
··· 471 471 } 472 472 473 473 func (h *Knot) processMessages(ctx context.Context, event *jmodels.Event) error { 474 - if event.Kind != jmodels.EventKindCommit { 474 + var err error 475 + switch event.Kind { 476 + case jmodels.EventKindIdentity: 477 + err = h.resolver.InvalidateIdent(ctx, event.Did) 478 + case jmodels.EventKindCommit: 479 + switch event.Commit.Collection { 480 + case tangled.PublicKeyNSID: 481 + err = h.processPublicKey(ctx, event) 482 + case tangled.KnotMemberNSID: 483 + err = h.processKnotMember(ctx, event) 484 + case tangled.RepoPullNSID: 485 + err = h.processPull(ctx, event) 486 + case tangled.RepoCollaboratorNSID: 487 + err = h.processCollaborator(ctx, event) 488 + } 489 + default: 475 490 return nil 476 491 } 477 492 478 - var err error 479 - switch event.Commit.Collection { 480 - case tangled.PublicKeyNSID: 481 - err = h.processPublicKey(ctx, event) 482 - case tangled.KnotMemberNSID: 483 - err = h.processKnotMember(ctx, event) 484 - case tangled.RepoPullNSID: 485 - err = h.processPull(ctx, event) 486 - case tangled.RepoCollaboratorNSID: 487 - err = h.processCollaborator(ctx, event) 488 - } 489 - 490 493 if err != nil { 491 - h.l.Warn("failed to process event, skipping", "nsid", event.Commit.Collection, "err", err) 494 + args := []any{"kind", event.Kind, "err", err} 495 + if event.Kind == jmodels.EventKindCommit { 496 + args = append(args, "nsid", event.Commit.Collection) 497 + } 498 + h.l.Warn("failed to process event, skipping", args...) 492 499 } 493 500 494 501 lastTimeUs := event.TimeUS + 1
+15 -17
knotserver/internal.go
··· 115 115 116 116 case len(components) == 2: 117 117 repoOwner := components[0] 118 - resolver := idresolver.DefaultResolver(h.c.Server.PlcUrl) 119 - repoOwnerIdent, resolveErr := resolver.ResolveIdent(r.Context(), repoOwner) 120 - if resolveErr != nil || repoOwnerIdent.Handle.IsInvalidHandle() { 121 - l.Error("Error resolving handle", "handle", repoOwner, "err", resolveErr) 118 + ownerIdent, resolveErr := h.res.ResolveAtIdentifier(r.Context(), repoOwner) 119 + if resolveErr != nil { 120 + l.Error("error resolving owner", "owner", repoOwner, "err", resolveErr) 122 121 w.WriteHeader(http.StatusInternalServerError) 123 - fmt.Fprintf(w, "error resolving handle: invalid handle\n") 122 + fmt.Fprintf(w, "error resolving owner: invalid did or handle\n") 124 123 return 125 124 } 126 - ownerDid := repoOwnerIdent.DID.String() 125 + ownerDid := ownerIdent.DID 127 126 repoName := components[1] 128 - repoDid, didErr := h.db.GetRepoDid(ownerDid, repoName) 127 + repoDid, didErr := h.db.GetRepoDid(ownerDid.String(), repoName) 129 128 var repoPath string 130 129 if didErr == nil { 131 130 var lookupErr error ··· 138 137 } 139 138 rbacResource = repoDid 140 139 } else { 141 - legacyPath, joinErr := securejoin.SecureJoin(h.c.Repo.ScanPath, filepath.Join(ownerDid, repoName)) 140 + legacyPath, joinErr := securejoin.SecureJoin(h.c.Repo.ScanPath, filepath.Join(ownerDid.String(), repoName)) 142 141 if joinErr != nil { 143 142 w.WriteHeader(http.StatusNotFound) 144 143 fmt.Fprintln(w, "repo not found") ··· 151 150 return 152 151 } 153 152 repoPath = legacyPath 154 - rbacResource = ownerDid + "/" + repoName 153 + rbacResource = ownerDid.String() + "/" + repoName 155 154 } 156 155 rel, relErr := filepath.Rel(h.c.Repo.ScanPath, repoPath) 157 156 if relErr != nil { ··· 476 475 return nil 477 476 } 478 477 479 - func Internal(ctx context.Context, c *config.Config, db *db.DB, e *rbac.Enforcer, n *notifier.Notifier) http.Handler { 478 + func Internal(ctx context.Context, c *config.Config, db *db.DB, e *rbac.Enforcer, n *notifier.Notifier, res *idresolver.Resolver) http.Handler { 480 479 r := chi.NewRouter() 481 480 l := log.FromContext(ctx) 482 481 l = log.SubLogger(l, "internal") 483 - res := idresolver.DefaultResolver(c.Server.PlcUrl) 484 482 485 483 h := InternalHandle{ 486 - db, 487 - c, 488 - e, 489 - l, 490 - n, 491 - res, 484 + db: db, 485 + c: c, 486 + e: e, 487 + l: l, 488 + n: n, 489 + res: res, 492 490 } 493 491 494 492 r.Get("/push-allowed", h.PushAllowed)
+2 -2
knotserver/router.go
··· 36 36 motdMu sync.RWMutex 37 37 } 38 38 39 - func Setup(ctx context.Context, c *config.Config, db *db.DB, e *rbac.Enforcer, jc *jetstream.JetstreamClient, n *notifier.Notifier) (http.Handler, error) { 39 + 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) { 40 40 h := Knot{ 41 41 c: c, 42 42 db: db, ··· 44 44 l: log.FromContext(ctx), 45 45 jc: jc, 46 46 n: n, 47 - resolver: idresolver.DefaultResolver(c.Server.PlcUrl), 47 + resolver: resolver, 48 48 motd: defaultMotd, 49 49 } 50 50
+5 -2
knotserver/server.go
··· 9 9 "github.com/urfave/cli/v3" 10 10 "tangled.org/core/api/tangled" 11 11 "tangled.org/core/hook" 12 + "tangled.org/core/idresolver" 12 13 "tangled.org/core/jetstream" 13 14 "tangled.org/core/knotserver/config" 14 15 "tangled.org/core/knotserver/db" ··· 89 90 90 91 notifier := notifier.New() 91 92 93 + resolver := idresolver.DefaultResolver(c.Server.PlcUrl) 94 + 92 95 go migrateReposOnStartup(ctx, c, db, e, &notifier, log.SubLogger(logger, "migrate")) 93 96 94 - mux, err := Setup(ctx, c, db, e, jc, &notifier) 97 + mux, err := Setup(ctx, c, db, e, jc, &notifier, resolver) 95 98 if err != nil { 96 99 return fmt.Errorf("failed to setup server: %w", err) 97 100 } 98 101 99 - imux := Internal(ctx, c, db, e, &notifier) 102 + imux := Internal(ctx, c, db, e, &notifier, resolver) 100 103 101 104 logger.Info("starting internal server", "address", c.Server.InternalListenAddr) 102 105 go http.ListenAndServe(c.Server.InternalListenAddr, imux)