nandi/gleanpublic Fork 0
8c01260
Commits
Clone
git clone https://git.rickub.com/nandi/glean.git
git clone ssh://git@rickub.com/nandi/glean.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Simplify user tableUnverified

Julien Robert committed 2026-04-23T20:02:42+02:00 Browse files
8c01260 parent: c8ebf4d
modified internal/atproto/auth.go +35 -6
@@ -10,6 +10,7 @@ import (
1010
1111 "github.com/bluesky-social/indigo/atproto/identity"
1212 "github.com/bluesky-social/indigo/atproto/syntax"
13+ "pkg.rbrt.fr/glean/internal/httpclient"
1314 )
1415
1516 type (
@@ -26,12 +27,8 @@ func InitIdentity(plcURL string) {
2627 base := identity.BaseDirectory{
2728 PLCURL: plcURL,
2829 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(),
3532 },
3633 Resolver: net.Resolver{
3734 Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
@@ -43,6 +40,7 @@ func InitIdentity(plcURL string) {
4340 SkipDNSDomainSuffixes: []string{".bsky.social"},
4441 UserAgent: "glean/1.0",
4542 }
43+
4644 directory = identity.NewCacheDirectory(&base, 250_000, 24*time.Hour, 2*time.Minute, 5*time.Minute)
4745 }
4846
@@ -113,3 +111,34 @@ func ResolvePDSEndpoint(ctx context.Context, did string) (string, error) {
113111
114112 return pds, nil
115113 }
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, nil112 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 (
1212 "encoding/json"
1313 "fmt"
1414 "log/slog"
15+ "maps"
16+ "slices"
1517 "time"
1618
17- "golang.org/x/sync/errgroup"
18-
1919 "pkg.rbrt.fr/glean/internal/db"
2020 )
2121
@@ -266,30 +266,9 @@ func (s *Sync) syncFollows(ctx context.Context, userDID string) error {
266266 return nil
267267 }
268268
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 {
292270 return fmt.Errorf("batch create users: %w", err)
293271 }
272+
294273 return s.users.SyncFollows(ctx, userDID, activeFollows)
295274 }
@@ -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 nil266 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) {
9393 cursor := r.URL.Query().Get("cursor")
9494
9595 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,
9797 a.quote, a.note, a.tags, a.rating, a.created_at
9898 FROM annotations a
9999 JOIN users u ON a.author_did = u.did
@@ -129,10 +129,10 @@ func (h *XRPCHandler) ListAnnotations(w http.ResponseWriter, r *http.Request) {
129129
130130 annotations := make([]AnnotationView, 0)
131131 for rows.Next() {
132- var uri, did, handle, fURL, artURL, createdAt string
132+ var uri, did, fURL, artURL, createdAt string
133133 var cid, quote, note, tags sql.NullString
134134 var rating sql.NullInt64
135- if err := rows.Scan(&uri, &cid, &did, &handle, &fURL, &artURL, &quote, &note, &tags, &rating, &createdAt); err != nil {
135+ if err := rows.Scan(&uri, &cid, &did, &fURL, &artURL, &quote, &note, &tags, &rating, &createdAt); err != nil {
136136 http.Error(w, err.Error(), http.StatusInternalServerError)
137137 return
138138 }
@@ -147,7 +147,7 @@ func (h *XRPCHandler) ListAnnotations(w http.ResponseWriter, r *http.Request) {
147147 CID: cid.String,
148148 Author: ActorView{
149149 DID: did,
150- Handle: handle,
150+ Handle: ResolveProfile(r.Context(), did).Handle,
151151 },
152152 Value: AnnotationRecord{
153153 CreatedAt: createdAt,
@@ -179,7 +179,7 @@ func (h *XRPCHandler) ListLikes(w http.ResponseWriter, r *http.Request) {
179179 cursor := r.URL.Query().Get("cursor")
180180
181181 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
183183 FROM likes l
184184 JOIN users u ON l.author_did = u.did
185185 WHERE 1=1`
@@ -210,9 +210,9 @@ func (h *XRPCHandler) ListLikes(w http.ResponseWriter, r *http.Request) {
210210
211211 likes := make([]LikeView, 0)
212212 for rows.Next() {
213- var uri, did, handle, fURL, artURL, createdAt string
213+ var uri, did, fURL, artURL, createdAt string
214214 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 {
216216 http.Error(w, err.Error(), http.StatusInternalServerError)
217217 return
218218 }
@@ -222,7 +222,7 @@ func (h *XRPCHandler) ListLikes(w http.ResponseWriter, r *http.Request) {
222222 CID: cid.String,
223223 Author: ActorView{
224224 DID: did,
225- Handle: handle,
225+ Handle: ResolveProfile(r.Context(), did).Handle,
226226 },
227227 Value: LikeRecord{
228228 CreatedAt: createdAt,
@@ -336,7 +336,7 @@ func (h *XRPCHandler) GetRecommendations(w http.ResponseWriter, r *http.Request)
336336 for _, rec := range peopleRecs {
337337 people = append(people, RecommendedPerson{
338338 DID: rec.DID,
339- Handle: rec.Handle,
339+ Handle: ResolveProfile(r.Context(), rec.DID).Handle,
340340 DisplayName: rec.DisplayName,
341341 Avatar: rec.AvatarURL,
342342 Jaccard: rec.Jaccard,
@@ -370,7 +370,7 @@ func (h *XRPCHandler) ListFeedLists(w http.ResponseWriter, r *http.Request) {
370370 }
371371
372372 query := `
373- SELECT u.did, u.handle, COUNT(s.id) as subscription_count
373+ SELECT u.did, COUNT(s.id) as subscription_count
374374 FROM users u
375375 LEFT JOIN subscriptions s ON u.did = s.user_did
376376 WHERE u.did IN (` + strings.Join(placeholders, ",") + `)`
@@ -398,9 +398,9 @@ func (h *XRPCHandler) ListFeedLists(w http.ResponseWriter, r *http.Request) {
398398 var users []userRow
399399
400400 for rows.Next() {
401- var did, handle string
401+ var did string
402402 var subCount int
403- if err := rows.Scan(&did, &handle, &subCount); err != nil {
403+ if err := rows.Scan(&did, &subCount); err != nil {
404404 http.Error(w, err.Error(), http.StatusInternalServerError)
405405 return
406406 }
@@ -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_at97 a.quote, a.note, a.tags, a.rating, a.created_at
98 FROM annotations a98 FROM annotations a
99 JOIN users u ON a.author_did = u.did99 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 string132+ var uri, did, fURL, artURL, createdAt string
133 var cid, quote, note, tags sql.NullString133 var cid, quote, note, tags sql.NullString
134 var rating sql.NullInt64134 var rating sql.NullInt64
135- if err := rows.Scan(&uri, &cid, &did, &handle, &fURL, &artURL, &quote, &note, &tags, &rating, &createdAt); err != nil {135+ if err := rows.Scan(&uri, &cid, &did, &fURL, &artURL, &quote, &note, &tags, &rating, &createdAt); err != nil {
136 http.Error(w, err.Error(), http.StatusInternalServerError)136 http.Error(w, err.Error(), http.StatusInternalServerError)
137 return137 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_at182+ SELECT l.uri, l.cid, u.did, l.feed_url, l.article_url, l.created_at
183 FROM likes l183 FROM likes l
184 JOIN users u ON l.author_did = u.did184 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 string213+ var uri, did, fURL, artURL, createdAt string
214 var cid sql.NullString214 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 return217 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_count373+ SELECT u.did, COUNT(s.id) as subscription_count
374 FROM users u374 FROM users u
375 LEFT JOIN subscriptions s ON u.did = s.user_did375 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 []userRow398 var users []userRow
399 399
400 for rows.Next() {400 for rows.Next() {
401- var did, handle string401+ var did string
402 var subCount int402 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 return405 return
406 }406 }
modified internal/cluster/jaccard_test.go +6 -10
@@ -40,13 +40,9 @@ func setupClusterTestDB(t *testing.T) *db.Databases {
4040 func seedClusterData(t *testing.T, ctx context.Context, dbs *db.Databases) {
4141 t.Helper()
4242
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)
5046 assert.NilError(t, err)
5147 }
5248
@@ -430,7 +426,7 @@ func TestColdStartRecommendations(t *testing.T) {
430426 _, err = dbs.DB().ExecContext(ctx, `UPDATE articles.feeds SET subscriber_count = 2 WHERE feed_url = 'https://b.com/feed'`)
431427 assert.NilError(t, err)
432428
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")
434430 assert.NilError(t, err)
435431
436432 recs, err := engine.ColdStartRecommendations(ctx, "did:test:newuser", 10)
@@ -513,9 +509,9 @@ func TestDescriptionBasedFeedSimilarity(t *testing.T) {
513509 ctx := context.Background()
514510 dbs := setupClusterTestDB(t)
515511
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")
517513 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")
519515 assert.NilError(t, err)
520516
521517 _, 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
248248
249249 func (e *Engine) ComputePeopleRecommendationsOnDemand(ctx context.Context, userDID string, limit int) ([]*PersonRecommendation, error) {
250250 rows, err := e.db.QueryContext(ctx, `
251- SELECT u.did, u.handle, COALESCE(u.display_name, ''), COALESCE(u.avatar_url, ''),
251+ SELECT u.did,
252252 sim.jaccard, sim.common_feeds, COALESCE(sim.common_likes, 0), COALESCE(sim.common_tags, 0)
253253 FROM (
254254 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
256256 SELECT user_a AS peer_did, jaccard, common_feeds, common_likes, common_tags FROM recs.user_similarity WHERE user_b = ?
257257 ) sim
258258 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)
261260 ORDER BY sim.jaccard DESC
262261 LIMIT ?
263262 `, userDID, userDID, limit)
@@ -269,7 +268,7 @@ func (e *Engine) ComputePeopleRecommendationsOnDemand(ctx context.Context, userD
269268 var results []*PersonRecommendation
270269 for rows.Next() {
271270 rec := &PersonRecommendation{}
272- if err := rows.Scan(&rec.DID, &rec.Handle, &rec.DisplayName, &rec.AvatarURL,
271+ if err := rows.Scan(&rec.DID,
273272 &rec.Jaccard, &rec.CommonFeeds, &rec.CommonLikes, &rec.CommonTags); err != nil {
274273 return nil, err
275274 }
@@ -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 ) sim257 ) sim
258 JOIN main.users u ON u.did = sim.peer_did258 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 DESC260 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 []*PersonRecommendation268 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, err273 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
3333 userDID = "did:test:user1"
3434 feedURL = "https://example.com/feed.xml"
3535
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)
3737 assert.NilError(t, err)
3838
3939 _, 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,
196196 userDID = "did:test:searcher"
197197 feedURL = "https://search.example.com/feed.xml"
198198
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)
200200 assert.NilError(t, err)
201201
202202 _, 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) {
1212 ctx := context.Background()
1313 dbs := setupTestDB(t)
1414
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"})
2016 assert.NilError(t, err)
2117
2218 u1, err := dbs.Users.GetUser(ctx, "did:test:u1")
2319 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")
2621
2722 u2, err := dbs.Users.GetUser(ctx, "did:test:u2")
2823 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")
3125 }
3226
33-func TestBatchCreateUsers_UpsertsExisting(t *testing.T) {
27+func TestBatchCreateUsers_IgnoresExisting(t *testing.T) {
3428 ctx := context.Background()
3529 dbs := setupTestDB(t)
3630
37- _, err := dbs.Users.CreateUser(ctx, "did:test:u1", "old-handle", "", "")
31+ _, err := dbs.Users.CreateUser(ctx, "did:test:u1")
3832 assert.NilError(t, err)
3933
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"})
4435 assert.NilError(t, err)
4536
4637 u, err := dbs.Users.GetUser(ctx, "did:test:u1")
4738 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")
5040 }
5141
5242 func TestBatchCreateUsers_Empty(t *testing.T) {
@@ -57,30 +47,10 @@ func TestBatchCreateUsers_Empty(t *testing.T) {
5747 assert.NilError(t, err)
5848 }
5949
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-
8050 func seedSubscriptionData(t *testing.T, ctx context.Context, dbs *Databases) (userDID string) {
8151 t.Helper()
8252 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)
8454 assert.NilError(t, err)
8555 return userDID
8656 }
@@ -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 userDID55 return userDID
86 }56 }
modified internal/db/db.go +0 -4
@@ -93,9 +93,6 @@ func initSchema(db *DB) error {
9393 var schema = []string{
9494 `CREATE TABLE IF NOT EXISTS users (
9595 did TEXT PRIMARY KEY,
96- handle TEXT NOT NULL,
97- display_name TEXT,
98- avatar_url TEXT,
9996 indexed_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
10097 updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
10198 )`,
@@ -274,7 +271,6 @@ var schema = []string{
274271 `CREATE INDEX IF NOT EXISTS idx_follow_distances_b ON follow_distances(user_b)`,
275272 `CREATE INDEX IF NOT EXISTS idx_follow_distances_a_dist ON follow_distances(user_a, distance)`,
276273 `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)`,
278274 `CREATE VIRTUAL TABLE IF NOT EXISTS articles_fts USING fts5(title, summary, content, author, content=articles, content_rowid=id)`,
279275 `CREATE TRIGGER IF NOT EXISTS articles_ai AFTER INSERT ON articles BEGIN
280276 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_TIMESTAMP97 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 BEGIN275 `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,
1313 userDID = "did:test:follower"
1414 targetDID = "did:test:followed"
1515
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)
1717 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)
1919 assert.NilError(t, err)
2020
2121 return userDID, targetDID
@@ -83,7 +83,7 @@ func TestListFollows(t *testing.T) {
8383 userDID, _ := seedFollowData(t, ctx, dbs)
8484
8585 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)
8787 assert.NilError(t, err)
8888
8989 err = dbs.Users.UpsertFollow(ctx, userDID, "did:test:followed", "uri1", "cid1")
@@ -102,7 +102,7 @@ func TestListFollowers(t *testing.T) {
102102 _, targetDID := seedFollowData(t, ctx, dbs)
103103
104104 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)
106106 assert.NilError(t, err)
107107
108108 err = dbs.Users.UpsertFollow(ctx, "did:test:follower", targetDID, "uri1", "cid1")
@@ -121,7 +121,7 @@ func TestGetFollowDIDs(t *testing.T) {
121121 userDID, _ := seedFollowData(t, ctx, dbs)
122122
123123 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)
125125 assert.NilError(t, err)
126126
127127 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, targetDID21 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 {
125125 var usersSchema = []string{
126126 `CREATE TABLE IF NOT EXISTS users (
127127 did TEXT PRIMARY KEY,
128- handle TEXT NOT NULL,
129- display_name TEXT,
130- avatar_url TEXT,
131128 indexed_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
132129 updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
133130 )`,
@@ -157,7 +154,6 @@ var usersSchema = []string{
157154 `CREATE INDEX IF NOT EXISTS idx_follows_target ON follows(target_did)`,
158155 `CREATE INDEX IF NOT EXISTS idx_follows_uri ON follows(uri)`,
159156 `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)`,
161157 }
162158
163159 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_TIMESTAMP129 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
4242 func (s *ArticleStore) GetAnnotation(ctx context.Context, id int64) (*Annotation, error) {
4343 a := &Annotation{}
4444 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
4646 FROM articles.annotations a
47- LEFT JOIN users u ON a.author_did = u.did
4847 LEFT JOIN articles.articles ar ON ar.url = a.article_url AND ar.feed_url = a.feed_url
4948 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,
5150 &a.Quote, &a.Note, &a.Tags, &a.Rating, &a.CreatedAt, &a.CID)
5251 if err != nil {
5352 return nil, err
@@ -89,9 +88,8 @@ func (s *ArticleStore) ListAnnotations(ctx context.Context, feedURL, articleURL,
8988 args = append(args, authorDID)
9089 }
9190
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
9392 FROM articles.annotations a
94- LEFT JOIN users u ON a.author_did = u.did
9593 LEFT JOIN articles.articles ar ON ar.url = a.article_url AND ar.feed_url = a.feed_url`
9694 if len(conds) > 0 {
9795 query += ` WHERE ` + strings.Join(conds, " AND ")
@@ -108,7 +106,7 @@ func (s *ArticleStore) ListAnnotations(ctx context.Context, feedURL, articleURL,
108106 var annotations []*Annotation
109107 for rows.Next() {
110108 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,
112110 &a.Quote, &a.Note, &a.Tags, &a.Rating, &a.CreatedAt, &a.CID); err != nil {
113111 return nil, err
114112 }
@@ -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.cid45+ 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 a46 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_url47 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, err52 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.cid91+ 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 a92 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 []*Annotation106 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, err111 return nil, err
114 }112 }
modified internal/db/user.go +14 -49
@@ -6,19 +6,12 @@ import (
66 )
77
88 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 {
189 DID string
1910 Handle string
2011 DisplayName string
2112 AvatarURL string
13+ IndexedAt sql.NullTime
14+ UpdatedAt sql.NullTime
2215 }
2316
2417 type UserStore struct {
@@ -29,8 +22,8 @@ func NewUserStore(db *DB) *UserStore {
2922 return &UserStore{db: db}
3023 }
3124
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 {
3427 return nil
3528 }
3629 tx, err := s.db.BeginTx(ctx, nil)
@@ -40,29 +33,24 @@ func (s *UserStore) BatchCreateUsers(ctx context.Context, users []UserData) erro
4033 defer tx.Rollback()
4134
4235 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)
5038 `)
5139 if err != nil {
5240 return err
5341 }
5442 defer stmt.Close()
5543
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 {
5846 return err
5947 }
6048 }
6149 return tx.Commit()
6250 }
6351
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})
6654 if err != nil {
6755 return nil, err
6856 }
@@ -72,21 +60,9 @@ func (s *UserStore) CreateUser(ctx context.Context, did, handle, displayName, av
7260 func (s *UserStore) GetUser(ctx context.Context, did string) (*User, error) {
7361 u := &User{}
7462 err := s.db.QueryRowContext(ctx, `
75- SELECT did, handle, display_name, avatar_url, indexed_at, updated_at
63+ SELECT did, indexed_at, updated_at
7664 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)
9066 if err != nil {
9167 return nil, err
9268 }
@@ -111,20 +87,9 @@ func (s *UserStore) ListUserDIDs(ctx context.Context) (map[string]bool, error) {
11187 return dids, rows.Err()
11288 }
11389
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-
12590 func (s *UserStore) ListUsers(ctx context.Context) ([]*User, error) {
12691 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
12893 FROM users ORDER BY updated_at DESC
12994 `)
13095 if err != nil {
@@ -135,7 +100,7 @@ func (s *UserStore) ListUsers(ctx context.Context) ([]*User, error) {
135100 var users []*User
136101 for rows.Next() {
137102 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 {
139104 return nil, err
140105 }
141106 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 string9 DID string
19 Handle string10 Handle string
20 DisplayName string11 DisplayName string
21 AvatarURL string12 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 nil27 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 err40 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 err46 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, err55 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_at63+ 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, err67 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_at92+ SELECT did, indexed_at, updated_at
128 FROM users ORDER BY updated_at DESC93 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 []*User100 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, err104 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 (
77 "gotest.tools/v3/assert"
88 )
99
10-func TestUpdateUserProfile_SetsFields(t *testing.T) {
10+func TestGetUser(t *testing.T) {
1111 ctx := context.Background()
1212 dbs := setupTestDB(t)
1313
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")
1515 assert.NilError(t, err)
16+ assert.Equal(t, u.DID, "did:test:profile")
1617
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")
1819 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")
4121 }
@@ -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 @@
11 package server
22
33 import (
4+ "context"
45 "database/sql"
56 "fmt"
67 "net/http"
@@ -50,6 +51,7 @@ func (s *Server) handleLibrary(w http.ResponseWriter, r *http.Request) {
5051 if err != nil {
5152 s.logger.Warn("failed to list annotations", "error", err, "did", user.DID)
5253 }
54+ resolveAnnotationHandles(ctx, annotations)
5355 annotHasMore := len(annotations) > limit
5456 if annotHasMore {
5557 annotations = annotations[:limit]
@@ -120,7 +122,7 @@ func (s *Server) handleCreateAnnotation(w http.ResponseWriter, r *http.Request)
120122 return
121123 }
122124
123- a.AuthorHandle = user.Handle
125+ a.AuthorHandle = atproto.ResolveProfile(r.Context(), user.DID).Handle
124126 s.render(w, r, "annotation-card.html", map[string]any{
125127 "annotation": a,
126128 "userDID": user.DID,
@@ -165,3 +167,11 @@ func (s *Server) handleDeleteAnnotation(w http.ResponseWriter, r *http.Request)
165167
166168 w.WriteHeader(http.StatusOK)
167169 }
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 server1 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) > limit55 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 return122 return
121 }123 }
122 124
123- a.AuthorHandle = user.Handle125+ 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) {
3333 http.Error(w, "could not resolve handle", http.StatusInternalServerError)
3434 return
3535 }
36- user, createErr := s.dbs.Users.CreateUser(r.Context(), did, handle, "", "")
36+ user, createErr := s.dbs.Users.CreateUser(r.Context(), did)
3737 if createErr != nil {
3838 http.Error(w, createErr.Error(), http.StatusInternalServerError)
3939 return
@@ -67,7 +67,7 @@ func (s *Server) handleAuthCallback(w http.ResponseWriter, r *http.Request) {
6767 return
6868 }
6969
70- user, err := s.dbs.Users.CreateUser(r.Context(), did, handle, "", "")
70+ user, err := s.dbs.Users.CreateUser(r.Context(), did)
7171 if err != nil {
7272 s.logger.Error("failed to create user", "error", err)
7373 http.Error(w, err.Error(), http.StatusInternalServerError)
@@ -87,22 +87,10 @@ func (s *Server) handleOAuthCallback(w http.ResponseWriter, r *http.Request) {
8787 }
8888
8989 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- }
9490
9591 client := s.pdsClientFromSession(sessData)
9692
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)
10694 if err != nil {
10795 s.logger.Error("failed to create user", "error", err)
10896 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 return34 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 return39 return
@@ -67,7 +67,7 @@ func (s *Server) handleAuthCallback(w http.ResponseWriter, r *http.Request) {
67 return67 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 string93+ 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 @@
11 package server
22
33 import (
4+ "context"
45 "net/http"
56 "time"
67
8+ "pkg.rbrt.fr/glean/internal/atproto"
79 "pkg.rbrt.fr/glean/internal/cluster"
810 )
911
@@ -41,6 +43,7 @@ func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) {
4143 if err != nil {
4244 s.logger.Warn("failed to get people recommendations", "error", err, "did", user.DID)
4345 }
46+ resolvePeopleHandles(ctx, peopleRecs)
4447
4548 feedRecs, err := s.engine.GetFeedRecommendations(ctx, user.DID, 5)
4649 if err != nil {
@@ -110,3 +113,12 @@ func (s *Server) handleDismissArticleRecommendation(w http.ResponseWriter, r *ht
110113
111114 w.WriteHeader(http.StatusOK)
112115 }
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 server1 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) {
4343 if err != nil {
4444 s.logger.Warn("failed to get people recommendations", "error", err, "did", user.DID)
4545 }
46+ resolvePeopleHandles(ctx, peopleRecs)
4647
4748 if len(feedRecs) > 0 {
4849 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) {
1717 if strings.HasPrefix(param, "did:") {
1818 did = param
1919 } 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
3125 }
26+ did = resolved
3227 }
3328
3429 profileUser, err := s.dbs.Users.GetUser(ctx, did)
@@ -38,16 +33,10 @@ func (s *Server) handleProfile(w http.ResponseWriter, r *http.Request) {
3833 return
3934 }
4035
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
5140
5241 subs, err := s.dbs.Articles.ListSubscriptions(ctx, did, "", 50, 0)
5342 if err != nil {
@@ -58,6 +47,7 @@ func (s *Server) handleProfile(w http.ResponseWriter, r *http.Request) {
5847 if err != nil {
5948 s.logger.Warn("failed to list annotations", "error", err, "did", did)
6049 }
50+ resolveAnnotationHandles(ctx, annotations)
6151
6252 subCount, err := s.dbs.Articles.GetSubscriptionCount(ctx, did)
6353 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 = param18 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.DID22+ 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 return33 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 (
1212 "strconv"
1313 "strings"
1414 "sync"
15-
16- "golang.org/x/sync/errgroup"
1715 "time"
1816
1917 "github.com/go-chi/chi/v5"
@@ -490,26 +488,7 @@ func (s *Server) BackfillFromCollectionDir(ctx context.Context, collectionDirURL
490488 defer func() { <-sem }()
491489 defer wg.Done()
492490
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 {
513492 s.logger.Error("failed to create user during backfill", "error", err, "did", did)
514493 return
515494 }
@@ -563,10 +542,6 @@ func (s *Server) runSyncAll(ctx context.Context) {
563542 s.logger.Error("periodic sync failed", "error", err, "did", u.DID)
564543 }
565544
566- if dn, avatar, err := client.GetProfile(ctx, u.DID); err == nil {
567- _ = s.dbs.Users.UpdateUserProfile(ctx, u.DID, dn, avatar)
568- }
569-
570545 metrics.SyncRuns.Inc()
571546 }
572547 }
@@ -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 return493 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 (
99 "net/http"
1010 "os"
1111
12+ "pkg.rbrt.fr/glean/internal/atproto"
1213 "pkg.rbrt.fr/glean/internal/db"
1314 )
1415
@@ -39,6 +40,10 @@ func (s *Server) getUserFromSession(r *http.Request) *db.User {
3940 return nil
4041 }
4142
43+ p := atproto.ResolveProfile(r.Context(), user.DID)
44+ user.Handle = p.Handle
45+ user.DisplayName = p.DisplayName
46+ user.AvatarURL = p.AvatarURL
4247 return user
4348 }
4449
@@ -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 nil40 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 user47 return user
43 }48 }
44 49
modified internal/tmpl/base.html +2 -2
@@ -96,7 +96,7 @@
9696 {{if .User}}
9797 <div class="flex items-center gap-2">
9898 <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}}
100100 <span class="text-sm font-medium truncate text-spot-text">@{{.User.Handle}}</span>
101101 </a>
102102 <form method="POST" action="/auth/logout" class="shrink-0">
@@ -142,7 +142,7 @@
142142 {{template "logo-text"}}
143143 {{if .User}}
144144 <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}}
146146 <span class="text-xs text-spot-secondary">@{{.User.Handle}}</span>
147147 </a>
148148 {{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 @@
22 <div class="bg-spot-surface rounded-xl p-4 flex items-center gap-3 hover:bg-spot-hover-50 transition">
33 {{if .AvatarURL}}<img src="{{.AvatarURL}}" class="w-10 h-10 rounded-full">{{end}}
44 <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>
66 {{if .DisplayName}}<div class="text-sm text-spot-secondary">{{.DisplayName}}</div>{{end}}
77 </div>
88 <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 @@
22 <div class="max-w-2xl mx-auto">
33 <div class="bg-spot-surface rounded-xl p-6 mb-6">
44 <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}}
66 <div class="min-w-0 flex-1">
77 <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}}
99 </h1>
1010 <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>
1111 <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">