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>

Lewis 1a0e98e9 3e1b9843

+153 -97
+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(
+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 {
+16 -4
appview/middleware/middleware.go
··· 184 184 185 185 return func(next http.Handler) http.Handler { 186 186 return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 187 - didOrHandle := chi.URLParam(req, "user") 188 - didOrHandle = strings.TrimPrefix(didOrHandle, "@") 187 + origSeg := chi.URLParam(req, "user") 188 + didOrHandle := strings.TrimPrefix(origSeg, "@") 189 189 190 190 if slices.Contains(excluded, didOrHandle) { 191 191 next.ServeHTTP(w, req) 192 192 return 193 193 } 194 194 195 - id, err := mw.idResolver.ResolveIdent(req.Context(), didOrHandle) 195 + id, err := mw.idResolver.ResolveAtIdentifier(req.Context(), didOrHandle) 196 196 if err != nil { 197 197 if h, parseErr := syntax.ParseHandle(didOrHandle); parseErr == nil { 198 198 if did, lookupErr := db.GetDidByPreferredHandle(mw.db, h); lookupErr == nil { 199 - id, err = mw.idResolver.ResolveIdent(req.Context(), string(did)) 199 + id, err = mw.idResolver.ResolveAtIdentifier(req.Context(), string(did)) 200 200 } 201 201 } 202 202 } ··· 204 204 mw.logger.Error("failed to resolve did/handle", "didOrHandle", didOrHandle, "err", err) 205 205 mw.pages.Error404(w) 206 206 return 207 + } 208 + 209 + if req.Method == http.MethodGet && !userutil.IsDid(didOrHandle) { 210 + if pref, prefErr := db.GetPreferredHandle(mw.db, id.DID.String()); prefErr == nil && didOrHandle != string(pref) { 211 + rest := strings.TrimPrefix(req.URL.Path, "/"+origSeg) 212 + target := "/" + string(pref) + rest 213 + if req.URL.RawQuery != "" { 214 + target += "?" + req.URL.RawQuery 215 + } 216 + http.Redirect(w, req, target, http.StatusFound) 217 + return 218 + } 207 219 } 208 220 209 221 ctx := context.WithValue(req.Context(), "resolvedId", *id)
+23 -15
appview/pages/funcmap.go
··· 32 32 "tangled.org/core/appview/oauth" 33 33 "tangled.org/core/appview/pages/markup" 34 34 "tangled.org/core/crypto" 35 + "tangled.org/core/idresolver" 35 36 ) 36 37 37 38 type tab map[string]string ··· 65 66 return mapValue.MapIndex(keyValue).IsValid() 66 67 }, 67 68 "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() 69 + return p.DisplayHandle(context.Background(), s) 70 + }, 71 + "primaryHandle": func(s string) string { 72 + return primaryHandle(p.resolver, s) 83 73 }, 84 74 "resolvePds": func(s string) string { 85 75 identity, err := p.resolver.ResolveIdent(context.Background(), s) ··· 512 502 } 513 503 }, 514 504 } 505 + } 506 + 507 + func primaryHandle(r *idresolver.Resolver, s string) string { 508 + identity, err := r.ResolveIdent(context.Background(), s) 509 + if err != nil || identity.Handle.IsInvalidHandle() { 510 + return "handle.invalid" 511 + } 512 + return identity.Handle.String() 513 + } 514 + 515 + func (p *Pages) DisplayHandle(ctx context.Context, did string) string { 516 + if h, err := db.GetPreferredHandle(p.db, did); err == nil { 517 + return string(h) 518 + } 519 + if id, err := p.resolver.ResolveIdent(ctx, did); err == nil && !id.Handle.IsInvalidHandle() { 520 + return id.Handle.String() 521 + } 522 + return did 515 523 } 516 524 517 525 func (p *Pages) AvatarUrl(actor, size string) string {
+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(
+6 -1
appview/reporesolver/resolver.go
··· 116 116 } 117 117 } 118 118 119 + ownerHandle := ownerId.Handle.String() 120 + if h, err := db.GetPreferredHandle(rr.execer, ownerId.DID.String()); err == nil { 121 + ownerHandle = string(h) 122 + } 123 + 119 124 repoInfo := repoinfo.RepoInfo{ 120 125 // this is basically a models.Repo 121 126 OwnerDid: ownerId.DID.String(), 122 - OwnerHandle: ownerId.Handle.String(), 127 + OwnerHandle: ownerHandle, 123 128 Name: repo.Name, 124 129 Rkey: repo.Rkey, 125 130 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))
+3 -1
appview/state/profile.go
··· 91 91 92 92 loggedInUser := s.oauth.GetMultiAccountUser(r) 93 93 followStatus := models.IsNotFollowing 94 + var loggedInDid string 94 95 if loggedInUser != nil { 95 96 followStatus = db.GetFollowStatus(s.db, loggedInUser.Did, did) 97 + loggedInDid = loggedInUser.Did 96 98 } 97 99 98 - showPunchcard := s.shouldShowPunchcard(did, loggedInUser.Did) 100 + showPunchcard := s.shouldShowPunchcard(did, loggedInDid) 99 101 100 102 var punchcard *models.Punchcard 101 103 if showPunchcard {
-1
appview/state/star.go
··· 137 137 } 138 138 139 139 } 140 -
+5 -3
appview/state/state.go
··· 631 631 aturi = "" 632 632 633 633 s.notifier.NewRepo(r.Context(), repo) 634 - if repoDid != "" { 634 + switch { 635 + case repoDid != "": 635 636 s.pages.HxLocation(w, fmt.Sprintf("/%s", repoDid)) 636 - } else { 637 - s.pages.HxLocation(w, fmt.Sprintf("/%s/%s", user.Did, repoName)) 637 + default: 638 + handle := s.pages.DisplayHandle(r.Context(), user.Did) 639 + s.pages.HxLocation(w, fmt.Sprintf("/%s/%s", handle, repoName)) 638 640 } 639 641 } 640 642 }
+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)