Simplify user tableUnverified
8c01260 parent: c8ebf4d modified
internal/atproto/auth.go +35 -6 | @@ -10,6 +10,7 @@ import ( | ||
| 10 | 10 | |
| 11 | 11 | "github.com/bluesky-social/indigo/atproto/identity" |
| 12 | 12 | "github.com/bluesky-social/indigo/atproto/syntax" |
| 13 | + "pkg.rbrt.fr/glean/internal/httpclient" | |
| 13 | 14 | ) |
| 14 | 15 | |
| 15 | 16 | type ( |
| @@ -26,12 +27,8 @@ func InitIdentity(plcURL string) { | ||
| 26 | 27 | base := identity.BaseDirectory{ |
| 27 | 28 | PLCURL: plcURL, |
| 28 | 29 | HTTPClient: http.Client{ |
| 29 | - Timeout: 10 * time.Second, | |
| 30 | - Transport: &http.Transport{ | |
| 31 | - Proxy: http.ProxyFromEnvironment, | |
| 32 | - IdleConnTimeout: 1000 * time.Millisecond, | |
| 33 | - MaxIdleConns: 100, | |
| 34 | - }, | |
| 30 | + Timeout: 10 * time.Second, | |
| 31 | + Transport: httpclient.NewTransport(), | |
| 35 | 32 | }, |
| 36 | 33 | Resolver: net.Resolver{ |
| 37 | 34 | Dial: func(ctx context.Context, network, address string) (net.Conn, error) { |
| @@ -43,6 +40,7 @@ func InitIdentity(plcURL string) { | ||
| 43 | 40 | SkipDNSDomainSuffixes: []string{".bsky.social"}, |
| 44 | 41 | UserAgent: "glean/1.0", |
| 45 | 42 | } |
| 43 | + | |
| 46 | 44 | directory = identity.NewCacheDirectory(&base, 250_000, 24*time.Hour, 2*time.Minute, 5*time.Minute) |
| 47 | 45 | } |
| 48 | 46 | |
| @@ -113,3 +111,34 @@ func ResolvePDSEndpoint(ctx context.Context, did string) (string, error) { | ||
| 113 | 111 | |
| 114 | 112 | return pds, nil |
| 115 | 113 | } |
| 114 | + | |
| 115 | +type Profile struct { | |
| 116 | + Handle string | |
| 117 | + DisplayName string | |
| 118 | + AvatarURL string | |
| 119 | +} | |
| 120 | + | |
| 121 | +func ResolveProfile(ctx context.Context, did string) Profile { | |
| 122 | + ident, err := ResolveIdentity(ctx, did) | |
| 123 | + if err != nil { | |
| 124 | + return Profile{} | |
| 125 | + } | |
| 126 | + | |
| 127 | + p := Profile{ | |
| 128 | + DisplayName: ident.Handle.String(), | |
| 129 | + Handle: ident.Handle.String(), | |
| 130 | + } | |
| 131 | + | |
| 132 | + h, dn, avatar, err := FetchProfile(ctx, did) | |
| 133 | + if err != nil { | |
| 134 | + return p | |
| 135 | + } | |
| 136 | + | |
| 137 | + if h != "" { | |
| 138 | + p.Handle = h | |
| 139 | + } | |
| 140 | + p.DisplayName = dn | |
| 141 | + p.AvatarURL = avatar | |
| 142 | + | |
| 143 | + return p | |
| 144 | +} | |
| @@ -10,6 +10,7 @@ import ( | |||
| 10 | 10 | ||
| 11 | "github.com/bluesky-social/indigo/atproto/identity" | 11 | "github.com/bluesky-social/indigo/atproto/identity" |
| 12 | "github.com/bluesky-social/indigo/atproto/syntax" | 12 | "github.com/bluesky-social/indigo/atproto/syntax" |
| 13 | + "pkg.rbrt.fr/glean/internal/httpclient" | ||
| 13 | ) | 14 | ) |
| 14 | 15 | ||
| 15 | type ( | 16 | type ( |
| @@ -26,12 +27,8 @@ func InitIdentity(plcURL string) { | |||
| 26 | base := identity.BaseDirectory{ | 27 | base := identity.BaseDirectory{ |
| 27 | PLCURL: plcURL, | 28 | PLCURL: plcURL, |
| 28 | HTTPClient: http.Client{ | 29 | HTTPClient: http.Client{ |
| 29 | - Timeout: 10 * time.Second, | 30 | + Timeout: 10 * time.Second, |
| 30 | - Transport: &http.Transport{ | 31 | + Transport: httpclient.NewTransport(), |
| 31 | - Proxy: http.ProxyFromEnvironment, | ||
| 32 | - IdleConnTimeout: 1000 * time.Millisecond, | ||
| 33 | - MaxIdleConns: 100, | ||
| 34 | - }, | ||
| 35 | }, | 32 | }, |
| 36 | Resolver: net.Resolver{ | 33 | Resolver: net.Resolver{ |
| 37 | Dial: func(ctx context.Context, network, address string) (net.Conn, error) { | 34 | Dial: func(ctx context.Context, network, address string) (net.Conn, error) { |
| @@ -43,6 +40,7 @@ func InitIdentity(plcURL string) { | |||
| 43 | SkipDNSDomainSuffixes: []string{".bsky.social"}, | 40 | SkipDNSDomainSuffixes: []string{".bsky.social"}, |
| 44 | UserAgent: "glean/1.0", | 41 | UserAgent: "glean/1.0", |
| 45 | } | 42 | } |
| 43 | + | ||
| 46 | directory = identity.NewCacheDirectory(&base, 250_000, 24*time.Hour, 2*time.Minute, 5*time.Minute) | 44 | directory = identity.NewCacheDirectory(&base, 250_000, 24*time.Hour, 2*time.Minute, 5*time.Minute) |
| 47 | } | 45 | } |
| 48 | 46 | ||
| @@ -113,3 +111,34 @@ func ResolvePDSEndpoint(ctx context.Context, did string) (string, error) { | |||
| 113 | 111 | ||
| 114 | return pds, nil | 112 | return pds, nil |
| 115 | } | 113 | } |
| 114 | + | ||
| 115 | +type Profile struct { | ||
| 116 | + Handle string | ||
| 117 | + DisplayName string | ||
| 118 | + AvatarURL string | ||
| 119 | +} | ||
| 120 | + | ||
| 121 | +func ResolveProfile(ctx context.Context, did string) Profile { | ||
| 122 | + ident, err := ResolveIdentity(ctx, did) | ||
| 123 | + if err != nil { | ||
| 124 | + return Profile{} | ||
| 125 | + } | ||
| 126 | + | ||
| 127 | + p := Profile{ | ||
| 128 | + DisplayName: ident.Handle.String(), | ||
| 129 | + Handle: ident.Handle.String(), | ||
| 130 | + } | ||
| 131 | + | ||
| 132 | + h, dn, avatar, err := FetchProfile(ctx, did) | ||
| 133 | + if err != nil { | ||
| 134 | + return p | ||
| 135 | + } | ||
| 136 | + | ||
| 137 | + if h != "" { | ||
| 138 | + p.Handle = h | ||
| 139 | + } | ||
| 140 | + p.DisplayName = dn | ||
| 141 | + p.AvatarURL = avatar | ||
| 142 | + | ||
| 143 | + return p | ||
| 144 | +} | ||
modified
internal/atproto/sync.go +4 -25 | @@ -12,10 +12,10 @@ import ( | ||
| 12 | 12 | "encoding/json" |
| 13 | 13 | "fmt" |
| 14 | 14 | "log/slog" |
| 15 | + "maps" | |
| 16 | + "slices" | |
| 15 | 17 | "time" |
| 16 | 18 | |
| 17 | - "golang.org/x/sync/errgroup" | |
| 18 | - | |
| 19 | 19 | "pkg.rbrt.fr/glean/internal/db" |
| 20 | 20 | ) |
| 21 | 21 | |
| @@ -266,30 +266,9 @@ func (s *Sync) syncFollows(ctx context.Context, userDID string) error { | ||
| 266 | 266 | return nil |
| 267 | 267 | } |
| 268 | 268 | |
| 269 | - g, gCtx := errgroup.WithContext(ctx) | |
| 270 | - g.SetLimit(20) | |
| 271 | - | |
| 272 | - dids := make([]string, 0, len(activeFollows)) | |
| 273 | - profiles := make([]db.UserData, len(activeFollows)) | |
| 274 | - for did := range activeFollows { | |
| 275 | - dids = append(dids, did) | |
| 276 | - } | |
| 277 | - | |
| 278 | - for i, did := range dids { | |
| 279 | - g.Go(func() error { | |
| 280 | - if h, dn, avatar, err := FetchProfile(gCtx, did); err == nil { | |
| 281 | - profiles[i] = db.UserData{DID: did, Handle: h, DisplayName: dn, AvatarURL: avatar} | |
| 282 | - } else { | |
| 283 | - profiles[i] = db.UserData{DID: did} | |
| 284 | - } | |
| 285 | - return nil | |
| 286 | - }) | |
| 287 | - } | |
| 288 | - if err := g.Wait(); err != nil { | |
| 289 | - return fmt.Errorf("fetch profiles: %w", err) | |
| 290 | - } | |
| 291 | - if err := s.users.BatchCreateUsers(ctx, profiles); err != nil { | |
| 269 | + if err := s.users.BatchCreateUsers(ctx, slices.Collect(maps.Keys(activeFollows))); err != nil { | |
| 292 | 270 | return fmt.Errorf("batch create users: %w", err) |
| 293 | 271 | } |
| 272 | + | |
| 294 | 273 | return s.users.SyncFollows(ctx, userDID, activeFollows) |
| 295 | 274 | } |
| @@ -12,10 +12,10 @@ import ( | |||
| 12 | "encoding/json" | 12 | "encoding/json" |
| 13 | "fmt" | 13 | "fmt" |
| 14 | "log/slog" | 14 | "log/slog" |
| 15 | + "maps" | ||
| 16 | + "slices" | ||
| 15 | "time" | 17 | "time" |
| 16 | 18 | ||
| 17 | - "golang.org/x/sync/errgroup" | ||
| 18 | - | ||
| 19 | "pkg.rbrt.fr/glean/internal/db" | 19 | "pkg.rbrt.fr/glean/internal/db" |
| 20 | ) | 20 | ) |
| 21 | 21 | ||
| @@ -266,30 +266,9 @@ func (s *Sync) syncFollows(ctx context.Context, userDID string) error { | |||
| 266 | return nil | 266 | return nil |
| 267 | } | 267 | } |
| 268 | 268 | ||
| 269 | - g, gCtx := errgroup.WithContext(ctx) | 269 | + if err := s.users.BatchCreateUsers(ctx, slices.Collect(maps.Keys(activeFollows))); err != nil { |
| 270 | - g.SetLimit(20) | ||
| 271 | - | ||
| 272 | - dids := make([]string, 0, len(activeFollows)) | ||
| 273 | - profiles := make([]db.UserData, len(activeFollows)) | ||
| 274 | - for did := range activeFollows { | ||
| 275 | - dids = append(dids, did) | ||
| 276 | - } | ||
| 277 | - | ||
| 278 | - for i, did := range dids { | ||
| 279 | - g.Go(func() error { | ||
| 280 | - if h, dn, avatar, err := FetchProfile(gCtx, did); err == nil { | ||
| 281 | - profiles[i] = db.UserData{DID: did, Handle: h, DisplayName: dn, AvatarURL: avatar} | ||
| 282 | - } else { | ||
| 283 | - profiles[i] = db.UserData{DID: did} | ||
| 284 | - } | ||
| 285 | - return nil | ||
| 286 | - }) | ||
| 287 | - } | ||
| 288 | - if err := g.Wait(); err != nil { | ||
| 289 | - return fmt.Errorf("fetch profiles: %w", err) | ||
| 290 | - } | ||
| 291 | - if err := s.users.BatchCreateUsers(ctx, profiles); err != nil { | ||
| 292 | return fmt.Errorf("batch create users: %w", err) | 270 | return fmt.Errorf("batch create users: %w", err) |
| 293 | } | 271 | } |
| 272 | + | ||
| 294 | return s.users.SyncFollows(ctx, userDID, activeFollows) | 273 | return s.users.SyncFollows(ctx, userDID, activeFollows) |
| 295 | } | 274 | } |
modified
internal/atproto/xrpc.go +12 -12 | @@ -93,7 +93,7 @@ func (h *XRPCHandler) ListAnnotations(w http.ResponseWriter, r *http.Request) { | ||
| 93 | 93 | cursor := r.URL.Query().Get("cursor") |
| 94 | 94 | |
| 95 | 95 | query := ` |
| 96 | - SELECT a.uri, a.cid, u.did, u.handle, a.feed_url, a.article_url, | |
| 96 | + SELECT a.uri, a.cid, u.did, a.feed_url, a.article_url, | |
| 97 | 97 | a.quote, a.note, a.tags, a.rating, a.created_at |
| 98 | 98 | FROM annotations a |
| 99 | 99 | JOIN users u ON a.author_did = u.did |
| @@ -129,10 +129,10 @@ func (h *XRPCHandler) ListAnnotations(w http.ResponseWriter, r *http.Request) { | ||
| 129 | 129 | |
| 130 | 130 | annotations := make([]AnnotationView, 0) |
| 131 | 131 | for rows.Next() { |
| 132 | - var uri, did, handle, fURL, artURL, createdAt string | |
| 132 | + var uri, did, fURL, artURL, createdAt string | |
| 133 | 133 | var cid, quote, note, tags sql.NullString |
| 134 | 134 | var rating sql.NullInt64 |
| 135 | - if err := rows.Scan(&uri, &cid, &did, &handle, &fURL, &artURL, "e, ¬e, &tags, &rating, &createdAt); err != nil { | |
| 135 | + if err := rows.Scan(&uri, &cid, &did, &fURL, &artURL, "e, ¬e, &tags, &rating, &createdAt); err != nil { | |
| 136 | 136 | http.Error(w, err.Error(), http.StatusInternalServerError) |
| 137 | 137 | return |
| 138 | 138 | } |
| @@ -147,7 +147,7 @@ func (h *XRPCHandler) ListAnnotations(w http.ResponseWriter, r *http.Request) { | ||
| 147 | 147 | CID: cid.String, |
| 148 | 148 | Author: ActorView{ |
| 149 | 149 | DID: did, |
| 150 | - Handle: handle, | |
| 150 | + Handle: ResolveProfile(r.Context(), did).Handle, | |
| 151 | 151 | }, |
| 152 | 152 | Value: AnnotationRecord{ |
| 153 | 153 | CreatedAt: createdAt, |
| @@ -179,7 +179,7 @@ func (h *XRPCHandler) ListLikes(w http.ResponseWriter, r *http.Request) { | ||
| 179 | 179 | cursor := r.URL.Query().Get("cursor") |
| 180 | 180 | |
| 181 | 181 | query := ` |
| 182 | - SELECT l.uri, l.cid, u.did, u.handle, l.feed_url, l.article_url, l.created_at | |
| 182 | + SELECT l.uri, l.cid, u.did, l.feed_url, l.article_url, l.created_at | |
| 183 | 183 | FROM likes l |
| 184 | 184 | JOIN users u ON l.author_did = u.did |
| 185 | 185 | WHERE 1=1` |
| @@ -210,9 +210,9 @@ func (h *XRPCHandler) ListLikes(w http.ResponseWriter, r *http.Request) { | ||
| 210 | 210 | |
| 211 | 211 | likes := make([]LikeView, 0) |
| 212 | 212 | for rows.Next() { |
| 213 | - var uri, did, handle, fURL, artURL, createdAt string | |
| 213 | + var uri, did, fURL, artURL, createdAt string | |
| 214 | 214 | var cid sql.NullString |
| 215 | - if err := rows.Scan(&uri, &cid, &did, &handle, &fURL, &artURL, &createdAt); err != nil { | |
| 215 | + if err := rows.Scan(&uri, &cid, &did, &fURL, &artURL, &createdAt); err != nil { | |
| 216 | 216 | http.Error(w, err.Error(), http.StatusInternalServerError) |
| 217 | 217 | return |
| 218 | 218 | } |
| @@ -222,7 +222,7 @@ func (h *XRPCHandler) ListLikes(w http.ResponseWriter, r *http.Request) { | ||
| 222 | 222 | CID: cid.String, |
| 223 | 223 | Author: ActorView{ |
| 224 | 224 | DID: did, |
| 225 | - Handle: handle, | |
| 225 | + Handle: ResolveProfile(r.Context(), did).Handle, | |
| 226 | 226 | }, |
| 227 | 227 | Value: LikeRecord{ |
| 228 | 228 | CreatedAt: createdAt, |
| @@ -336,7 +336,7 @@ func (h *XRPCHandler) GetRecommendations(w http.ResponseWriter, r *http.Request) | ||
| 336 | 336 | for _, rec := range peopleRecs { |
| 337 | 337 | people = append(people, RecommendedPerson{ |
| 338 | 338 | DID: rec.DID, |
| 339 | - Handle: rec.Handle, | |
| 339 | + Handle: ResolveProfile(r.Context(), rec.DID).Handle, | |
| 340 | 340 | DisplayName: rec.DisplayName, |
| 341 | 341 | Avatar: rec.AvatarURL, |
| 342 | 342 | Jaccard: rec.Jaccard, |
| @@ -370,7 +370,7 @@ func (h *XRPCHandler) ListFeedLists(w http.ResponseWriter, r *http.Request) { | ||
| 370 | 370 | } |
| 371 | 371 | |
| 372 | 372 | query := ` |
| 373 | - SELECT u.did, u.handle, COUNT(s.id) as subscription_count | |
| 373 | + SELECT u.did, COUNT(s.id) as subscription_count | |
| 374 | 374 | FROM users u |
| 375 | 375 | LEFT JOIN subscriptions s ON u.did = s.user_did |
| 376 | 376 | WHERE u.did IN (` + strings.Join(placeholders, ",") + `)` |
| @@ -398,9 +398,9 @@ func (h *XRPCHandler) ListFeedLists(w http.ResponseWriter, r *http.Request) { | ||
| 398 | 398 | var users []userRow |
| 399 | 399 | |
| 400 | 400 | for rows.Next() { |
| 401 | - var did, handle string | |
| 401 | + var did string | |
| 402 | 402 | var subCount int |
| 403 | - if err := rows.Scan(&did, &handle, &subCount); err != nil { | |
| 403 | + if err := rows.Scan(&did, &subCount); err != nil { | |
| 404 | 404 | http.Error(w, err.Error(), http.StatusInternalServerError) |
| 405 | 405 | return |
| 406 | 406 | } |
| @@ -93,7 +93,7 @@ func (h *XRPCHandler) ListAnnotations(w http.ResponseWriter, r *http.Request) { | |||
| 93 | cursor := r.URL.Query().Get("cursor") | 93 | cursor := r.URL.Query().Get("cursor") |
| 94 | 94 | ||
| 95 | query := ` | 95 | query := ` |
| 96 | - SELECT a.uri, a.cid, u.did, u.handle, a.feed_url, a.article_url, | 96 | + SELECT a.uri, a.cid, u.did, a.feed_url, a.article_url, |
| 97 | a.quote, a.note, a.tags, a.rating, a.created_at | 97 | a.quote, a.note, a.tags, a.rating, a.created_at |
| 98 | FROM annotations a | 98 | FROM annotations a |
| 99 | JOIN users u ON a.author_did = u.did | 99 | JOIN users u ON a.author_did = u.did |
| @@ -129,10 +129,10 @@ func (h *XRPCHandler) ListAnnotations(w http.ResponseWriter, r *http.Request) { | |||
| 129 | 129 | ||
| 130 | annotations := make([]AnnotationView, 0) | 130 | annotations := make([]AnnotationView, 0) |
| 131 | for rows.Next() { | 131 | for rows.Next() { |
| 132 | - var uri, did, handle, fURL, artURL, createdAt string | 132 | + var uri, did, fURL, artURL, createdAt string |
| 133 | var cid, quote, note, tags sql.NullString | 133 | var cid, quote, note, tags sql.NullString |
| 134 | var rating sql.NullInt64 | 134 | var rating sql.NullInt64 |
| 135 | - if err := rows.Scan(&uri, &cid, &did, &handle, &fURL, &artURL, "e, ¬e, &tags, &rating, &createdAt); err != nil { | 135 | + if err := rows.Scan(&uri, &cid, &did, &fURL, &artURL, "e, ¬e, &tags, &rating, &createdAt); err != nil { |
| 136 | http.Error(w, err.Error(), http.StatusInternalServerError) | 136 | http.Error(w, err.Error(), http.StatusInternalServerError) |
| 137 | return | 137 | return |
| 138 | } | 138 | } |
| @@ -147,7 +147,7 @@ func (h *XRPCHandler) ListAnnotations(w http.ResponseWriter, r *http.Request) { | |||
| 147 | CID: cid.String, | 147 | CID: cid.String, |
| 148 | Author: ActorView{ | 148 | Author: ActorView{ |
| 149 | DID: did, | 149 | DID: did, |
| 150 | - Handle: handle, | 150 | + Handle: ResolveProfile(r.Context(), did).Handle, |
| 151 | }, | 151 | }, |
| 152 | Value: AnnotationRecord{ | 152 | Value: AnnotationRecord{ |
| 153 | CreatedAt: createdAt, | 153 | CreatedAt: createdAt, |
| @@ -179,7 +179,7 @@ func (h *XRPCHandler) ListLikes(w http.ResponseWriter, r *http.Request) { | |||
| 179 | cursor := r.URL.Query().Get("cursor") | 179 | cursor := r.URL.Query().Get("cursor") |
| 180 | 180 | ||
| 181 | query := ` | 181 | query := ` |
| 182 | - SELECT l.uri, l.cid, u.did, u.handle, l.feed_url, l.article_url, l.created_at | 182 | + SELECT l.uri, l.cid, u.did, l.feed_url, l.article_url, l.created_at |
| 183 | FROM likes l | 183 | FROM likes l |
| 184 | JOIN users u ON l.author_did = u.did | 184 | JOIN users u ON l.author_did = u.did |
| 185 | WHERE 1=1` | 185 | WHERE 1=1` |
| @@ -210,9 +210,9 @@ func (h *XRPCHandler) ListLikes(w http.ResponseWriter, r *http.Request) { | |||
| 210 | 210 | ||
| 211 | likes := make([]LikeView, 0) | 211 | likes := make([]LikeView, 0) |
| 212 | for rows.Next() { | 212 | for rows.Next() { |
| 213 | - var uri, did, handle, fURL, artURL, createdAt string | 213 | + var uri, did, fURL, artURL, createdAt string |
| 214 | var cid sql.NullString | 214 | var cid sql.NullString |
| 215 | - if err := rows.Scan(&uri, &cid, &did, &handle, &fURL, &artURL, &createdAt); err != nil { | 215 | + if err := rows.Scan(&uri, &cid, &did, &fURL, &artURL, &createdAt); err != nil { |
| 216 | http.Error(w, err.Error(), http.StatusInternalServerError) | 216 | http.Error(w, err.Error(), http.StatusInternalServerError) |
| 217 | return | 217 | return |
| 218 | } | 218 | } |
| @@ -222,7 +222,7 @@ func (h *XRPCHandler) ListLikes(w http.ResponseWriter, r *http.Request) { | |||
| 222 | CID: cid.String, | 222 | CID: cid.String, |
| 223 | Author: ActorView{ | 223 | Author: ActorView{ |
| 224 | DID: did, | 224 | DID: did, |
| 225 | - Handle: handle, | 225 | + Handle: ResolveProfile(r.Context(), did).Handle, |
| 226 | }, | 226 | }, |
| 227 | Value: LikeRecord{ | 227 | Value: LikeRecord{ |
| 228 | CreatedAt: createdAt, | 228 | CreatedAt: createdAt, |
| @@ -336,7 +336,7 @@ func (h *XRPCHandler) GetRecommendations(w http.ResponseWriter, r *http.Request) | |||
| 336 | for _, rec := range peopleRecs { | 336 | for _, rec := range peopleRecs { |
| 337 | people = append(people, RecommendedPerson{ | 337 | people = append(people, RecommendedPerson{ |
| 338 | DID: rec.DID, | 338 | DID: rec.DID, |
| 339 | - Handle: rec.Handle, | 339 | + Handle: ResolveProfile(r.Context(), rec.DID).Handle, |
| 340 | DisplayName: rec.DisplayName, | 340 | DisplayName: rec.DisplayName, |
| 341 | Avatar: rec.AvatarURL, | 341 | Avatar: rec.AvatarURL, |
| 342 | Jaccard: rec.Jaccard, | 342 | Jaccard: rec.Jaccard, |
| @@ -370,7 +370,7 @@ func (h *XRPCHandler) ListFeedLists(w http.ResponseWriter, r *http.Request) { | |||
| 370 | } | 370 | } |
| 371 | 371 | ||
| 372 | query := ` | 372 | query := ` |
| 373 | - SELECT u.did, u.handle, COUNT(s.id) as subscription_count | 373 | + SELECT u.did, COUNT(s.id) as subscription_count |
| 374 | FROM users u | 374 | FROM users u |
| 375 | LEFT JOIN subscriptions s ON u.did = s.user_did | 375 | LEFT JOIN subscriptions s ON u.did = s.user_did |
| 376 | WHERE u.did IN (` + strings.Join(placeholders, ",") + `)` | 376 | WHERE u.did IN (` + strings.Join(placeholders, ",") + `)` |
| @@ -398,9 +398,9 @@ func (h *XRPCHandler) ListFeedLists(w http.ResponseWriter, r *http.Request) { | |||
| 398 | var users []userRow | 398 | var users []userRow |
| 399 | 399 | ||
| 400 | for rows.Next() { | 400 | for rows.Next() { |
| 401 | - var did, handle string | 401 | + var did string |
| 402 | var subCount int | 402 | var subCount int |
| 403 | - if err := rows.Scan(&did, &handle, &subCount); err != nil { | 403 | + if err := rows.Scan(&did, &subCount); err != nil { |
| 404 | http.Error(w, err.Error(), http.StatusInternalServerError) | 404 | http.Error(w, err.Error(), http.StatusInternalServerError) |
| 405 | return | 405 | return |
| 406 | } | 406 | } |
modified
internal/cluster/jaccard_test.go +6 -10 | @@ -40,13 +40,9 @@ func setupClusterTestDB(t *testing.T) *db.Databases { | ||
| 40 | 40 | func seedClusterData(t *testing.T, ctx context.Context, dbs *db.Databases) { |
| 41 | 41 | t.Helper() |
| 42 | 42 | |
| 43 | - users := []struct{ did, handle string }{ | |
| 44 | - {"did:test:alice", "alice"}, | |
| 45 | - {"did:test:bob", "bob"}, | |
| 46 | - {"did:test:carol", "carol"}, | |
| 47 | - } | |
| 48 | - for _, u := range users { | |
| 49 | - _, err := dbs.DB().ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, u.did, u.handle) | |
| 43 | + users := []string{"did:test:alice", "did:test:bob", "did:test:carol"} | |
| 44 | + for _, did := range users { | |
| 45 | + _, err := dbs.DB().ExecContext(ctx, `INSERT INTO users (did) VALUES (?)`, did) | |
| 50 | 46 | assert.NilError(t, err) |
| 51 | 47 | } |
| 52 | 48 | |
| @@ -430,7 +426,7 @@ func TestColdStartRecommendations(t *testing.T) { | ||
| 430 | 426 | _, err = dbs.DB().ExecContext(ctx, `UPDATE articles.feeds SET subscriber_count = 2 WHERE feed_url = 'https://b.com/feed'`) |
| 431 | 427 | assert.NilError(t, err) |
| 432 | 428 | |
| 433 | - _, err = dbs.DB().ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, "did:test:newuser", "newuser") | |
| 429 | + _, err = dbs.DB().ExecContext(ctx, `INSERT INTO users (did) VALUES (?)`, "did:test:newuser") | |
| 434 | 430 | assert.NilError(t, err) |
| 435 | 431 | |
| 436 | 432 | recs, err := engine.ColdStartRecommendations(ctx, "did:test:newuser", 10) |
| @@ -513,9 +509,9 @@ func TestDescriptionBasedFeedSimilarity(t *testing.T) { | ||
| 513 | 509 | ctx := context.Background() |
| 514 | 510 | dbs := setupClusterTestDB(t) |
| 515 | 511 | |
| 516 | - _, err := dbs.DB().ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, "did:test:alice", "alice") | |
| 512 | + _, err := dbs.DB().ExecContext(ctx, `INSERT INTO users (did) VALUES (?)`, "did:test:alice") | |
| 517 | 513 | assert.NilError(t, err) |
| 518 | - _, err = dbs.DB().ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, "did:test:bob", "bob") | |
| 514 | + _, err = dbs.DB().ExecContext(ctx, `INSERT INTO users (did) VALUES (?)`, "did:test:bob") | |
| 519 | 515 | assert.NilError(t, err) |
| 520 | 516 | |
| 521 | 517 | _, err = dbs.DB().ExecContext(ctx, `INSERT INTO articles.feeds (feed_url, title, site_url, description, feed_type) VALUES (?, ?, ?, ?, 'rss')`, |
| @@ -40,13 +40,9 @@ func setupClusterTestDB(t *testing.T) *db.Databases { | |||
| 40 | func seedClusterData(t *testing.T, ctx context.Context, dbs *db.Databases) { | 40 | func seedClusterData(t *testing.T, ctx context.Context, dbs *db.Databases) { |
| 41 | t.Helper() | 41 | t.Helper() |
| 42 | 42 | ||
| 43 | - users := []struct{ did, handle string }{ | 43 | + users := []string{"did:test:alice", "did:test:bob", "did:test:carol"} |
| 44 | - {"did:test:alice", "alice"}, | 44 | + for _, did := range users { |
| 45 | - {"did:test:bob", "bob"}, | 45 | + _, err := dbs.DB().ExecContext(ctx, `INSERT INTO users (did) VALUES (?)`, did) |
| 46 | - {"did:test:carol", "carol"}, | ||
| 47 | - } | ||
| 48 | - for _, u := range users { | ||
| 49 | - _, err := dbs.DB().ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, u.did, u.handle) | ||
| 50 | assert.NilError(t, err) | 46 | assert.NilError(t, err) |
| 51 | } | 47 | } |
| 52 | 48 | ||
| @@ -430,7 +426,7 @@ func TestColdStartRecommendations(t *testing.T) { | |||
| 430 | _, err = dbs.DB().ExecContext(ctx, `UPDATE articles.feeds SET subscriber_count = 2 WHERE feed_url = 'https://b.com/feed'`) | 426 | _, err = dbs.DB().ExecContext(ctx, `UPDATE articles.feeds SET subscriber_count = 2 WHERE feed_url = 'https://b.com/feed'`) |
| 431 | assert.NilError(t, err) | 427 | assert.NilError(t, err) |
| 432 | 428 | ||
| 433 | - _, err = dbs.DB().ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, "did:test:newuser", "newuser") | 429 | + _, err = dbs.DB().ExecContext(ctx, `INSERT INTO users (did) VALUES (?)`, "did:test:newuser") |
| 434 | assert.NilError(t, err) | 430 | assert.NilError(t, err) |
| 435 | 431 | ||
| 436 | recs, err := engine.ColdStartRecommendations(ctx, "did:test:newuser", 10) | 432 | recs, err := engine.ColdStartRecommendations(ctx, "did:test:newuser", 10) |
| @@ -513,9 +509,9 @@ func TestDescriptionBasedFeedSimilarity(t *testing.T) { | |||
| 513 | ctx := context.Background() | 509 | ctx := context.Background() |
| 514 | dbs := setupClusterTestDB(t) | 510 | dbs := setupClusterTestDB(t) |
| 515 | 511 | ||
| 516 | - _, err := dbs.DB().ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, "did:test:alice", "alice") | 512 | + _, err := dbs.DB().ExecContext(ctx, `INSERT INTO users (did) VALUES (?)`, "did:test:alice") |
| 517 | assert.NilError(t, err) | 513 | assert.NilError(t, err) |
| 518 | - _, err = dbs.DB().ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, "did:test:bob", "bob") | 514 | + _, err = dbs.DB().ExecContext(ctx, `INSERT INTO users (did) VALUES (?)`, "did:test:bob") |
| 519 | assert.NilError(t, err) | 515 | assert.NilError(t, err) |
| 520 | 516 | ||
| 521 | _, err = dbs.DB().ExecContext(ctx, `INSERT INTO articles.feeds (feed_url, title, site_url, description, feed_type) VALUES (?, ?, ?, ?, 'rss')`, | 517 | _, err = dbs.DB().ExecContext(ctx, `INSERT INTO articles.feeds (feed_url, title, site_url, description, feed_type) VALUES (?, ?, ?, ?, 'rss')`, |
modified
internal/cluster/scoring.go +3 -4 | @@ -248,7 +248,7 @@ func (e *Engine) ComputeArticleRecommendationsOnDemand(ctx context.Context, user | ||
| 248 | 248 | |
| 249 | 249 | func (e *Engine) ComputePeopleRecommendationsOnDemand(ctx context.Context, userDID string, limit int) ([]*PersonRecommendation, error) { |
| 250 | 250 | rows, err := e.db.QueryContext(ctx, ` |
| 251 | - SELECT u.did, u.handle, COALESCE(u.display_name, ''), COALESCE(u.avatar_url, ''), | |
| 251 | + SELECT u.did, | |
| 252 | 252 | sim.jaccard, sim.common_feeds, COALESCE(sim.common_likes, 0), COALESCE(sim.common_tags, 0) |
| 253 | 253 | FROM ( |
| 254 | 254 | SELECT user_b AS peer_did, jaccard, common_feeds, common_likes, common_tags FROM recs.user_similarity WHERE user_a = ? |
| @@ -256,8 +256,7 @@ func (e *Engine) ComputePeopleRecommendationsOnDemand(ctx context.Context, userD | ||
| 256 | 256 | SELECT user_a AS peer_did, jaccard, common_feeds, common_likes, common_tags FROM recs.user_similarity WHERE user_b = ? |
| 257 | 257 | ) sim |
| 258 | 258 | JOIN main.users u ON u.did = sim.peer_did |
| 259 | - WHERE u.handle IS NOT NULL AND u.handle != '' | |
| 260 | - AND EXISTS (SELECT 1 FROM articles.subscriptions s JOIN articles.feeds f ON s.feed_url = f.feed_url WHERE s.user_did = u.did AND f.subscriber_count > 0) | |
| 259 | + WHERE EXISTS (SELECT 1 FROM articles.subscriptions s JOIN articles.feeds f ON s.feed_url = f.feed_url WHERE s.user_did = u.did AND f.subscriber_count > 0) | |
| 261 | 260 | ORDER BY sim.jaccard DESC |
| 262 | 261 | LIMIT ? |
| 263 | 262 | `, userDID, userDID, limit) |
| @@ -269,7 +268,7 @@ func (e *Engine) ComputePeopleRecommendationsOnDemand(ctx context.Context, userD | ||
| 269 | 268 | var results []*PersonRecommendation |
| 270 | 269 | for rows.Next() { |
| 271 | 270 | rec := &PersonRecommendation{} |
| 272 | - if err := rows.Scan(&rec.DID, &rec.Handle, &rec.DisplayName, &rec.AvatarURL, | |
| 271 | + if err := rows.Scan(&rec.DID, | |
| 273 | 272 | &rec.Jaccard, &rec.CommonFeeds, &rec.CommonLikes, &rec.CommonTags); err != nil { |
| 274 | 273 | return nil, err |
| 275 | 274 | } |
| @@ -248,7 +248,7 @@ func (e *Engine) ComputeArticleRecommendationsOnDemand(ctx context.Context, user | |||
| 248 | 248 | ||
| 249 | func (e *Engine) ComputePeopleRecommendationsOnDemand(ctx context.Context, userDID string, limit int) ([]*PersonRecommendation, error) { | 249 | func (e *Engine) ComputePeopleRecommendationsOnDemand(ctx context.Context, userDID string, limit int) ([]*PersonRecommendation, error) { |
| 250 | rows, err := e.db.QueryContext(ctx, ` | 250 | rows, err := e.db.QueryContext(ctx, ` |
| 251 | - SELECT u.did, u.handle, COALESCE(u.display_name, ''), COALESCE(u.avatar_url, ''), | 251 | + SELECT u.did, |
| 252 | sim.jaccard, sim.common_feeds, COALESCE(sim.common_likes, 0), COALESCE(sim.common_tags, 0) | 252 | sim.jaccard, sim.common_feeds, COALESCE(sim.common_likes, 0), COALESCE(sim.common_tags, 0) |
| 253 | FROM ( | 253 | FROM ( |
| 254 | SELECT user_b AS peer_did, jaccard, common_feeds, common_likes, common_tags FROM recs.user_similarity WHERE user_a = ? | 254 | SELECT user_b AS peer_did, jaccard, common_feeds, common_likes, common_tags FROM recs.user_similarity WHERE user_a = ? |
| @@ -256,8 +256,7 @@ func (e *Engine) ComputePeopleRecommendationsOnDemand(ctx context.Context, userD | |||
| 256 | SELECT user_a AS peer_did, jaccard, common_feeds, common_likes, common_tags FROM recs.user_similarity WHERE user_b = ? | 256 | SELECT user_a AS peer_did, jaccard, common_feeds, common_likes, common_tags FROM recs.user_similarity WHERE user_b = ? |
| 257 | ) sim | 257 | ) sim |
| 258 | JOIN main.users u ON u.did = sim.peer_did | 258 | JOIN main.users u ON u.did = sim.peer_did |
| 259 | - WHERE u.handle IS NOT NULL AND u.handle != '' | 259 | + WHERE EXISTS (SELECT 1 FROM articles.subscriptions s JOIN articles.feeds f ON s.feed_url = f.feed_url WHERE s.user_did = u.did AND f.subscriber_count > 0) |
| 260 | - AND EXISTS (SELECT 1 FROM articles.subscriptions s JOIN articles.feeds f ON s.feed_url = f.feed_url WHERE s.user_did = u.did AND f.subscriber_count > 0) | ||
| 261 | ORDER BY sim.jaccard DESC | 260 | ORDER BY sim.jaccard DESC |
| 262 | LIMIT ? | 261 | LIMIT ? |
| 263 | `, userDID, userDID, limit) | 262 | `, userDID, userDID, limit) |
| @@ -269,7 +268,7 @@ func (e *Engine) ComputePeopleRecommendationsOnDemand(ctx context.Context, userD | |||
| 269 | var results []*PersonRecommendation | 268 | var results []*PersonRecommendation |
| 270 | for rows.Next() { | 269 | for rows.Next() { |
| 271 | rec := &PersonRecommendation{} | 270 | rec := &PersonRecommendation{} |
| 272 | - if err := rows.Scan(&rec.DID, &rec.Handle, &rec.DisplayName, &rec.AvatarURL, | 271 | + if err := rows.Scan(&rec.DID, |
| 273 | &rec.Jaccard, &rec.CommonFeeds, &rec.CommonLikes, &rec.CommonTags); err != nil { | 272 | &rec.Jaccard, &rec.CommonFeeds, &rec.CommonLikes, &rec.CommonTags); err != nil { |
| 274 | return nil, err | 273 | return nil, err |
| 275 | } | 274 | } |
modified
internal/db/article_test.go +2 -2 | @@ -33,7 +33,7 @@ func seedArticleReadState(t *testing.T, ctx context.Context, dbs *Databases) (us | ||
| 33 | 33 | userDID = "did:test:user1" |
| 34 | 34 | feedURL = "https://example.com/feed.xml" |
| 35 | 35 | |
| 36 | - _, err := dbs.DB().ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, userDID, "user1") | |
| 36 | + _, err := dbs.DB().ExecContext(ctx, `INSERT INTO users (did) VALUES (?)`, userDID) | |
| 37 | 37 | assert.NilError(t, err) |
| 38 | 38 | |
| 39 | 39 | _, err = dbs.DB().ExecContext(ctx, `INSERT INTO articles.feeds (feed_url, title) VALUES (?, ?)`, feedURL, "Test Feed") |
| @@ -196,7 +196,7 @@ func seedSearchData(t *testing.T, ctx context.Context, dbs *Databases) (userDID, | ||
| 196 | 196 | userDID = "did:test:searcher" |
| 197 | 197 | feedURL = "https://search.example.com/feed.xml" |
| 198 | 198 | |
| 199 | - _, err := dbs.DB().ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, userDID, "searcher") | |
| 199 | + _, err := dbs.DB().ExecContext(ctx, `INSERT INTO users (did) VALUES (?)`, userDID) | |
| 200 | 200 | assert.NilError(t, err) |
| 201 | 201 | |
| 202 | 202 | _, err = dbs.DB().ExecContext(ctx, `INSERT INTO articles.feeds (feed_url, title) VALUES (?, ?)`, feedURL, "Tech Blog") |
| @@ -33,7 +33,7 @@ func seedArticleReadState(t *testing.T, ctx context.Context, dbs *Databases) (us | |||
| 33 | userDID = "did:test:user1" | 33 | userDID = "did:test:user1" |
| 34 | feedURL = "https://example.com/feed.xml" | 34 | feedURL = "https://example.com/feed.xml" |
| 35 | 35 | ||
| 36 | - _, err := dbs.DB().ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, userDID, "user1") | 36 | + _, err := dbs.DB().ExecContext(ctx, `INSERT INTO users (did) VALUES (?)`, userDID) |
| 37 | assert.NilError(t, err) | 37 | assert.NilError(t, err) |
| 38 | 38 | ||
| 39 | _, err = dbs.DB().ExecContext(ctx, `INSERT INTO articles.feeds (feed_url, title) VALUES (?, ?)`, feedURL, "Test Feed") | 39 | _, err = dbs.DB().ExecContext(ctx, `INSERT INTO articles.feeds (feed_url, title) VALUES (?, ?)`, feedURL, "Test Feed") |
| @@ -196,7 +196,7 @@ func seedSearchData(t *testing.T, ctx context.Context, dbs *Databases) (userDID, | |||
| 196 | userDID = "did:test:searcher" | 196 | userDID = "did:test:searcher" |
| 197 | feedURL = "https://search.example.com/feed.xml" | 197 | feedURL = "https://search.example.com/feed.xml" |
| 198 | 198 | ||
| 199 | - _, err := dbs.DB().ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, userDID, "searcher") | 199 | + _, err := dbs.DB().ExecContext(ctx, `INSERT INTO users (did) VALUES (?)`, userDID) |
| 200 | assert.NilError(t, err) | 200 | assert.NilError(t, err) |
| 201 | 201 | ||
| 202 | _, err = dbs.DB().ExecContext(ctx, `INSERT INTO articles.feeds (feed_url, title) VALUES (?, ?)`, feedURL, "Tech Blog") | 202 | _, err = dbs.DB().ExecContext(ctx, `INSERT INTO articles.feeds (feed_url, title) VALUES (?, ?)`, feedURL, "Tech Blog") |
modified
internal/db/batch_test.go +8 -38 | @@ -12,41 +12,31 @@ func TestBatchCreateUsers_InsertsAll(t *testing.T) { | ||
| 12 | 12 | ctx := context.Background() |
| 13 | 13 | dbs := setupTestDB(t) |
| 14 | 14 | |
| 15 | - data := []UserData{ | |
| 16 | - {DID: "did:test:u1", Handle: "user1", DisplayName: "User One", AvatarURL: "https://avatar1.png"}, | |
| 17 | - {DID: "did:test:u2", Handle: "user2", DisplayName: "User Two"}, | |
| 18 | - } | |
| 19 | - err := dbs.Users.BatchCreateUsers(ctx, data) | |
| 15 | + err := dbs.Users.BatchCreateUsers(ctx, []string{"did:test:u1", "did:test:u2"}) | |
| 20 | 16 | assert.NilError(t, err) |
| 21 | 17 | |
| 22 | 18 | u1, err := dbs.Users.GetUser(ctx, "did:test:u1") |
| 23 | 19 | assert.NilError(t, err) |
| 24 | - assert.Equal(t, u1.Handle, "user1") | |
| 25 | - assert.Equal(t, u1.DisplayName.String, "User One") | |
| 20 | + assert.Equal(t, u1.DID, "did:test:u1") | |
| 26 | 21 | |
| 27 | 22 | u2, err := dbs.Users.GetUser(ctx, "did:test:u2") |
| 28 | 23 | assert.NilError(t, err) |
| 29 | - assert.Equal(t, u2.Handle, "user2") | |
| 30 | - assert.Equal(t, u2.DisplayName.String, "User Two") | |
| 24 | + assert.Equal(t, u2.DID, "did:test:u2") | |
| 31 | 25 | } |
| 32 | 26 | |
| 33 | -func TestBatchCreateUsers_UpsertsExisting(t *testing.T) { | |
| 27 | +func TestBatchCreateUsers_IgnoresExisting(t *testing.T) { | |
| 34 | 28 | ctx := context.Background() |
| 35 | 29 | dbs := setupTestDB(t) |
| 36 | 30 | |
| 37 | - _, err := dbs.Users.CreateUser(ctx, "did:test:u1", "old-handle", "", "") | |
| 31 | + _, err := dbs.Users.CreateUser(ctx, "did:test:u1") | |
| 38 | 32 | assert.NilError(t, err) |
| 39 | 33 | |
| 40 | - data := []UserData{ | |
| 41 | - {DID: "did:test:u1", Handle: "new-handle", DisplayName: "New Name"}, | |
| 42 | - } | |
| 43 | - err = dbs.Users.BatchCreateUsers(ctx, data) | |
| 34 | + err = dbs.Users.BatchCreateUsers(ctx, []string{"did:test:u1"}) | |
| 44 | 35 | assert.NilError(t, err) |
| 45 | 36 | |
| 46 | 37 | u, err := dbs.Users.GetUser(ctx, "did:test:u1") |
| 47 | 38 | assert.NilError(t, err) |
| 48 | - assert.Equal(t, u.Handle, "new-handle") | |
| 49 | - assert.Equal(t, u.DisplayName.String, "New Name") | |
| 39 | + assert.Equal(t, u.DID, "did:test:u1") | |
| 50 | 40 | } |
| 51 | 41 | |
| 52 | 42 | func TestBatchCreateUsers_Empty(t *testing.T) { |
| @@ -57,30 +47,10 @@ func TestBatchCreateUsers_Empty(t *testing.T) { | ||
| 57 | 47 | assert.NilError(t, err) |
| 58 | 48 | } |
| 59 | 49 | |
| 60 | -func TestBatchCreateUsers_DoesNotOverwriteWithEmpty(t *testing.T) { | |
| 61 | - ctx := context.Background() | |
| 62 | - dbs := setupTestDB(t) | |
| 63 | - | |
| 64 | - _, err := dbs.Users.CreateUser(ctx, "did:test:u1", "handle", "Existing Name", "https://avatar.png") | |
| 65 | - assert.NilError(t, err) | |
| 66 | - | |
| 67 | - data := []UserData{ | |
| 68 | - {DID: "did:test:u1", Handle: "", DisplayName: "", AvatarURL: ""}, | |
| 69 | - } | |
| 70 | - err = dbs.Users.BatchCreateUsers(ctx, data) | |
| 71 | - assert.NilError(t, err) | |
| 72 | - | |
| 73 | - u, err := dbs.Users.GetUser(ctx, "did:test:u1") | |
| 74 | - assert.NilError(t, err) | |
| 75 | - assert.Equal(t, u.Handle, "handle") | |
| 76 | - assert.Equal(t, u.DisplayName.String, "Existing Name") | |
| 77 | - assert.Equal(t, u.AvatarURL.String, "https://avatar.png") | |
| 78 | -} | |
| 79 | - | |
| 80 | 50 | func seedSubscriptionData(t *testing.T, ctx context.Context, dbs *Databases) (userDID string) { |
| 81 | 51 | t.Helper() |
| 82 | 52 | userDID = "did:test:subuser" |
| 83 | - _, err := dbs.DB().ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, userDID, "subuser") | |
| 53 | + _, err := dbs.DB().ExecContext(ctx, `INSERT INTO users (did) VALUES (?)`, userDID) | |
| 84 | 54 | assert.NilError(t, err) |
| 85 | 55 | return userDID |
| 86 | 56 | } |
| @@ -12,41 +12,31 @@ func TestBatchCreateUsers_InsertsAll(t *testing.T) { | |||
| 12 | ctx := context.Background() | 12 | ctx := context.Background() |
| 13 | dbs := setupTestDB(t) | 13 | dbs := setupTestDB(t) |
| 14 | 14 | ||
| 15 | - data := []UserData{ | 15 | + err := dbs.Users.BatchCreateUsers(ctx, []string{"did:test:u1", "did:test:u2"}) |
| 16 | - {DID: "did:test:u1", Handle: "user1", DisplayName: "User One", AvatarURL: "https://avatar1.png"}, | ||
| 17 | - {DID: "did:test:u2", Handle: "user2", DisplayName: "User Two"}, | ||
| 18 | - } | ||
| 19 | - err := dbs.Users.BatchCreateUsers(ctx, data) | ||
| 20 | assert.NilError(t, err) | 16 | assert.NilError(t, err) |
| 21 | 17 | ||
| 22 | u1, err := dbs.Users.GetUser(ctx, "did:test:u1") | 18 | u1, err := dbs.Users.GetUser(ctx, "did:test:u1") |
| 23 | assert.NilError(t, err) | 19 | assert.NilError(t, err) |
| 24 | - assert.Equal(t, u1.Handle, "user1") | 20 | + assert.Equal(t, u1.DID, "did:test:u1") |
| 25 | - assert.Equal(t, u1.DisplayName.String, "User One") | ||
| 26 | 21 | ||
| 27 | u2, err := dbs.Users.GetUser(ctx, "did:test:u2") | 22 | u2, err := dbs.Users.GetUser(ctx, "did:test:u2") |
| 28 | assert.NilError(t, err) | 23 | assert.NilError(t, err) |
| 29 | - assert.Equal(t, u2.Handle, "user2") | 24 | + assert.Equal(t, u2.DID, "did:test:u2") |
| 30 | - assert.Equal(t, u2.DisplayName.String, "User Two") | ||
| 31 | } | 25 | } |
| 32 | 26 | ||
| 33 | -func TestBatchCreateUsers_UpsertsExisting(t *testing.T) { | 27 | +func TestBatchCreateUsers_IgnoresExisting(t *testing.T) { |
| 34 | ctx := context.Background() | 28 | ctx := context.Background() |
| 35 | dbs := setupTestDB(t) | 29 | dbs := setupTestDB(t) |
| 36 | 30 | ||
| 37 | - _, err := dbs.Users.CreateUser(ctx, "did:test:u1", "old-handle", "", "") | 31 | + _, err := dbs.Users.CreateUser(ctx, "did:test:u1") |
| 38 | assert.NilError(t, err) | 32 | assert.NilError(t, err) |
| 39 | 33 | ||
| 40 | - data := []UserData{ | 34 | + err = dbs.Users.BatchCreateUsers(ctx, []string{"did:test:u1"}) |
| 41 | - {DID: "did:test:u1", Handle: "new-handle", DisplayName: "New Name"}, | ||
| 42 | - } | ||
| 43 | - err = dbs.Users.BatchCreateUsers(ctx, data) | ||
| 44 | assert.NilError(t, err) | 35 | assert.NilError(t, err) |
| 45 | 36 | ||
| 46 | u, err := dbs.Users.GetUser(ctx, "did:test:u1") | 37 | u, err := dbs.Users.GetUser(ctx, "did:test:u1") |
| 47 | assert.NilError(t, err) | 38 | assert.NilError(t, err) |
| 48 | - assert.Equal(t, u.Handle, "new-handle") | 39 | + assert.Equal(t, u.DID, "did:test:u1") |
| 49 | - assert.Equal(t, u.DisplayName.String, "New Name") | ||
| 50 | } | 40 | } |
| 51 | 41 | ||
| 52 | func TestBatchCreateUsers_Empty(t *testing.T) { | 42 | func TestBatchCreateUsers_Empty(t *testing.T) { |
| @@ -57,30 +47,10 @@ func TestBatchCreateUsers_Empty(t *testing.T) { | |||
| 57 | assert.NilError(t, err) | 47 | assert.NilError(t, err) |
| 58 | } | 48 | } |
| 59 | 49 | ||
| 60 | -func TestBatchCreateUsers_DoesNotOverwriteWithEmpty(t *testing.T) { | ||
| 61 | - ctx := context.Background() | ||
| 62 | - dbs := setupTestDB(t) | ||
| 63 | - | ||
| 64 | - _, err := dbs.Users.CreateUser(ctx, "did:test:u1", "handle", "Existing Name", "https://avatar.png") | ||
| 65 | - assert.NilError(t, err) | ||
| 66 | - | ||
| 67 | - data := []UserData{ | ||
| 68 | - {DID: "did:test:u1", Handle: "", DisplayName: "", AvatarURL: ""}, | ||
| 69 | - } | ||
| 70 | - err = dbs.Users.BatchCreateUsers(ctx, data) | ||
| 71 | - assert.NilError(t, err) | ||
| 72 | - | ||
| 73 | - u, err := dbs.Users.GetUser(ctx, "did:test:u1") | ||
| 74 | - assert.NilError(t, err) | ||
| 75 | - assert.Equal(t, u.Handle, "handle") | ||
| 76 | - assert.Equal(t, u.DisplayName.String, "Existing Name") | ||
| 77 | - assert.Equal(t, u.AvatarURL.String, "https://avatar.png") | ||
| 78 | -} | ||
| 79 | - | ||
| 80 | func seedSubscriptionData(t *testing.T, ctx context.Context, dbs *Databases) (userDID string) { | 50 | func seedSubscriptionData(t *testing.T, ctx context.Context, dbs *Databases) (userDID string) { |
| 81 | t.Helper() | 51 | t.Helper() |
| 82 | userDID = "did:test:subuser" | 52 | userDID = "did:test:subuser" |
| 83 | - _, err := dbs.DB().ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, userDID, "subuser") | 53 | + _, err := dbs.DB().ExecContext(ctx, `INSERT INTO users (did) VALUES (?)`, userDID) |
| 84 | assert.NilError(t, err) | 54 | assert.NilError(t, err) |
| 85 | return userDID | 55 | return userDID |
| 86 | } | 56 | } |
modified
internal/db/db.go +0 -4 | @@ -93,9 +93,6 @@ func initSchema(db *DB) error { | ||
| 93 | 93 | var schema = []string{ |
| 94 | 94 | `CREATE TABLE IF NOT EXISTS users ( |
| 95 | 95 | did TEXT PRIMARY KEY, |
| 96 | - handle TEXT NOT NULL, | |
| 97 | - display_name TEXT, | |
| 98 | - avatar_url TEXT, | |
| 99 | 96 | indexed_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, |
| 100 | 97 | updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP |
| 101 | 98 | )`, |
| @@ -274,7 +271,6 @@ var schema = []string{ | ||
| 274 | 271 | `CREATE INDEX IF NOT EXISTS idx_follow_distances_b ON follow_distances(user_b)`, |
| 275 | 272 | `CREATE INDEX IF NOT EXISTS idx_follow_distances_a_dist ON follow_distances(user_a, distance)`, |
| 276 | 273 | `CREATE INDEX IF NOT EXISTS idx_follows_followed_at ON follows(followed_at)`, |
| 277 | - `CREATE INDEX IF NOT EXISTS idx_users_handle ON users(handle)`, | |
| 278 | 274 | `CREATE VIRTUAL TABLE IF NOT EXISTS articles_fts USING fts5(title, summary, content, author, content=articles, content_rowid=id)`, |
| 279 | 275 | `CREATE TRIGGER IF NOT EXISTS articles_ai AFTER INSERT ON articles BEGIN |
| 280 | 276 | INSERT INTO articles_fts(rowid, title, summary, content, author) VALUES (new.id, new.title, new.summary, new.content, new.author); |
| @@ -93,9 +93,6 @@ func initSchema(db *DB) error { | |||
| 93 | var schema = []string{ | 93 | var schema = []string{ |
| 94 | `CREATE TABLE IF NOT EXISTS users ( | 94 | `CREATE TABLE IF NOT EXISTS users ( |
| 95 | did TEXT PRIMARY KEY, | 95 | did TEXT PRIMARY KEY, |
| 96 | - handle TEXT NOT NULL, | ||
| 97 | - display_name TEXT, | ||
| 98 | - avatar_url TEXT, | ||
| 99 | indexed_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, | 96 | indexed_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, |
| 100 | updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP | 97 | updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP |
| 101 | )`, | 98 | )`, |
| @@ -274,7 +271,6 @@ var schema = []string{ | |||
| 274 | `CREATE INDEX IF NOT EXISTS idx_follow_distances_b ON follow_distances(user_b)`, | 271 | `CREATE INDEX IF NOT EXISTS idx_follow_distances_b ON follow_distances(user_b)`, |
| 275 | `CREATE INDEX IF NOT EXISTS idx_follow_distances_a_dist ON follow_distances(user_a, distance)`, | 272 | `CREATE INDEX IF NOT EXISTS idx_follow_distances_a_dist ON follow_distances(user_a, distance)`, |
| 276 | `CREATE INDEX IF NOT EXISTS idx_follows_followed_at ON follows(followed_at)`, | 273 | `CREATE INDEX IF NOT EXISTS idx_follows_followed_at ON follows(followed_at)`, |
| 277 | - `CREATE INDEX IF NOT EXISTS idx_users_handle ON users(handle)`, | ||
| 278 | `CREATE VIRTUAL TABLE IF NOT EXISTS articles_fts USING fts5(title, summary, content, author, content=articles, content_rowid=id)`, | 274 | `CREATE VIRTUAL TABLE IF NOT EXISTS articles_fts USING fts5(title, summary, content, author, content=articles, content_rowid=id)`, |
| 279 | `CREATE TRIGGER IF NOT EXISTS articles_ai AFTER INSERT ON articles BEGIN | 275 | `CREATE TRIGGER IF NOT EXISTS articles_ai AFTER INSERT ON articles BEGIN |
| 280 | INSERT INTO articles_fts(rowid, title, summary, content, author) VALUES (new.id, new.title, new.summary, new.content, new.author); | 276 | INSERT INTO articles_fts(rowid, title, summary, content, author) VALUES (new.id, new.title, new.summary, new.content, new.author); |
modified
internal/db/follow_test.go +5 -5 | @@ -13,9 +13,9 @@ func seedFollowData(t *testing.T, ctx context.Context, dbs *Databases) (userDID, | ||
| 13 | 13 | userDID = "did:test:follower" |
| 14 | 14 | targetDID = "did:test:followed" |
| 15 | 15 | |
| 16 | - _, err := dbs.DB().ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, userDID, "follower") | |
| 16 | + _, err := dbs.DB().ExecContext(ctx, `INSERT INTO users (did) VALUES (?)`, userDID) | |
| 17 | 17 | assert.NilError(t, err) |
| 18 | - _, err = dbs.DB().ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, targetDID, "followed") | |
| 18 | + _, err = dbs.DB().ExecContext(ctx, `INSERT INTO users (did) VALUES (?)`, targetDID) | |
| 19 | 19 | assert.NilError(t, err) |
| 20 | 20 | |
| 21 | 21 | return userDID, targetDID |
| @@ -83,7 +83,7 @@ func TestListFollows(t *testing.T) { | ||
| 83 | 83 | userDID, _ := seedFollowData(t, ctx, dbs) |
| 84 | 84 | |
| 85 | 85 | target2 := "did:test:followed2" |
| 86 | - _, err := dbs.DB().ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, target2, "followed2") | |
| 86 | + _, err := dbs.DB().ExecContext(ctx, `INSERT INTO users (did) VALUES (?)`, target2) | |
| 87 | 87 | assert.NilError(t, err) |
| 88 | 88 | |
| 89 | 89 | err = dbs.Users.UpsertFollow(ctx, userDID, "did:test:followed", "uri1", "cid1") |
| @@ -102,7 +102,7 @@ func TestListFollowers(t *testing.T) { | ||
| 102 | 102 | _, targetDID := seedFollowData(t, ctx, dbs) |
| 103 | 103 | |
| 104 | 104 | follower2 := "did:test:follower2" |
| 105 | - _, err := dbs.DB().ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, follower2, "follower2") | |
| 105 | + _, err := dbs.DB().ExecContext(ctx, `INSERT INTO users (did) VALUES (?)`, follower2) | |
| 106 | 106 | assert.NilError(t, err) |
| 107 | 107 | |
| 108 | 108 | err = dbs.Users.UpsertFollow(ctx, "did:test:follower", targetDID, "uri1", "cid1") |
| @@ -121,7 +121,7 @@ func TestGetFollowDIDs(t *testing.T) { | ||
| 121 | 121 | userDID, _ := seedFollowData(t, ctx, dbs) |
| 122 | 122 | |
| 123 | 123 | target2 := "did:test:followed2" |
| 124 | - _, err := dbs.DB().ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, target2, "followed2") | |
| 124 | + _, err := dbs.DB().ExecContext(ctx, `INSERT INTO users (did) VALUES (?)`, target2) | |
| 125 | 125 | assert.NilError(t, err) |
| 126 | 126 | |
| 127 | 127 | err = dbs.Users.UpsertFollow(ctx, userDID, "did:test:followed", "uri1", "cid1") |
| @@ -13,9 +13,9 @@ func seedFollowData(t *testing.T, ctx context.Context, dbs *Databases) (userDID, | |||
| 13 | userDID = "did:test:follower" | 13 | userDID = "did:test:follower" |
| 14 | targetDID = "did:test:followed" | 14 | targetDID = "did:test:followed" |
| 15 | 15 | ||
| 16 | - _, err := dbs.DB().ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, userDID, "follower") | 16 | + _, err := dbs.DB().ExecContext(ctx, `INSERT INTO users (did) VALUES (?)`, userDID) |
| 17 | assert.NilError(t, err) | 17 | assert.NilError(t, err) |
| 18 | - _, err = dbs.DB().ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, targetDID, "followed") | 18 | + _, err = dbs.DB().ExecContext(ctx, `INSERT INTO users (did) VALUES (?)`, targetDID) |
| 19 | assert.NilError(t, err) | 19 | assert.NilError(t, err) |
| 20 | 20 | ||
| 21 | return userDID, targetDID | 21 | return userDID, targetDID |
| @@ -83,7 +83,7 @@ func TestListFollows(t *testing.T) { | |||
| 83 | userDID, _ := seedFollowData(t, ctx, dbs) | 83 | userDID, _ := seedFollowData(t, ctx, dbs) |
| 84 | 84 | ||
| 85 | target2 := "did:test:followed2" | 85 | target2 := "did:test:followed2" |
| 86 | - _, err := dbs.DB().ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, target2, "followed2") | 86 | + _, err := dbs.DB().ExecContext(ctx, `INSERT INTO users (did) VALUES (?)`, target2) |
| 87 | assert.NilError(t, err) | 87 | assert.NilError(t, err) |
| 88 | 88 | ||
| 89 | err = dbs.Users.UpsertFollow(ctx, userDID, "did:test:followed", "uri1", "cid1") | 89 | err = dbs.Users.UpsertFollow(ctx, userDID, "did:test:followed", "uri1", "cid1") |
| @@ -102,7 +102,7 @@ func TestListFollowers(t *testing.T) { | |||
| 102 | _, targetDID := seedFollowData(t, ctx, dbs) | 102 | _, targetDID := seedFollowData(t, ctx, dbs) |
| 103 | 103 | ||
| 104 | follower2 := "did:test:follower2" | 104 | follower2 := "did:test:follower2" |
| 105 | - _, err := dbs.DB().ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, follower2, "follower2") | 105 | + _, err := dbs.DB().ExecContext(ctx, `INSERT INTO users (did) VALUES (?)`, follower2) |
| 106 | assert.NilError(t, err) | 106 | assert.NilError(t, err) |
| 107 | 107 | ||
| 108 | err = dbs.Users.UpsertFollow(ctx, "did:test:follower", targetDID, "uri1", "cid1") | 108 | err = dbs.Users.UpsertFollow(ctx, "did:test:follower", targetDID, "uri1", "cid1") |
| @@ -121,7 +121,7 @@ func TestGetFollowDIDs(t *testing.T) { | |||
| 121 | userDID, _ := seedFollowData(t, ctx, dbs) | 121 | userDID, _ := seedFollowData(t, ctx, dbs) |
| 122 | 122 | ||
| 123 | target2 := "did:test:followed2" | 123 | target2 := "did:test:followed2" |
| 124 | - _, err := dbs.DB().ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, target2, "followed2") | 124 | + _, err := dbs.DB().ExecContext(ctx, `INSERT INTO users (did) VALUES (?)`, target2) |
| 125 | assert.NilError(t, err) | 125 | assert.NilError(t, err) |
| 126 | 126 | ||
| 127 | err = dbs.Users.UpsertFollow(ctx, userDID, "did:test:followed", "uri1", "cid1") | 127 | err = dbs.Users.UpsertFollow(ctx, userDID, "did:test:followed", "uri1", "cid1") |
modified
internal/db/multi.go +0 -4 | @@ -125,9 +125,6 @@ func initRecsSchema(db *DB) error { | ||
| 125 | 125 | var usersSchema = []string{ |
| 126 | 126 | `CREATE TABLE IF NOT EXISTS users ( |
| 127 | 127 | did TEXT PRIMARY KEY, |
| 128 | - handle TEXT NOT NULL, | |
| 129 | - display_name TEXT, | |
| 130 | - avatar_url TEXT, | |
| 131 | 128 | indexed_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, |
| 132 | 129 | updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP |
| 133 | 130 | )`, |
| @@ -157,7 +154,6 @@ var usersSchema = []string{ | ||
| 157 | 154 | `CREATE INDEX IF NOT EXISTS idx_follows_target ON follows(target_did)`, |
| 158 | 155 | `CREATE INDEX IF NOT EXISTS idx_follows_uri ON follows(uri)`, |
| 159 | 156 | `CREATE INDEX IF NOT EXISTS idx_follows_followed_at ON follows(followed_at)`, |
| 160 | - `CREATE INDEX IF NOT EXISTS idx_users_handle ON users(handle)`, | |
| 161 | 157 | } |
| 162 | 158 | |
| 163 | 159 | var articlesSchema = []string{ |
| @@ -125,9 +125,6 @@ func initRecsSchema(db *DB) error { | |||
| 125 | var usersSchema = []string{ | 125 | var usersSchema = []string{ |
| 126 | `CREATE TABLE IF NOT EXISTS users ( | 126 | `CREATE TABLE IF NOT EXISTS users ( |
| 127 | did TEXT PRIMARY KEY, | 127 | did TEXT PRIMARY KEY, |
| 128 | - handle TEXT NOT NULL, | ||
| 129 | - display_name TEXT, | ||
| 130 | - avatar_url TEXT, | ||
| 131 | indexed_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, | 128 | indexed_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, |
| 132 | updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP | 129 | updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP |
| 133 | )`, | 130 | )`, |
| @@ -157,7 +154,6 @@ var usersSchema = []string{ | |||
| 157 | `CREATE INDEX IF NOT EXISTS idx_follows_target ON follows(target_did)`, | 154 | `CREATE INDEX IF NOT EXISTS idx_follows_target ON follows(target_did)`, |
| 158 | `CREATE INDEX IF NOT EXISTS idx_follows_uri ON follows(uri)`, | 155 | `CREATE INDEX IF NOT EXISTS idx_follows_uri ON follows(uri)`, |
| 159 | `CREATE INDEX IF NOT EXISTS idx_follows_followed_at ON follows(followed_at)`, | 156 | `CREATE INDEX IF NOT EXISTS idx_follows_followed_at ON follows(followed_at)`, |
| 160 | - `CREATE INDEX IF NOT EXISTS idx_users_handle ON users(handle)`, | ||
| 161 | } | 157 | } |
| 162 | 158 | ||
| 163 | var articlesSchema = []string{ | 159 | var articlesSchema = []string{ |
modified
internal/db/social.go +4 -6 | @@ -42,12 +42,11 @@ func (s *ArticleStore) CreateAnnotation(ctx context.Context, a *Annotation) erro | ||
| 42 | 42 | func (s *ArticleStore) GetAnnotation(ctx context.Context, id int64) (*Annotation, error) { |
| 43 | 43 | a := &Annotation{} |
| 44 | 44 | err := s.db.QueryRowContext(ctx, ` |
| 45 | - SELECT a.id, a.uri, a.author_did, COALESCE(u.handle, ''), a.feed_url, a.article_url, ar.id, a.quote, a.note, a.tags, a.rating, a.created_at, a.cid | |
| 45 | + SELECT a.id, a.uri, a.author_did, a.feed_url, a.article_url, ar.id, a.quote, a.note, a.tags, a.rating, a.created_at, a.cid | |
| 46 | 46 | FROM articles.annotations a |
| 47 | - LEFT JOIN users u ON a.author_did = u.did | |
| 48 | 47 | LEFT JOIN articles.articles ar ON ar.url = a.article_url AND ar.feed_url = a.feed_url |
| 49 | 48 | WHERE a.id = ? |
| 50 | - `, id).Scan(&a.ID, &a.URI, &a.AuthorDID, &a.AuthorHandle, &a.FeedURL, &a.ArticleURL, &a.ArticleID, | |
| 49 | + `, id).Scan(&a.ID, &a.URI, &a.AuthorDID, &a.FeedURL, &a.ArticleURL, &a.ArticleID, | |
| 51 | 50 | &a.Quote, &a.Note, &a.Tags, &a.Rating, &a.CreatedAt, &a.CID) |
| 52 | 51 | if err != nil { |
| 53 | 52 | return nil, err |
| @@ -89,9 +88,8 @@ func (s *ArticleStore) ListAnnotations(ctx context.Context, feedURL, articleURL, | ||
| 89 | 88 | args = append(args, authorDID) |
| 90 | 89 | } |
| 91 | 90 | |
| 92 | - query := `SELECT a.id, a.uri, a.author_did, COALESCE(u.handle, ''), a.feed_url, a.article_url, ar.id, a.quote, a.note, a.tags, a.rating, a.created_at, a.cid | |
| 91 | + query := `SELECT a.id, a.uri, a.author_did, a.feed_url, a.article_url, ar.id, a.quote, a.note, a.tags, a.rating, a.created_at, a.cid | |
| 93 | 92 | FROM articles.annotations a |
| 94 | - LEFT JOIN users u ON a.author_did = u.did | |
| 95 | 93 | LEFT JOIN articles.articles ar ON ar.url = a.article_url AND ar.feed_url = a.feed_url` |
| 96 | 94 | if len(conds) > 0 { |
| 97 | 95 | query += ` WHERE ` + strings.Join(conds, " AND ") |
| @@ -108,7 +106,7 @@ func (s *ArticleStore) ListAnnotations(ctx context.Context, feedURL, articleURL, | ||
| 108 | 106 | var annotations []*Annotation |
| 109 | 107 | for rows.Next() { |
| 110 | 108 | a := &Annotation{} |
| 111 | - if err := rows.Scan(&a.ID, &a.URI, &a.AuthorDID, &a.AuthorHandle, &a.FeedURL, &a.ArticleURL, &a.ArticleID, | |
| 109 | + if err := rows.Scan(&a.ID, &a.URI, &a.AuthorDID, &a.FeedURL, &a.ArticleURL, &a.ArticleID, | |
| 112 | 110 | &a.Quote, &a.Note, &a.Tags, &a.Rating, &a.CreatedAt, &a.CID); err != nil { |
| 113 | 111 | return nil, err |
| 114 | 112 | } |
| @@ -42,12 +42,11 @@ func (s *ArticleStore) CreateAnnotation(ctx context.Context, a *Annotation) erro | |||
| 42 | func (s *ArticleStore) GetAnnotation(ctx context.Context, id int64) (*Annotation, error) { | 42 | func (s *ArticleStore) GetAnnotation(ctx context.Context, id int64) (*Annotation, error) { |
| 43 | a := &Annotation{} | 43 | a := &Annotation{} |
| 44 | err := s.db.QueryRowContext(ctx, ` | 44 | err := s.db.QueryRowContext(ctx, ` |
| 45 | - SELECT a.id, a.uri, a.author_did, COALESCE(u.handle, ''), a.feed_url, a.article_url, ar.id, a.quote, a.note, a.tags, a.rating, a.created_at, a.cid | 45 | + SELECT a.id, a.uri, a.author_did, a.feed_url, a.article_url, ar.id, a.quote, a.note, a.tags, a.rating, a.created_at, a.cid |
| 46 | FROM articles.annotations a | 46 | FROM articles.annotations a |
| 47 | - LEFT JOIN users u ON a.author_did = u.did | ||
| 48 | LEFT JOIN articles.articles ar ON ar.url = a.article_url AND ar.feed_url = a.feed_url | 47 | LEFT JOIN articles.articles ar ON ar.url = a.article_url AND ar.feed_url = a.feed_url |
| 49 | WHERE a.id = ? | 48 | WHERE a.id = ? |
| 50 | - `, id).Scan(&a.ID, &a.URI, &a.AuthorDID, &a.AuthorHandle, &a.FeedURL, &a.ArticleURL, &a.ArticleID, | 49 | + `, id).Scan(&a.ID, &a.URI, &a.AuthorDID, &a.FeedURL, &a.ArticleURL, &a.ArticleID, |
| 51 | &a.Quote, &a.Note, &a.Tags, &a.Rating, &a.CreatedAt, &a.CID) | 50 | &a.Quote, &a.Note, &a.Tags, &a.Rating, &a.CreatedAt, &a.CID) |
| 52 | if err != nil { | 51 | if err != nil { |
| 53 | return nil, err | 52 | return nil, err |
| @@ -89,9 +88,8 @@ func (s *ArticleStore) ListAnnotations(ctx context.Context, feedURL, articleURL, | |||
| 89 | args = append(args, authorDID) | 88 | args = append(args, authorDID) |
| 90 | } | 89 | } |
| 91 | 90 | ||
| 92 | - query := `SELECT a.id, a.uri, a.author_did, COALESCE(u.handle, ''), a.feed_url, a.article_url, ar.id, a.quote, a.note, a.tags, a.rating, a.created_at, a.cid | 91 | + query := `SELECT a.id, a.uri, a.author_did, a.feed_url, a.article_url, ar.id, a.quote, a.note, a.tags, a.rating, a.created_at, a.cid |
| 93 | FROM articles.annotations a | 92 | FROM articles.annotations a |
| 94 | - LEFT JOIN users u ON a.author_did = u.did | ||
| 95 | LEFT JOIN articles.articles ar ON ar.url = a.article_url AND ar.feed_url = a.feed_url` | 93 | LEFT JOIN articles.articles ar ON ar.url = a.article_url AND ar.feed_url = a.feed_url` |
| 96 | if len(conds) > 0 { | 94 | if len(conds) > 0 { |
| 97 | query += ` WHERE ` + strings.Join(conds, " AND ") | 95 | query += ` WHERE ` + strings.Join(conds, " AND ") |
| @@ -108,7 +106,7 @@ func (s *ArticleStore) ListAnnotations(ctx context.Context, feedURL, articleURL, | |||
| 108 | var annotations []*Annotation | 106 | var annotations []*Annotation |
| 109 | for rows.Next() { | 107 | for rows.Next() { |
| 110 | a := &Annotation{} | 108 | a := &Annotation{} |
| 111 | - if err := rows.Scan(&a.ID, &a.URI, &a.AuthorDID, &a.AuthorHandle, &a.FeedURL, &a.ArticleURL, &a.ArticleID, | 109 | + if err := rows.Scan(&a.ID, &a.URI, &a.AuthorDID, &a.FeedURL, &a.ArticleURL, &a.ArticleID, |
| 112 | &a.Quote, &a.Note, &a.Tags, &a.Rating, &a.CreatedAt, &a.CID); err != nil { | 110 | &a.Quote, &a.Note, &a.Tags, &a.Rating, &a.CreatedAt, &a.CID); err != nil { |
| 113 | return nil, err | 111 | return nil, err |
| 114 | } | 112 | } |
modified
internal/db/user.go +14 -49 | @@ -6,19 +6,12 @@ import ( | ||
| 6 | 6 | ) |
| 7 | 7 | |
| 8 | 8 | type User struct { |
| 9 | - DID string | |
| 10 | - Handle string | |
| 11 | - DisplayName sql.NullString | |
| 12 | - AvatarURL sql.NullString | |
| 13 | - IndexedAt sql.NullTime | |
| 14 | - UpdatedAt sql.NullTime | |
| 15 | -} | |
| 16 | - | |
| 17 | -type UserData struct { | |
| 18 | 9 | DID string |
| 19 | 10 | Handle string |
| 20 | 11 | DisplayName string |
| 21 | 12 | AvatarURL string |
| 13 | + IndexedAt sql.NullTime | |
| 14 | + UpdatedAt sql.NullTime | |
| 22 | 15 | } |
| 23 | 16 | |
| 24 | 17 | type UserStore struct { |
| @@ -29,8 +22,8 @@ func NewUserStore(db *DB) *UserStore { | ||
| 29 | 22 | return &UserStore{db: db} |
| 30 | 23 | } |
| 31 | 24 | |
| 32 | -func (s *UserStore) BatchCreateUsers(ctx context.Context, users []UserData) error { | |
| 33 | - if len(users) == 0 { | |
| 25 | +func (s *UserStore) BatchCreateUsers(ctx context.Context, dids []string) error { | |
| 26 | + if len(dids) == 0 { | |
| 34 | 27 | return nil |
| 35 | 28 | } |
| 36 | 29 | tx, err := s.db.BeginTx(ctx, nil) |
| @@ -40,29 +33,24 @@ func (s *UserStore) BatchCreateUsers(ctx context.Context, users []UserData) erro | ||
| 40 | 33 | defer tx.Rollback() |
| 41 | 34 | |
| 42 | 35 | stmt, err := tx.PrepareContext(ctx, ` |
| 43 | - INSERT INTO users (did, handle, display_name, avatar_url, updated_at) | |
| 44 | - VALUES (?, COALESCE(NULLIF(?, ''), ?), NULLIF(?, ''), NULLIF(?, ''), CURRENT_TIMESTAMP) | |
| 45 | - ON CONFLICT(did) DO UPDATE SET | |
| 46 | - handle = COALESCE(NULLIF(excluded.handle, ''), users.handle), | |
| 47 | - display_name = COALESCE(NULLIF(excluded.display_name, ''), users.display_name), | |
| 48 | - avatar_url = COALESCE(NULLIF(excluded.avatar_url, ''), users.avatar_url), | |
| 49 | - updated_at = CURRENT_TIMESTAMP | |
| 36 | + INSERT OR IGNORE INTO users (did, updated_at) | |
| 37 | + VALUES (?, CURRENT_TIMESTAMP) | |
| 50 | 38 | `) |
| 51 | 39 | if err != nil { |
| 52 | 40 | return err |
| 53 | 41 | } |
| 54 | 42 | defer stmt.Close() |
| 55 | 43 | |
| 56 | - for _, u := range users { | |
| 57 | - if _, err := stmt.ExecContext(ctx, u.DID, u.Handle, u.DID, u.DisplayName, u.AvatarURL); err != nil { | |
| 44 | + for _, did := range dids { | |
| 45 | + if _, err := stmt.ExecContext(ctx, did); err != nil { | |
| 58 | 46 | return err |
| 59 | 47 | } |
| 60 | 48 | } |
| 61 | 49 | return tx.Commit() |
| 62 | 50 | } |
| 63 | 51 | |
| 64 | -func (s *UserStore) CreateUser(ctx context.Context, did, handle, displayName, avatarURL string) (*User, error) { | |
| 65 | - err := s.BatchCreateUsers(ctx, []UserData{{DID: did, Handle: handle, DisplayName: displayName, AvatarURL: avatarURL}}) | |
| 52 | +func (s *UserStore) CreateUser(ctx context.Context, did string) (*User, error) { | |
| 53 | + err := s.BatchCreateUsers(ctx, []string{did}) | |
| 66 | 54 | if err != nil { |
| 67 | 55 | return nil, err |
| 68 | 56 | } |
| @@ -72,21 +60,9 @@ func (s *UserStore) CreateUser(ctx context.Context, did, handle, displayName, av | ||
| 72 | 60 | func (s *UserStore) GetUser(ctx context.Context, did string) (*User, error) { |
| 73 | 61 | u := &User{} |
| 74 | 62 | err := s.db.QueryRowContext(ctx, ` |
| 75 | - SELECT did, handle, display_name, avatar_url, indexed_at, updated_at | |
| 63 | + SELECT did, indexed_at, updated_at | |
| 76 | 64 | FROM users WHERE did = ? |
| 77 | - `, did).Scan(&u.DID, &u.Handle, &u.DisplayName, &u.AvatarURL, &u.IndexedAt, &u.UpdatedAt) | |
| 78 | - if err != nil { | |
| 79 | - return nil, err | |
| 80 | - } | |
| 81 | - return u, nil | |
| 82 | -} | |
| 83 | - | |
| 84 | -func (s *UserStore) GetUserByHandle(ctx context.Context, handle string) (*User, error) { | |
| 85 | - u := &User{} | |
| 86 | - err := s.db.QueryRowContext(ctx, ` | |
| 87 | - SELECT did, handle, display_name, avatar_url, indexed_at, updated_at | |
| 88 | - FROM users WHERE handle = ? | |
| 89 | - `, handle).Scan(&u.DID, &u.Handle, &u.DisplayName, &u.AvatarURL, &u.IndexedAt, &u.UpdatedAt) | |
| 65 | + `, did).Scan(&u.DID, &u.IndexedAt, &u.UpdatedAt) | |
| 90 | 66 | if err != nil { |
| 91 | 67 | return nil, err |
| 92 | 68 | } |
| @@ -111,20 +87,9 @@ func (s *UserStore) ListUserDIDs(ctx context.Context) (map[string]bool, error) { | ||
| 111 | 87 | return dids, rows.Err() |
| 112 | 88 | } |
| 113 | 89 | |
| 114 | -func (s *UserStore) UpdateUserProfile(ctx context.Context, did, displayName, avatarURL string) error { | |
| 115 | - _, err := s.db.ExecContext(ctx, ` | |
| 116 | - UPDATE users SET | |
| 117 | - display_name = COALESCE(NULLIF(?, ''), display_name), | |
| 118 | - avatar_url = COALESCE(NULLIF(?, ''), avatar_url), | |
| 119 | - updated_at = CURRENT_TIMESTAMP | |
| 120 | - WHERE did = ? | |
| 121 | - `, displayName, avatarURL, did) | |
| 122 | - return err | |
| 123 | -} | |
| 124 | - | |
| 125 | 90 | func (s *UserStore) ListUsers(ctx context.Context) ([]*User, error) { |
| 126 | 91 | rows, err := s.db.QueryContext(ctx, ` |
| 127 | - SELECT did, handle, display_name, avatar_url, indexed_at, updated_at | |
| 92 | + SELECT did, indexed_at, updated_at | |
| 128 | 93 | FROM users ORDER BY updated_at DESC |
| 129 | 94 | `) |
| 130 | 95 | if err != nil { |
| @@ -135,7 +100,7 @@ func (s *UserStore) ListUsers(ctx context.Context) ([]*User, error) { | ||
| 135 | 100 | var users []*User |
| 136 | 101 | for rows.Next() { |
| 137 | 102 | u := &User{} |
| 138 | - if err := rows.Scan(&u.DID, &u.Handle, &u.DisplayName, &u.AvatarURL, &u.IndexedAt, &u.UpdatedAt); err != nil { | |
| 103 | + if err := rows.Scan(&u.DID, &u.IndexedAt, &u.UpdatedAt); err != nil { | |
| 139 | 104 | return nil, err |
| 140 | 105 | } |
| 141 | 106 | users = append(users, u) |
| @@ -6,19 +6,12 @@ import ( | |||
| 6 | ) | 6 | ) |
| 7 | 7 | ||
| 8 | type User struct { | 8 | type User struct { |
| 9 | - DID string | ||
| 10 | - Handle string | ||
| 11 | - DisplayName sql.NullString | ||
| 12 | - AvatarURL sql.NullString | ||
| 13 | - IndexedAt sql.NullTime | ||
| 14 | - UpdatedAt sql.NullTime | ||
| 15 | -} | ||
| 16 | - | ||
| 17 | -type UserData struct { | ||
| 18 | DID string | 9 | DID string |
| 19 | Handle string | 10 | Handle string |
| 20 | DisplayName string | 11 | DisplayName string |
| 21 | AvatarURL string | 12 | AvatarURL string |
| 13 | + IndexedAt sql.NullTime | ||
| 14 | + UpdatedAt sql.NullTime | ||
| 22 | } | 15 | } |
| 23 | 16 | ||
| 24 | type UserStore struct { | 17 | type UserStore struct { |
| @@ -29,8 +22,8 @@ func NewUserStore(db *DB) *UserStore { | |||
| 29 | return &UserStore{db: db} | 22 | return &UserStore{db: db} |
| 30 | } | 23 | } |
| 31 | 24 | ||
| 32 | -func (s *UserStore) BatchCreateUsers(ctx context.Context, users []UserData) error { | 25 | +func (s *UserStore) BatchCreateUsers(ctx context.Context, dids []string) error { |
| 33 | - if len(users) == 0 { | 26 | + if len(dids) == 0 { |
| 34 | return nil | 27 | return nil |
| 35 | } | 28 | } |
| 36 | tx, err := s.db.BeginTx(ctx, nil) | 29 | tx, err := s.db.BeginTx(ctx, nil) |
| @@ -40,29 +33,24 @@ func (s *UserStore) BatchCreateUsers(ctx context.Context, users []UserData) erro | |||
| 40 | defer tx.Rollback() | 33 | defer tx.Rollback() |
| 41 | 34 | ||
| 42 | stmt, err := tx.PrepareContext(ctx, ` | 35 | stmt, err := tx.PrepareContext(ctx, ` |
| 43 | - INSERT INTO users (did, handle, display_name, avatar_url, updated_at) | 36 | + INSERT OR IGNORE INTO users (did, updated_at) |
| 44 | - VALUES (?, COALESCE(NULLIF(?, ''), ?), NULLIF(?, ''), NULLIF(?, ''), CURRENT_TIMESTAMP) | 37 | + VALUES (?, CURRENT_TIMESTAMP) |
| 45 | - ON CONFLICT(did) DO UPDATE SET | ||
| 46 | - handle = COALESCE(NULLIF(excluded.handle, ''), users.handle), | ||
| 47 | - display_name = COALESCE(NULLIF(excluded.display_name, ''), users.display_name), | ||
| 48 | - avatar_url = COALESCE(NULLIF(excluded.avatar_url, ''), users.avatar_url), | ||
| 49 | - updated_at = CURRENT_TIMESTAMP | ||
| 50 | `) | 38 | `) |
| 51 | if err != nil { | 39 | if err != nil { |
| 52 | return err | 40 | return err |
| 53 | } | 41 | } |
| 54 | defer stmt.Close() | 42 | defer stmt.Close() |
| 55 | 43 | ||
| 56 | - for _, u := range users { | 44 | + for _, did := range dids { |
| 57 | - if _, err := stmt.ExecContext(ctx, u.DID, u.Handle, u.DID, u.DisplayName, u.AvatarURL); err != nil { | 45 | + if _, err := stmt.ExecContext(ctx, did); err != nil { |
| 58 | return err | 46 | return err |
| 59 | } | 47 | } |
| 60 | } | 48 | } |
| 61 | return tx.Commit() | 49 | return tx.Commit() |
| 62 | } | 50 | } |
| 63 | 51 | ||
| 64 | -func (s *UserStore) CreateUser(ctx context.Context, did, handle, displayName, avatarURL string) (*User, error) { | 52 | +func (s *UserStore) CreateUser(ctx context.Context, did string) (*User, error) { |
| 65 | - err := s.BatchCreateUsers(ctx, []UserData{{DID: did, Handle: handle, DisplayName: displayName, AvatarURL: avatarURL}}) | 53 | + err := s.BatchCreateUsers(ctx, []string{did}) |
| 66 | if err != nil { | 54 | if err != nil { |
| 67 | return nil, err | 55 | return nil, err |
| 68 | } | 56 | } |
| @@ -72,21 +60,9 @@ func (s *UserStore) CreateUser(ctx context.Context, did, handle, displayName, av | |||
| 72 | func (s *UserStore) GetUser(ctx context.Context, did string) (*User, error) { | 60 | func (s *UserStore) GetUser(ctx context.Context, did string) (*User, error) { |
| 73 | u := &User{} | 61 | u := &User{} |
| 74 | err := s.db.QueryRowContext(ctx, ` | 62 | err := s.db.QueryRowContext(ctx, ` |
| 75 | - SELECT did, handle, display_name, avatar_url, indexed_at, updated_at | 63 | + SELECT did, indexed_at, updated_at |
| 76 | FROM users WHERE did = ? | 64 | FROM users WHERE did = ? |
| 77 | - `, did).Scan(&u.DID, &u.Handle, &u.DisplayName, &u.AvatarURL, &u.IndexedAt, &u.UpdatedAt) | 65 | + `, did).Scan(&u.DID, &u.IndexedAt, &u.UpdatedAt) |
| 78 | - if err != nil { | ||
| 79 | - return nil, err | ||
| 80 | - } | ||
| 81 | - return u, nil | ||
| 82 | -} | ||
| 83 | - | ||
| 84 | -func (s *UserStore) GetUserByHandle(ctx context.Context, handle string) (*User, error) { | ||
| 85 | - u := &User{} | ||
| 86 | - err := s.db.QueryRowContext(ctx, ` | ||
| 87 | - SELECT did, handle, display_name, avatar_url, indexed_at, updated_at | ||
| 88 | - FROM users WHERE handle = ? | ||
| 89 | - `, handle).Scan(&u.DID, &u.Handle, &u.DisplayName, &u.AvatarURL, &u.IndexedAt, &u.UpdatedAt) | ||
| 90 | if err != nil { | 66 | if err != nil { |
| 91 | return nil, err | 67 | return nil, err |
| 92 | } | 68 | } |
| @@ -111,20 +87,9 @@ func (s *UserStore) ListUserDIDs(ctx context.Context) (map[string]bool, error) { | |||
| 111 | return dids, rows.Err() | 87 | return dids, rows.Err() |
| 112 | } | 88 | } |
| 113 | 89 | ||
| 114 | -func (s *UserStore) UpdateUserProfile(ctx context.Context, did, displayName, avatarURL string) error { | ||
| 115 | - _, err := s.db.ExecContext(ctx, ` | ||
| 116 | - UPDATE users SET | ||
| 117 | - display_name = COALESCE(NULLIF(?, ''), display_name), | ||
| 118 | - avatar_url = COALESCE(NULLIF(?, ''), avatar_url), | ||
| 119 | - updated_at = CURRENT_TIMESTAMP | ||
| 120 | - WHERE did = ? | ||
| 121 | - `, displayName, avatarURL, did) | ||
| 122 | - return err | ||
| 123 | -} | ||
| 124 | - | ||
| 125 | func (s *UserStore) ListUsers(ctx context.Context) ([]*User, error) { | 90 | func (s *UserStore) ListUsers(ctx context.Context) ([]*User, error) { |
| 126 | rows, err := s.db.QueryContext(ctx, ` | 91 | rows, err := s.db.QueryContext(ctx, ` |
| 127 | - SELECT did, handle, display_name, avatar_url, indexed_at, updated_at | 92 | + SELECT did, indexed_at, updated_at |
| 128 | FROM users ORDER BY updated_at DESC | 93 | FROM users ORDER BY updated_at DESC |
| 129 | `) | 94 | `) |
| 130 | if err != nil { | 95 | if err != nil { |
| @@ -135,7 +100,7 @@ func (s *UserStore) ListUsers(ctx context.Context) ([]*User, error) { | |||
| 135 | var users []*User | 100 | var users []*User |
| 136 | for rows.Next() { | 101 | for rows.Next() { |
| 137 | u := &User{} | 102 | u := &User{} |
| 138 | - if err := rows.Scan(&u.DID, &u.Handle, &u.DisplayName, &u.AvatarURL, &u.IndexedAt, &u.UpdatedAt); err != nil { | 103 | + if err := rows.Scan(&u.DID, &u.IndexedAt, &u.UpdatedAt); err != nil { |
| 139 | return nil, err | 104 | return nil, err |
| 140 | } | 105 | } |
| 141 | users = append(users, u) | 106 | users = append(users, u) |
modified
internal/db/user_test.go +5 -25 | @@ -7,35 +7,15 @@ import ( | ||
| 7 | 7 | "gotest.tools/v3/assert" |
| 8 | 8 | ) |
| 9 | 9 | |
| 10 | -func TestUpdateUserProfile_SetsFields(t *testing.T) { | |
| 10 | +func TestGetUser(t *testing.T) { | |
| 11 | 11 | ctx := context.Background() |
| 12 | 12 | dbs := setupTestDB(t) |
| 13 | 13 | |
| 14 | - _, err := dbs.DB().ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, "did:test:profile", "tester") | |
| 14 | + u, err := dbs.Users.CreateUser(ctx, "did:test:profile") | |
| 15 | 15 | assert.NilError(t, err) |
| 16 | + assert.Equal(t, u.DID, "did:test:profile") | |
| 16 | 17 | |
| 17 | - err = dbs.Users.UpdateUserProfile(ctx, "did:test:profile", "Display Name", "https://cdn.bsky.app/img/avatar.png") | |
| 18 | + got, err := dbs.Users.GetUser(ctx, "did:test:profile") | |
| 18 | 19 | assert.NilError(t, err) |
| 19 | - | |
| 20 | - u, err := dbs.Users.GetUser(ctx, "did:test:profile") | |
| 21 | - assert.NilError(t, err) | |
| 22 | - assert.Equal(t, u.DisplayName.String, "Display Name") | |
| 23 | - assert.Equal(t, u.AvatarURL.String, "https://cdn.bsky.app/img/avatar.png") | |
| 24 | -} | |
| 25 | - | |
| 26 | -func TestUpdateUserProfile_DoesNotOverwriteWithEmpty(t *testing.T) { | |
| 27 | - ctx := context.Background() | |
| 28 | - dbs := setupTestDB(t) | |
| 29 | - | |
| 30 | - _, err := dbs.DB().ExecContext(ctx, `INSERT INTO users (did, handle, display_name, avatar_url) VALUES (?, ?, ?, ?)`, | |
| 31 | - "did:test:profile2", "tester2", "Existing Name", "https://old.avatar/url") | |
| 32 | - assert.NilError(t, err) | |
| 33 | - | |
| 34 | - err = dbs.Users.UpdateUserProfile(ctx, "did:test:profile2", "", "") | |
| 35 | - assert.NilError(t, err) | |
| 36 | - | |
| 37 | - u, err := dbs.Users.GetUser(ctx, "did:test:profile2") | |
| 38 | - assert.NilError(t, err) | |
| 39 | - assert.Equal(t, u.DisplayName.String, "Existing Name") | |
| 40 | - assert.Equal(t, u.AvatarURL.String, "https://old.avatar/url") | |
| 20 | + assert.Equal(t, got.DID, "did:test:profile") | |
| 41 | 21 | } |
| @@ -7,35 +7,15 @@ import ( | |||
| 7 | "gotest.tools/v3/assert" | 7 | "gotest.tools/v3/assert" |
| 8 | ) | 8 | ) |
| 9 | 9 | ||
| 10 | -func TestUpdateUserProfile_SetsFields(t *testing.T) { | 10 | +func TestGetUser(t *testing.T) { |
| 11 | ctx := context.Background() | 11 | ctx := context.Background() |
| 12 | dbs := setupTestDB(t) | 12 | dbs := setupTestDB(t) |
| 13 | 13 | ||
| 14 | - _, err := dbs.DB().ExecContext(ctx, `INSERT INTO users (did, handle) VALUES (?, ?)`, "did:test:profile", "tester") | 14 | + u, err := dbs.Users.CreateUser(ctx, "did:test:profile") |
| 15 | assert.NilError(t, err) | 15 | assert.NilError(t, err) |
| 16 | + assert.Equal(t, u.DID, "did:test:profile") | ||
| 16 | 17 | ||
| 17 | - err = dbs.Users.UpdateUserProfile(ctx, "did:test:profile", "Display Name", "https://cdn.bsky.app/img/avatar.png") | 18 | + got, err := dbs.Users.GetUser(ctx, "did:test:profile") |
| 18 | assert.NilError(t, err) | 19 | assert.NilError(t, err) |
| 19 | - | 20 | + assert.Equal(t, got.DID, "did:test:profile") |
| 20 | - u, err := dbs.Users.GetUser(ctx, "did:test:profile") | ||
| 21 | - assert.NilError(t, err) | ||
| 22 | - assert.Equal(t, u.DisplayName.String, "Display Name") | ||
| 23 | - assert.Equal(t, u.AvatarURL.String, "https://cdn.bsky.app/img/avatar.png") | ||
| 24 | -} | ||
| 25 | - | ||
| 26 | -func TestUpdateUserProfile_DoesNotOverwriteWithEmpty(t *testing.T) { | ||
| 27 | - ctx := context.Background() | ||
| 28 | - dbs := setupTestDB(t) | ||
| 29 | - | ||
| 30 | - _, err := dbs.DB().ExecContext(ctx, `INSERT INTO users (did, handle, display_name, avatar_url) VALUES (?, ?, ?, ?)`, | ||
| 31 | - "did:test:profile2", "tester2", "Existing Name", "https://old.avatar/url") | ||
| 32 | - assert.NilError(t, err) | ||
| 33 | - | ||
| 34 | - err = dbs.Users.UpdateUserProfile(ctx, "did:test:profile2", "", "") | ||
| 35 | - assert.NilError(t, err) | ||
| 36 | - | ||
| 37 | - u, err := dbs.Users.GetUser(ctx, "did:test:profile2") | ||
| 38 | - assert.NilError(t, err) | ||
| 39 | - assert.Equal(t, u.DisplayName.String, "Existing Name") | ||
| 40 | - assert.Equal(t, u.AvatarURL.String, "https://old.avatar/url") | ||
| 41 | } | 21 | } |
modified
internal/server/annotations_handler.go +11 -1 | @@ -1,6 +1,7 @@ | ||
| 1 | 1 | package server |
| 2 | 2 | |
| 3 | 3 | import ( |
| 4 | + "context" | |
| 4 | 5 | "database/sql" |
| 5 | 6 | "fmt" |
| 6 | 7 | "net/http" |
| @@ -50,6 +51,7 @@ func (s *Server) handleLibrary(w http.ResponseWriter, r *http.Request) { | ||
| 50 | 51 | if err != nil { |
| 51 | 52 | s.logger.Warn("failed to list annotations", "error", err, "did", user.DID) |
| 52 | 53 | } |
| 54 | + resolveAnnotationHandles(ctx, annotations) | |
| 53 | 55 | annotHasMore := len(annotations) > limit |
| 54 | 56 | if annotHasMore { |
| 55 | 57 | annotations = annotations[:limit] |
| @@ -120,7 +122,7 @@ func (s *Server) handleCreateAnnotation(w http.ResponseWriter, r *http.Request) | ||
| 120 | 122 | return |
| 121 | 123 | } |
| 122 | 124 | |
| 123 | - a.AuthorHandle = user.Handle | |
| 125 | + a.AuthorHandle = atproto.ResolveProfile(r.Context(), user.DID).Handle | |
| 124 | 126 | s.render(w, r, "annotation-card.html", map[string]any{ |
| 125 | 127 | "annotation": a, |
| 126 | 128 | "userDID": user.DID, |
| @@ -165,3 +167,11 @@ func (s *Server) handleDeleteAnnotation(w http.ResponseWriter, r *http.Request) | ||
| 165 | 167 | |
| 166 | 168 | w.WriteHeader(http.StatusOK) |
| 167 | 169 | } |
| 170 | + | |
| 171 | +func resolveAnnotationHandles(ctx context.Context, annotations []*db.Annotation) { | |
| 172 | + for _, a := range annotations { | |
| 173 | + if a.AuthorDID != "" { | |
| 174 | + a.AuthorHandle = atproto.ResolveProfile(ctx, a.AuthorDID).Handle | |
| 175 | + } | |
| 176 | + } | |
| 177 | +} | |
| @@ -1,6 +1,7 @@ | |||
| 1 | package server | 1 | package server |
| 2 | 2 | ||
| 3 | import ( | 3 | import ( |
| 4 | + "context" | ||
| 4 | "database/sql" | 5 | "database/sql" |
| 5 | "fmt" | 6 | "fmt" |
| 6 | "net/http" | 7 | "net/http" |
| @@ -50,6 +51,7 @@ func (s *Server) handleLibrary(w http.ResponseWriter, r *http.Request) { | |||
| 50 | if err != nil { | 51 | if err != nil { |
| 51 | s.logger.Warn("failed to list annotations", "error", err, "did", user.DID) | 52 | s.logger.Warn("failed to list annotations", "error", err, "did", user.DID) |
| 52 | } | 53 | } |
| 54 | + resolveAnnotationHandles(ctx, annotations) | ||
| 53 | annotHasMore := len(annotations) > limit | 55 | annotHasMore := len(annotations) > limit |
| 54 | if annotHasMore { | 56 | if annotHasMore { |
| 55 | annotations = annotations[:limit] | 57 | annotations = annotations[:limit] |
| @@ -120,7 +122,7 @@ func (s *Server) handleCreateAnnotation(w http.ResponseWriter, r *http.Request) | |||
| 120 | return | 122 | return |
| 121 | } | 123 | } |
| 122 | 124 | ||
| 123 | - a.AuthorHandle = user.Handle | 125 | + a.AuthorHandle = atproto.ResolveProfile(r.Context(), user.DID).Handle |
| 124 | s.render(w, r, "annotation-card.html", map[string]any{ | 126 | s.render(w, r, "annotation-card.html", map[string]any{ |
| 125 | "annotation": a, | 127 | "annotation": a, |
| 126 | "userDID": user.DID, | 128 | "userDID": user.DID, |
| @@ -165,3 +167,11 @@ func (s *Server) handleDeleteAnnotation(w http.ResponseWriter, r *http.Request) | |||
| 165 | 167 | ||
| 166 | w.WriteHeader(http.StatusOK) | 168 | w.WriteHeader(http.StatusOK) |
| 167 | } | 169 | } |
| 170 | + | ||
| 171 | +func resolveAnnotationHandles(ctx context.Context, annotations []*db.Annotation) { | ||
| 172 | + for _, a := range annotations { | ||
| 173 | + if a.AuthorDID != "" { | ||
| 174 | + a.AuthorHandle = atproto.ResolveProfile(ctx, a.AuthorDID).Handle | ||
| 175 | + } | ||
| 176 | + } | ||
| 177 | +} | ||
modified
internal/server/auth_handler.go +3 -15 | @@ -33,7 +33,7 @@ func (s *Server) handleAuthStart(w http.ResponseWriter, r *http.Request) { | ||
| 33 | 33 | http.Error(w, "could not resolve handle", http.StatusInternalServerError) |
| 34 | 34 | return |
| 35 | 35 | } |
| 36 | - user, createErr := s.dbs.Users.CreateUser(r.Context(), did, handle, "", "") | |
| 36 | + user, createErr := s.dbs.Users.CreateUser(r.Context(), did) | |
| 37 | 37 | if createErr != nil { |
| 38 | 38 | http.Error(w, createErr.Error(), http.StatusInternalServerError) |
| 39 | 39 | return |
| @@ -67,7 +67,7 @@ func (s *Server) handleAuthCallback(w http.ResponseWriter, r *http.Request) { | ||
| 67 | 67 | return |
| 68 | 68 | } |
| 69 | 69 | |
| 70 | - user, err := s.dbs.Users.CreateUser(r.Context(), did, handle, "", "") | |
| 70 | + user, err := s.dbs.Users.CreateUser(r.Context(), did) | |
| 71 | 71 | if err != nil { |
| 72 | 72 | s.logger.Error("failed to create user", "error", err) |
| 73 | 73 | http.Error(w, err.Error(), http.StatusInternalServerError) |
| @@ -87,22 +87,10 @@ func (s *Server) handleOAuthCallback(w http.ResponseWriter, r *http.Request) { | ||
| 87 | 87 | } |
| 88 | 88 | |
| 89 | 89 | did := sessData.AccountDID.String() |
| 90 | - handle := did | |
| 91 | - if ident, err := s.oauth.Dir.LookupDID(r.Context(), sessData.AccountDID); err == nil { | |
| 92 | - handle = ident.Handle.String() | |
| 93 | - } | |
| 94 | 90 | |
| 95 | 91 | client := s.pdsClientFromSession(sessData) |
| 96 | 92 | |
| 97 | - var displayName, avatarURL string | |
| 98 | - if client != nil { | |
| 99 | - if dn, avatar, err := client.GetProfile(r.Context(), did); err == nil { | |
| 100 | - displayName = dn | |
| 101 | - avatarURL = avatar | |
| 102 | - } | |
| 103 | - } | |
| 104 | - | |
| 105 | - user, err := s.dbs.Users.CreateUser(r.Context(), did, handle, displayName, avatarURL) | |
| 93 | + user, err := s.dbs.Users.CreateUser(r.Context(), did) | |
| 106 | 94 | if err != nil { |
| 107 | 95 | s.logger.Error("failed to create user", "error", err) |
| 108 | 96 | http.Error(w, err.Error(), http.StatusInternalServerError) |
| @@ -33,7 +33,7 @@ func (s *Server) handleAuthStart(w http.ResponseWriter, r *http.Request) { | |||
| 33 | http.Error(w, "could not resolve handle", http.StatusInternalServerError) | 33 | http.Error(w, "could not resolve handle", http.StatusInternalServerError) |
| 34 | return | 34 | return |
| 35 | } | 35 | } |
| 36 | - user, createErr := s.dbs.Users.CreateUser(r.Context(), did, handle, "", "") | 36 | + user, createErr := s.dbs.Users.CreateUser(r.Context(), did) |
| 37 | if createErr != nil { | 37 | if createErr != nil { |
| 38 | http.Error(w, createErr.Error(), http.StatusInternalServerError) | 38 | http.Error(w, createErr.Error(), http.StatusInternalServerError) |
| 39 | return | 39 | return |
| @@ -67,7 +67,7 @@ func (s *Server) handleAuthCallback(w http.ResponseWriter, r *http.Request) { | |||
| 67 | return | 67 | return |
| 68 | } | 68 | } |
| 69 | 69 | ||
| 70 | - user, err := s.dbs.Users.CreateUser(r.Context(), did, handle, "", "") | 70 | + user, err := s.dbs.Users.CreateUser(r.Context(), did) |
| 71 | if err != nil { | 71 | if err != nil { |
| 72 | s.logger.Error("failed to create user", "error", err) | 72 | s.logger.Error("failed to create user", "error", err) |
| 73 | http.Error(w, err.Error(), http.StatusInternalServerError) | 73 | http.Error(w, err.Error(), http.StatusInternalServerError) |
| @@ -87,22 +87,10 @@ func (s *Server) handleOAuthCallback(w http.ResponseWriter, r *http.Request) { | |||
| 87 | } | 87 | } |
| 88 | 88 | ||
| 89 | did := sessData.AccountDID.String() | 89 | did := sessData.AccountDID.String() |
| 90 | - handle := did | ||
| 91 | - if ident, err := s.oauth.Dir.LookupDID(r.Context(), sessData.AccountDID); err == nil { | ||
| 92 | - handle = ident.Handle.String() | ||
| 93 | - } | ||
| 94 | 90 | ||
| 95 | client := s.pdsClientFromSession(sessData) | 91 | client := s.pdsClientFromSession(sessData) |
| 96 | 92 | ||
| 97 | - var displayName, avatarURL string | 93 | + user, err := s.dbs.Users.CreateUser(r.Context(), did) |
| 98 | - if client != nil { | ||
| 99 | - if dn, avatar, err := client.GetProfile(r.Context(), did); err == nil { | ||
| 100 | - displayName = dn | ||
| 101 | - avatarURL = avatar | ||
| 102 | - } | ||
| 103 | - } | ||
| 104 | - | ||
| 105 | - user, err := s.dbs.Users.CreateUser(r.Context(), did, handle, displayName, avatarURL) | ||
| 106 | if err != nil { | 94 | if err != nil { |
| 107 | s.logger.Error("failed to create user", "error", err) | 95 | s.logger.Error("failed to create user", "error", err) |
| 108 | http.Error(w, err.Error(), http.StatusInternalServerError) | 96 | http.Error(w, err.Error(), http.StatusInternalServerError) |
modified
internal/server/dashboard_handler.go +12 -0 | @@ -1,9 +1,11 @@ | ||
| 1 | 1 | package server |
| 2 | 2 | |
| 3 | 3 | import ( |
| 4 | + "context" | |
| 4 | 5 | "net/http" |
| 5 | 6 | "time" |
| 6 | 7 | |
| 8 | + "pkg.rbrt.fr/glean/internal/atproto" | |
| 7 | 9 | "pkg.rbrt.fr/glean/internal/cluster" |
| 8 | 10 | ) |
| 9 | 11 | |
| @@ -41,6 +43,7 @@ func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) { | ||
| 41 | 43 | if err != nil { |
| 42 | 44 | s.logger.Warn("failed to get people recommendations", "error", err, "did", user.DID) |
| 43 | 45 | } |
| 46 | + resolvePeopleHandles(ctx, peopleRecs) | |
| 44 | 47 | |
| 45 | 48 | feedRecs, err := s.engine.GetFeedRecommendations(ctx, user.DID, 5) |
| 46 | 49 | if err != nil { |
| @@ -110,3 +113,12 @@ func (s *Server) handleDismissArticleRecommendation(w http.ResponseWriter, r *ht | ||
| 110 | 113 | |
| 111 | 114 | w.WriteHeader(http.StatusOK) |
| 112 | 115 | } |
| 116 | + | |
| 117 | +func resolvePeopleHandles(ctx context.Context, people []*cluster.PersonRecommendation) { | |
| 118 | + for _, p := range people { | |
| 119 | + prof := atproto.ResolveProfile(ctx, p.DID) | |
| 120 | + p.Handle = prof.Handle | |
| 121 | + p.DisplayName = prof.DisplayName | |
| 122 | + p.AvatarURL = prof.AvatarURL | |
| 123 | + } | |
| 124 | +} | |
| @@ -1,9 +1,11 @@ | |||
| 1 | package server | 1 | package server |
| 2 | 2 | ||
| 3 | import ( | 3 | import ( |
| 4 | + "context" | ||
| 4 | "net/http" | 5 | "net/http" |
| 5 | "time" | 6 | "time" |
| 6 | 7 | ||
| 8 | + "pkg.rbrt.fr/glean/internal/atproto" | ||
| 7 | "pkg.rbrt.fr/glean/internal/cluster" | 9 | "pkg.rbrt.fr/glean/internal/cluster" |
| 8 | ) | 10 | ) |
| 9 | 11 | ||
| @@ -41,6 +43,7 @@ func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) { | |||
| 41 | if err != nil { | 43 | if err != nil { |
| 42 | s.logger.Warn("failed to get people recommendations", "error", err, "did", user.DID) | 44 | s.logger.Warn("failed to get people recommendations", "error", err, "did", user.DID) |
| 43 | } | 45 | } |
| 46 | + resolvePeopleHandles(ctx, peopleRecs) | ||
| 44 | 47 | ||
| 45 | feedRecs, err := s.engine.GetFeedRecommendations(ctx, user.DID, 5) | 48 | feedRecs, err := s.engine.GetFeedRecommendations(ctx, user.DID, 5) |
| 46 | if err != nil { | 49 | if err != nil { |
| @@ -110,3 +113,12 @@ func (s *Server) handleDismissArticleRecommendation(w http.ResponseWriter, r *ht | |||
| 110 | 113 | ||
| 111 | w.WriteHeader(http.StatusOK) | 114 | w.WriteHeader(http.StatusOK) |
| 112 | } | 115 | } |
| 116 | + | ||
| 117 | +func resolvePeopleHandles(ctx context.Context, people []*cluster.PersonRecommendation) { | ||
| 118 | + for _, p := range people { | ||
| 119 | + prof := atproto.ResolveProfile(ctx, p.DID) | ||
| 120 | + p.Handle = prof.Handle | ||
| 121 | + p.DisplayName = prof.DisplayName | ||
| 122 | + p.AvatarURL = prof.AvatarURL | ||
| 123 | + } | ||
| 124 | +} | ||
modified
internal/server/feeds_handler.go +1 -0 | @@ -43,6 +43,7 @@ func (s *Server) handleFeeds(w http.ResponseWriter, r *http.Request) { | ||
| 43 | 43 | if err != nil { |
| 44 | 44 | s.logger.Warn("failed to get people recommendations", "error", err, "did", user.DID) |
| 45 | 45 | } |
| 46 | + resolvePeopleHandles(ctx, peopleRecs) | |
| 46 | 47 | |
| 47 | 48 | if len(feedRecs) > 0 { |
| 48 | 49 | impressions := make([]cluster.Impression, len(feedRecs)) |
| @@ -43,6 +43,7 @@ func (s *Server) handleFeeds(w http.ResponseWriter, r *http.Request) { | |||
| 43 | if err != nil { | 43 | if err != nil { |
| 44 | s.logger.Warn("failed to get people recommendations", "error", err, "did", user.DID) | 44 | s.logger.Warn("failed to get people recommendations", "error", err, "did", user.DID) |
| 45 | } | 45 | } |
| 46 | + resolvePeopleHandles(ctx, peopleRecs) | ||
| 46 | 47 | ||
| 47 | if len(feedRecs) > 0 { | 48 | if len(feedRecs) > 0 { |
| 48 | impressions := make([]cluster.Impression, len(feedRecs)) | 49 | impressions := make([]cluster.Impression, len(feedRecs)) |
modified
internal/server/profile_handler.go +11 -21 | @@ -17,18 +17,13 @@ func (s *Server) handleProfile(w http.ResponseWriter, r *http.Request) { | ||
| 17 | 17 | if strings.HasPrefix(param, "did:") { |
| 18 | 18 | did = param |
| 19 | 19 | } else { |
| 20 | - profileUser, err := s.dbs.Users.GetUserByHandle(ctx, param) | |
| 21 | - if err == nil { | |
| 22 | - did = profileUser.DID | |
| 23 | - } else { | |
| 24 | - resolved, err := atproto.ResolveHandle(ctx, param) | |
| 25 | - if err != nil { | |
| 26 | - s.logger.Warn("failed to resolve handle", "error", err, "handle", param) | |
| 27 | - http.Error(w, "handle not found", http.StatusNotFound) | |
| 28 | - return | |
| 29 | - } | |
| 30 | - did = resolved | |
| 20 | + resolved, err := atproto.ResolveHandle(ctx, param) | |
| 21 | + if err != nil { | |
| 22 | + s.logger.Warn("failed to resolve handle", "error", err, "handle", param) | |
| 23 | + http.Error(w, "handle not found", http.StatusNotFound) | |
| 24 | + return | |
| 31 | 25 | } |
| 26 | + did = resolved | |
| 32 | 27 | } |
| 33 | 28 | |
| 34 | 29 | profileUser, err := s.dbs.Users.GetUser(ctx, did) |
| @@ -38,16 +33,10 @@ func (s *Server) handleProfile(w http.ResponseWriter, r *http.Request) { | ||
| 38 | 33 | return |
| 39 | 34 | } |
| 40 | 35 | |
| 41 | - if !profileUser.AvatarURL.Valid || profileUser.AvatarURL.String == "" { | |
| 42 | - _, displayName, avatarURL, err := atproto.FetchProfile(ctx, did) | |
| 43 | - if err == nil && avatarURL != "" { | |
| 44 | - if err := s.dbs.Users.UpdateUserProfile(ctx, did, displayName, avatarURL); err != nil { | |
| 45 | - s.logger.Warn("failed to update user profile", "error", err, "did", did) | |
| 46 | - } | |
| 47 | - profileUser.DisplayName = nullString(displayName) | |
| 48 | - profileUser.AvatarURL = nullString(avatarURL) | |
| 49 | - } | |
| 50 | - } | |
| 36 | + p := atproto.ResolveProfile(ctx, did) | |
| 37 | + profileUser.Handle = p.Handle | |
| 38 | + profileUser.DisplayName = p.DisplayName | |
| 39 | + profileUser.AvatarURL = p.AvatarURL | |
| 51 | 40 | |
| 52 | 41 | subs, err := s.dbs.Articles.ListSubscriptions(ctx, did, "", 50, 0) |
| 53 | 42 | if err != nil { |
| @@ -58,6 +47,7 @@ func (s *Server) handleProfile(w http.ResponseWriter, r *http.Request) { | ||
| 58 | 47 | if err != nil { |
| 59 | 48 | s.logger.Warn("failed to list annotations", "error", err, "did", did) |
| 60 | 49 | } |
| 50 | + resolveAnnotationHandles(ctx, annotations) | |
| 61 | 51 | |
| 62 | 52 | subCount, err := s.dbs.Articles.GetSubscriptionCount(ctx, did) |
| 63 | 53 | if err != nil { |
| @@ -17,18 +17,13 @@ func (s *Server) handleProfile(w http.ResponseWriter, r *http.Request) { | |||
| 17 | if strings.HasPrefix(param, "did:") { | 17 | if strings.HasPrefix(param, "did:") { |
| 18 | did = param | 18 | did = param |
| 19 | } else { | 19 | } else { |
| 20 | - profileUser, err := s.dbs.Users.GetUserByHandle(ctx, param) | 20 | + resolved, err := atproto.ResolveHandle(ctx, param) |
| 21 | - if err == nil { | 21 | + if err != nil { |
| 22 | - did = profileUser.DID | 22 | + s.logger.Warn("failed to resolve handle", "error", err, "handle", param) |
| 23 | - } else { | 23 | + http.Error(w, "handle not found", http.StatusNotFound) |
| 24 | - resolved, err := atproto.ResolveHandle(ctx, param) | 24 | + return |
| 25 | - if err != nil { | ||
| 26 | - s.logger.Warn("failed to resolve handle", "error", err, "handle", param) | ||
| 27 | - http.Error(w, "handle not found", http.StatusNotFound) | ||
| 28 | - return | ||
| 29 | - } | ||
| 30 | - did = resolved | ||
| 31 | } | 25 | } |
| 26 | + did = resolved | ||
| 32 | } | 27 | } |
| 33 | 28 | ||
| 34 | profileUser, err := s.dbs.Users.GetUser(ctx, did) | 29 | profileUser, err := s.dbs.Users.GetUser(ctx, did) |
| @@ -38,16 +33,10 @@ func (s *Server) handleProfile(w http.ResponseWriter, r *http.Request) { | |||
| 38 | return | 33 | return |
| 39 | } | 34 | } |
| 40 | 35 | ||
| 41 | - if !profileUser.AvatarURL.Valid || profileUser.AvatarURL.String == "" { | 36 | + p := atproto.ResolveProfile(ctx, did) |
| 42 | - _, displayName, avatarURL, err := atproto.FetchProfile(ctx, did) | 37 | + profileUser.Handle = p.Handle |
| 43 | - if err == nil && avatarURL != "" { | 38 | + profileUser.DisplayName = p.DisplayName |
| 44 | - if err := s.dbs.Users.UpdateUserProfile(ctx, did, displayName, avatarURL); err != nil { | 39 | + profileUser.AvatarURL = p.AvatarURL |
| 45 | - s.logger.Warn("failed to update user profile", "error", err, "did", did) | ||
| 46 | - } | ||
| 47 | - profileUser.DisplayName = nullString(displayName) | ||
| 48 | - profileUser.AvatarURL = nullString(avatarURL) | ||
| 49 | - } | ||
| 50 | - } | ||
| 51 | 40 | ||
| 52 | subs, err := s.dbs.Articles.ListSubscriptions(ctx, did, "", 50, 0) | 41 | subs, err := s.dbs.Articles.ListSubscriptions(ctx, did, "", 50, 0) |
| 53 | if err != nil { | 42 | if err != nil { |
| @@ -58,6 +47,7 @@ func (s *Server) handleProfile(w http.ResponseWriter, r *http.Request) { | |||
| 58 | if err != nil { | 47 | if err != nil { |
| 59 | s.logger.Warn("failed to list annotations", "error", err, "did", did) | 48 | s.logger.Warn("failed to list annotations", "error", err, "did", did) |
| 60 | } | 49 | } |
| 50 | + resolveAnnotationHandles(ctx, annotations) | ||
| 61 | 51 | ||
| 62 | subCount, err := s.dbs.Articles.GetSubscriptionCount(ctx, did) | 52 | subCount, err := s.dbs.Articles.GetSubscriptionCount(ctx, did) |
| 63 | if err != nil { | 53 | if err != nil { |
modified
internal/server/server.go +1 -26 | @@ -12,8 +12,6 @@ import ( | ||
| 12 | 12 | "strconv" |
| 13 | 13 | "strings" |
| 14 | 14 | "sync" |
| 15 | - | |
| 16 | - "golang.org/x/sync/errgroup" | |
| 17 | 15 | "time" |
| 18 | 16 | |
| 19 | 17 | "github.com/go-chi/chi/v5" |
| @@ -490,26 +488,7 @@ func (s *Server) BackfillFromCollectionDir(ctx context.Context, collectionDirURL | ||
| 490 | 488 | defer func() { <-sem }() |
| 491 | 489 | defer wg.Done() |
| 492 | 490 | |
| 493 | - g, gCtx := errgroup.WithContext(ctx) | |
| 494 | - | |
| 495 | - var handle, displayName, avatarURL string | |
| 496 | - g.Go(func() error { | |
| 497 | - if ident, err := atproto.ResolveIdentity(gCtx, did); err == nil { | |
| 498 | - handle = ident.Handle.String() | |
| 499 | - } | |
| 500 | - return nil | |
| 501 | - }) | |
| 502 | - g.Go(func() error { | |
| 503 | - if h, dn, avatar, err := atproto.FetchProfile(gCtx, did); err == nil { | |
| 504 | - handle = h | |
| 505 | - displayName = dn | |
| 506 | - avatarURL = avatar | |
| 507 | - } | |
| 508 | - return nil | |
| 509 | - }) | |
| 510 | - _ = g.Wait() | |
| 511 | - | |
| 512 | - if _, err := s.dbs.Users.CreateUser(ctx, did, handle, displayName, avatarURL); err != nil { | |
| 491 | + if _, err := s.dbs.Users.CreateUser(ctx, did); err != nil { | |
| 513 | 492 | s.logger.Error("failed to create user during backfill", "error", err, "did", did) |
| 514 | 493 | return |
| 515 | 494 | } |
| @@ -563,10 +542,6 @@ func (s *Server) runSyncAll(ctx context.Context) { | ||
| 563 | 542 | s.logger.Error("periodic sync failed", "error", err, "did", u.DID) |
| 564 | 543 | } |
| 565 | 544 | |
| 566 | - if dn, avatar, err := client.GetProfile(ctx, u.DID); err == nil { | |
| 567 | - _ = s.dbs.Users.UpdateUserProfile(ctx, u.DID, dn, avatar) | |
| 568 | - } | |
| 569 | - | |
| 570 | 545 | metrics.SyncRuns.Inc() |
| 571 | 546 | } |
| 572 | 547 | } |
| @@ -12,8 +12,6 @@ import ( | |||
| 12 | "strconv" | 12 | "strconv" |
| 13 | "strings" | 13 | "strings" |
| 14 | "sync" | 14 | "sync" |
| 15 | - | ||
| 16 | - "golang.org/x/sync/errgroup" | ||
| 17 | "time" | 15 | "time" |
| 18 | 16 | ||
| 19 | "github.com/go-chi/chi/v5" | 17 | "github.com/go-chi/chi/v5" |
| @@ -490,26 +488,7 @@ func (s *Server) BackfillFromCollectionDir(ctx context.Context, collectionDirURL | |||
| 490 | defer func() { <-sem }() | 488 | defer func() { <-sem }() |
| 491 | defer wg.Done() | 489 | defer wg.Done() |
| 492 | 490 | ||
| 493 | - g, gCtx := errgroup.WithContext(ctx) | 491 | + if _, err := s.dbs.Users.CreateUser(ctx, did); err != nil { |
| 494 | - | ||
| 495 | - var handle, displayName, avatarURL string | ||
| 496 | - g.Go(func() error { | ||
| 497 | - if ident, err := atproto.ResolveIdentity(gCtx, did); err == nil { | ||
| 498 | - handle = ident.Handle.String() | ||
| 499 | - } | ||
| 500 | - return nil | ||
| 501 | - }) | ||
| 502 | - g.Go(func() error { | ||
| 503 | - if h, dn, avatar, err := atproto.FetchProfile(gCtx, did); err == nil { | ||
| 504 | - handle = h | ||
| 505 | - displayName = dn | ||
| 506 | - avatarURL = avatar | ||
| 507 | - } | ||
| 508 | - return nil | ||
| 509 | - }) | ||
| 510 | - _ = g.Wait() | ||
| 511 | - | ||
| 512 | - if _, err := s.dbs.Users.CreateUser(ctx, did, handle, displayName, avatarURL); err != nil { | ||
| 513 | s.logger.Error("failed to create user during backfill", "error", err, "did", did) | 492 | s.logger.Error("failed to create user during backfill", "error", err, "did", did) |
| 514 | return | 493 | return |
| 515 | } | 494 | } |
| @@ -563,10 +542,6 @@ func (s *Server) runSyncAll(ctx context.Context) { | |||
| 563 | s.logger.Error("periodic sync failed", "error", err, "did", u.DID) | 542 | s.logger.Error("periodic sync failed", "error", err, "did", u.DID) |
| 564 | } | 543 | } |
| 565 | 544 | ||
| 566 | - if dn, avatar, err := client.GetProfile(ctx, u.DID); err == nil { | ||
| 567 | - _ = s.dbs.Users.UpdateUserProfile(ctx, u.DID, dn, avatar) | ||
| 568 | - } | ||
| 569 | - | ||
| 570 | metrics.SyncRuns.Inc() | 545 | metrics.SyncRuns.Inc() |
| 571 | } | 546 | } |
| 572 | } | 547 | } |
modified
internal/server/session.go +5 -0 | @@ -9,6 +9,7 @@ import ( | ||
| 9 | 9 | "net/http" |
| 10 | 10 | "os" |
| 11 | 11 | |
| 12 | + "pkg.rbrt.fr/glean/internal/atproto" | |
| 12 | 13 | "pkg.rbrt.fr/glean/internal/db" |
| 13 | 14 | ) |
| 14 | 15 | |
| @@ -39,6 +40,10 @@ func (s *Server) getUserFromSession(r *http.Request) *db.User { | ||
| 39 | 40 | return nil |
| 40 | 41 | } |
| 41 | 42 | |
| 43 | + p := atproto.ResolveProfile(r.Context(), user.DID) | |
| 44 | + user.Handle = p.Handle | |
| 45 | + user.DisplayName = p.DisplayName | |
| 46 | + user.AvatarURL = p.AvatarURL | |
| 42 | 47 | return user |
| 43 | 48 | } |
| 44 | 49 | |
| @@ -9,6 +9,7 @@ import ( | |||
| 9 | "net/http" | 9 | "net/http" |
| 10 | "os" | 10 | "os" |
| 11 | 11 | ||
| 12 | + "pkg.rbrt.fr/glean/internal/atproto" | ||
| 12 | "pkg.rbrt.fr/glean/internal/db" | 13 | "pkg.rbrt.fr/glean/internal/db" |
| 13 | ) | 14 | ) |
| 14 | 15 | ||
| @@ -39,6 +40,10 @@ func (s *Server) getUserFromSession(r *http.Request) *db.User { | |||
| 39 | return nil | 40 | return nil |
| 40 | } | 41 | } |
| 41 | 42 | ||
| 43 | + p := atproto.ResolveProfile(r.Context(), user.DID) | ||
| 44 | + user.Handle = p.Handle | ||
| 45 | + user.DisplayName = p.DisplayName | ||
| 46 | + user.AvatarURL = p.AvatarURL | ||
| 42 | return user | 47 | return user |
| 43 | } | 48 | } |
| 44 | 49 | ||
modified
internal/tmpl/base.html +2 -2 | @@ -96,7 +96,7 @@ | ||
| 96 | 96 | {{if .User}} |
| 97 | 97 | <div class="flex items-center gap-2"> |
| 98 | 98 | <a href="/profile/{{.User.DID}}" class="flex items-center gap-2 flex-1 min-w-0 px-2 py-1.5 rounded-md hover:bg-spot-hover-50 transition"> |
| 99 | - {{if .User.AvatarURL.Valid}}<img src="{{.User.AvatarURL.String}}" class="w-7 h-7 rounded-full shrink-0">{{end}} | |
| 99 | + {{if .User.AvatarURL}}<img src="{{.User.AvatarURL}}" class="w-7 h-7 rounded-full shrink-0">{{end}} | |
| 100 | 100 | <span class="text-sm font-medium truncate text-spot-text">@{{.User.Handle}}</span> |
| 101 | 101 | </a> |
| 102 | 102 | <form method="POST" action="/auth/logout" class="shrink-0"> |
| @@ -142,7 +142,7 @@ | ||
| 142 | 142 | {{template "logo-text"}} |
| 143 | 143 | {{if .User}} |
| 144 | 144 | <a href="/profile/{{.User.DID}}" class="flex items-center gap-2"> |
| 145 | - {{if .User.AvatarURL.Valid}}<img src="{{.User.AvatarURL.String}}" class="w-7 h-7 rounded-full">{{end}} | |
| 145 | + {{if .User.AvatarURL}}<img src="{{.User.AvatarURL}}" class="w-7 h-7 rounded-full">{{end}} | |
| 146 | 146 | <span class="text-xs text-spot-secondary">@{{.User.Handle}}</span> |
| 147 | 147 | </a> |
| 148 | 148 | {{else}} |
| @@ -96,7 +96,7 @@ | |||
| 96 | {{if .User}} | 96 | {{if .User}} |
| 97 | <div class="flex items-center gap-2"> | 97 | <div class="flex items-center gap-2"> |
| 98 | <a href="/profile/{{.User.DID}}" class="flex items-center gap-2 flex-1 min-w-0 px-2 py-1.5 rounded-md hover:bg-spot-hover-50 transition"> | 98 | <a href="/profile/{{.User.DID}}" class="flex items-center gap-2 flex-1 min-w-0 px-2 py-1.5 rounded-md hover:bg-spot-hover-50 transition"> |
| 99 | - {{if .User.AvatarURL.Valid}}<img src="{{.User.AvatarURL.String}}" class="w-7 h-7 rounded-full shrink-0">{{end}} | 99 | + {{if .User.AvatarURL}}<img src="{{.User.AvatarURL}}" class="w-7 h-7 rounded-full shrink-0">{{end}} |
| 100 | <span class="text-sm font-medium truncate text-spot-text">@{{.User.Handle}}</span> | 100 | <span class="text-sm font-medium truncate text-spot-text">@{{.User.Handle}}</span> |
| 101 | </a> | 101 | </a> |
| 102 | <form method="POST" action="/auth/logout" class="shrink-0"> | 102 | <form method="POST" action="/auth/logout" class="shrink-0"> |
| @@ -142,7 +142,7 @@ | |||
| 142 | {{template "logo-text"}} | 142 | {{template "logo-text"}} |
| 143 | {{if .User}} | 143 | {{if .User}} |
| 144 | <a href="/profile/{{.User.DID}}" class="flex items-center gap-2"> | 144 | <a href="/profile/{{.User.DID}}" class="flex items-center gap-2"> |
| 145 | - {{if .User.AvatarURL.Valid}}<img src="{{.User.AvatarURL.String}}" class="w-7 h-7 rounded-full">{{end}} | 145 | + {{if .User.AvatarURL}}<img src="{{.User.AvatarURL}}" class="w-7 h-7 rounded-full">{{end}} |
| 146 | <span class="text-xs text-spot-secondary">@{{.User.Handle}}</span> | 146 | <span class="text-xs text-spot-secondary">@{{.User.Handle}}</span> |
| 147 | </a> | 147 | </a> |
| 148 | {{else}} | 148 | {{else}} |
modified
internal/tmpl/partials/profile-card.html +1 -1 | @@ -2,7 +2,7 @@ | ||
| 2 | 2 | <div class="bg-spot-surface rounded-xl p-4 flex items-center gap-3 hover:bg-spot-hover-50 transition"> |
| 3 | 3 | {{if .AvatarURL}}<img src="{{.AvatarURL}}" class="w-10 h-10 rounded-full">{{end}} |
| 4 | 4 | <div class="min-w-0 flex-1"> |
| 5 | - <a href="/profile/{{.Handle}}" class="font-bold text-spot-text hover:text-spot-green transition">@{{.Handle}}</a> | |
| 5 | + <a href="/profile/{{.DID}}" class="font-bold text-spot-text hover:text-spot-green transition">@{{.Handle}}</a> | |
| 6 | 6 | {{if .DisplayName}}<div class="text-sm text-spot-secondary">{{.DisplayName}}</div>{{end}} |
| 7 | 7 | </div> |
| 8 | 8 | <span class="text-xs text-spot-secondary">{{.CommonFeeds}} shared</span> |
| @@ -2,7 +2,7 @@ | |||
| 2 | <div class="bg-spot-surface rounded-xl p-4 flex items-center gap-3 hover:bg-spot-hover-50 transition"> | 2 | <div class="bg-spot-surface rounded-xl p-4 flex items-center gap-3 hover:bg-spot-hover-50 transition"> |
| 3 | {{if .AvatarURL}}<img src="{{.AvatarURL}}" class="w-10 h-10 rounded-full">{{end}} | 3 | {{if .AvatarURL}}<img src="{{.AvatarURL}}" class="w-10 h-10 rounded-full">{{end}} |
| 4 | <div class="min-w-0 flex-1"> | 4 | <div class="min-w-0 flex-1"> |
| 5 | - <a href="/profile/{{.Handle}}" class="font-bold text-spot-text hover:text-spot-green transition">@{{.Handle}}</a> | 5 | + <a href="/profile/{{.DID}}" class="font-bold text-spot-text hover:text-spot-green transition">@{{.Handle}}</a> |
| 6 | {{if .DisplayName}}<div class="text-sm text-spot-secondary">{{.DisplayName}}</div>{{end}} | 6 | {{if .DisplayName}}<div class="text-sm text-spot-secondary">{{.DisplayName}}</div>{{end}} |
| 7 | </div> | 7 | </div> |
| 8 | <span class="text-xs text-spot-secondary">{{.CommonFeeds}} shared</span> | 8 | <span class="text-xs text-spot-secondary">{{.CommonFeeds}} shared</span> |
modified
internal/tmpl/profile.html +2 -2 | @@ -2,10 +2,10 @@ | ||
| 2 | 2 | <div class="max-w-2xl mx-auto"> |
| 3 | 3 | <div class="bg-spot-surface rounded-xl p-6 mb-6"> |
| 4 | 4 | <div class="flex items-center gap-4"> |
| 5 | - {{if .ProfileUser.AvatarURL.Valid}}<img src="{{.ProfileUser.AvatarURL.String}}" class="w-20 h-20 rounded-full ring-2 ring-spot-divider">{{else}}<div class="w-20 h-20 rounded-full bg-spot-hover flex items-center justify-center text-spot-muted"><svg class="w-10 h-10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="8" r="4"/><path d="M4 20c0-4 4-7 8-7s8 3 8 7"/></svg></div>{{end}} | |
| 5 | + {{if .ProfileUser.AvatarURL}}<img src="{{.ProfileUser.AvatarURL}}" class="w-20 h-20 rounded-full ring-2 ring-spot-divider">{{else}}<div class="w-20 h-20 rounded-full bg-spot-hover flex items-center justify-center text-spot-muted"><svg class="w-10 h-10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="8" r="4"/><path d="M4 20c0-4 4-7 8-7s8 3 8 7"/></svg></div>{{end}} | |
| 6 | 6 | <div class="min-w-0 flex-1"> |
| 7 | 7 | <h1 class="text-2xl font-bold text-spot-text truncate"> |
| 8 | - {{if .ProfileUser.DisplayName.Valid}}{{.ProfileUser.DisplayName.String}}{{end}} | |
| 8 | + {{if .ProfileUser.DisplayName}}{{.ProfileUser.DisplayName}}{{end}} | |
| 9 | 9 | </h1> |
| 10 | 10 | <p class="flex items-center gap-1.5 text-spot-secondary">@{{.ProfileUser.Handle}}<a href="https://bsky.app/profile/{{.ProfileUser.Handle}}" target="_blank" rel="noopener" class="text-spot-secondary hover:text-spot-green transition"><svg class="w-4 h-4" viewBox="0 0 24 24" fill="currentColor">{{template "icon-bluesky"}}</svg></a></p> |
| 11 | 11 | <div class="flex gap-4 mt-3"> |
| @@ -2,10 +2,10 @@ | |||
| 2 | <div class="max-w-2xl mx-auto"> | 2 | <div class="max-w-2xl mx-auto"> |
| 3 | <div class="bg-spot-surface rounded-xl p-6 mb-6"> | 3 | <div class="bg-spot-surface rounded-xl p-6 mb-6"> |
| 4 | <div class="flex items-center gap-4"> | 4 | <div class="flex items-center gap-4"> |
| 5 | - {{if .ProfileUser.AvatarURL.Valid}}<img src="{{.ProfileUser.AvatarURL.String}}" class="w-20 h-20 rounded-full ring-2 ring-spot-divider">{{else}}<div class="w-20 h-20 rounded-full bg-spot-hover flex items-center justify-center text-spot-muted"><svg class="w-10 h-10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="8" r="4"/><path d="M4 20c0-4 4-7 8-7s8 3 8 7"/></svg></div>{{end}} | 5 | + {{if .ProfileUser.AvatarURL}}<img src="{{.ProfileUser.AvatarURL}}" class="w-20 h-20 rounded-full ring-2 ring-spot-divider">{{else}}<div class="w-20 h-20 rounded-full bg-spot-hover flex items-center justify-center text-spot-muted"><svg class="w-10 h-10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="8" r="4"/><path d="M4 20c0-4 4-7 8-7s8 3 8 7"/></svg></div>{{end}} |
| 6 | <div class="min-w-0 flex-1"> | 6 | <div class="min-w-0 flex-1"> |
| 7 | <h1 class="text-2xl font-bold text-spot-text truncate"> | 7 | <h1 class="text-2xl font-bold text-spot-text truncate"> |
| 8 | - {{if .ProfileUser.DisplayName.Valid}}{{.ProfileUser.DisplayName.String}}{{end}} | 8 | + {{if .ProfileUser.DisplayName}}{{.ProfileUser.DisplayName}}{{end}} |
| 9 | </h1> | 9 | </h1> |
| 10 | <p class="flex items-center gap-1.5 text-spot-secondary">@{{.ProfileUser.Handle}}<a href="https://bsky.app/profile/{{.ProfileUser.Handle}}" target="_blank" rel="noopener" class="text-spot-secondary hover:text-spot-green transition"><svg class="w-4 h-4" viewBox="0 0 24 24" fill="currentColor">{{template "icon-bluesky"}}</svg></a></p> | 10 | <p class="flex items-center gap-1.5 text-spot-secondary">@{{.ProfileUser.Handle}}<a href="https://bsky.app/profile/{{.ProfileUser.Handle}}" target="_blank" rel="noopener" class="text-spot-secondary hover:text-spot-green transition"><svg class="w-4 h-4" viewBox="0 0 24 24" fill="currentColor">{{template "icon-bluesky"}}</svg></a></p> |
| 11 | <div class="flex gap-4 mt-3"> | 11 | <div class="flex gap-4 mt-3"> |