nandi/gleanpublic Fork 0
2345d08
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.

Refactor simplify helpers and methodsUnverified

Julien Robert committed 2026-05-08T00:16:21+02:00 Browse files
2345d08 parent: 73f96db
modified internal/atproto/sync.go +5 -18
@@ -50,20 +50,7 @@ func (s *Sync) Run(ctx context.Context, userDID string) error {
5050 }
5151
5252 func (s *Sync) listRecords(ctx context.Context, userDID, collection string) ([]Record, error) {
53- var allRecords []Record
54- cursor := ""
55- for {
56- records, next, err := s.client.ListRecords(ctx, userDID, collection, 100, cursor)
57- if err != nil {
58- return nil, err
59- }
60- allRecords = append(allRecords, records...)
61- if next == "" || len(records) == 0 {
62- break
63- }
64- cursor = next
65- }
66- return allRecords, nil
53+ return listAllRecords(ctx, s.client, userDID, collection)
6754 }
6855
6956 func (s *Sync) syncSubscriptions(ctx context.Context, userDID string) error {
@@ -148,7 +135,7 @@ func (s *Sync) syncLikes(ctx context.Context, userDID string) error {
148135 continue
149136 }
150137 activeURIs[r.URI] = true
151- t, _ := time.Parse(time.RFC3339, rec.CreatedAt)
138+ t := parseRFC3339(rec.CreatedAt)
152139 likes = append(likes, &db.Like{
153140 URI: r.URI,
154141 AuthorDID: userDID,
@@ -187,7 +174,7 @@ func (s *Sync) syncAnnotations(ctx context.Context, userDID string) error {
187174 continue
188175 }
189176 activeURIs[r.URI] = true
190- t, _ := time.Parse(time.RFC3339, rec.CreatedAt)
177+ t := parseRFC3339(rec.CreatedAt)
191178 a := &db.Annotation{
192179 URI: r.URI,
193180 AuthorDID: userDID,
@@ -221,7 +208,7 @@ func (s *Sync) syncAnnotations(ctx context.Context, userDID string) error {
221208 feedURL = article.FeedURL
222209 }
223210
224- t, _ := time.Parse(time.RFC3339, rec.CreatedAt)
211+ t := parseRFC3339(rec.CreatedAt)
225212 annotations = append(annotations, &db.Annotation{
226213 URI: r.URI,
227214 AuthorDID: userDID,
@@ -259,7 +246,7 @@ func (s *Sync) syncFollows(ctx context.Context, userDID string) error {
259246 continue
260247 }
261248
262- t, _ := time.Parse(time.RFC3339, rec.CreatedAt)
249+ t := parseRFC3339(rec.CreatedAt)
263250 activeFollows[rec.Subject] = db.Follow{
264251 URI: db.NullStr(r.URI),
265252 CID: db.NullStr(r.CID),
@@ -50,20 +50,7 @@ func (s *Sync) Run(ctx context.Context, userDID string) error {
50 }50 }
51 51
52 func (s *Sync) listRecords(ctx context.Context, userDID, collection string) ([]Record, error) {52 func (s *Sync) listRecords(ctx context.Context, userDID, collection string) ([]Record, error) {
53- var allRecords []Record53+ return listAllRecords(ctx, s.client, userDID, collection)
54- cursor := ""
55- for {
56- records, next, err := s.client.ListRecords(ctx, userDID, collection, 100, cursor)
57- if err != nil {
58- return nil, err
59- }
60- allRecords = append(allRecords, records...)
61- if next == "" || len(records) == 0 {
62- break
63- }
64- cursor = next
65- }
66- return allRecords, nil
67 }54 }
68 55
69 func (s *Sync) syncSubscriptions(ctx context.Context, userDID string) error {56 func (s *Sync) syncSubscriptions(ctx context.Context, userDID string) error {
@@ -148,7 +135,7 @@ func (s *Sync) syncLikes(ctx context.Context, userDID string) error {
148 continue135 continue
149 }136 }
150 activeURIs[r.URI] = true137 activeURIs[r.URI] = true
151- t, _ := time.Parse(time.RFC3339, rec.CreatedAt)138+ t := parseRFC3339(rec.CreatedAt)
152 likes = append(likes, &db.Like{139 likes = append(likes, &db.Like{
153 URI: r.URI,140 URI: r.URI,
154 AuthorDID: userDID,141 AuthorDID: userDID,
@@ -187,7 +174,7 @@ func (s *Sync) syncAnnotations(ctx context.Context, userDID string) error {
187 continue174 continue
188 }175 }
189 activeURIs[r.URI] = true176 activeURIs[r.URI] = true
190- t, _ := time.Parse(time.RFC3339, rec.CreatedAt)177+ t := parseRFC3339(rec.CreatedAt)
191 a := &db.Annotation{178 a := &db.Annotation{
192 URI: r.URI,179 URI: r.URI,
193 AuthorDID: userDID,180 AuthorDID: userDID,
@@ -221,7 +208,7 @@ func (s *Sync) syncAnnotations(ctx context.Context, userDID string) error {
221 feedURL = article.FeedURL208 feedURL = article.FeedURL
222 }209 }
223 210
224- t, _ := time.Parse(time.RFC3339, rec.CreatedAt)211+ t := parseRFC3339(rec.CreatedAt)
225 annotations = append(annotations, &db.Annotation{212 annotations = append(annotations, &db.Annotation{
226 URI: r.URI,213 URI: r.URI,
227 AuthorDID: userDID,214 AuthorDID: userDID,
@@ -259,7 +246,7 @@ func (s *Sync) syncFollows(ctx context.Context, userDID string) error {
259 continue246 continue
260 }247 }
261 248
262- t, _ := time.Parse(time.RFC3339, rec.CreatedAt)249+ t := parseRFC3339(rec.CreatedAt)
263 activeFollows[rec.Subject] = db.Follow{250 activeFollows[rec.Subject] = db.Follow{
264 URI: db.NullStr(r.URI),251 URI: db.NullStr(r.URI),
265 CID: db.NullStr(r.CID),252 CID: db.NullStr(r.CID),
modified internal/cluster/dismiss.go +11 -15
@@ -11,31 +11,27 @@ type Impression struct {
1111 TargetID string
1212 }
1313
14-func (e *Engine) DismissFeed(ctx context.Context, userDID, feedURL, reason string) error {
14+// Dismiss records that the user dismissed a recommendation. targetType is one
15+// of "feed", "article", "person".
16+func (e *Engine) Dismiss(ctx context.Context, userDID, targetType, targetID, reason string) error {
1517 _, err := e.db.ExecContext(ctx, `
1618 INSERT INTO main.dismissed_recommendations (user_did, target_type, target_id, reason)
17- VALUES (?, 'feed', ?, ?)
19+ VALUES (?, ?, ?, ?)
1820 ON CONFLICT(user_did, target_type, target_id) DO UPDATE SET reason = excluded.reason, dismissed_at = CURRENT_TIMESTAMP
19- `, userDID, feedURL, reason)
21+ `, userDID, targetType, targetID, reason)
2022 return err
2123 }
2224
25+func (e *Engine) DismissFeed(ctx context.Context, userDID, feedURL, reason string) error {
26+ return e.Dismiss(ctx, userDID, "feed", feedURL, reason)
27+}
28+
2329 func (e *Engine) DismissArticle(ctx context.Context, userDID, articleURL, reason string) error {
24- _, err := e.db.ExecContext(ctx, `
25- INSERT INTO main.dismissed_recommendations (user_did, target_type, target_id, reason)
26- VALUES (?, 'article', ?, ?)
27- ON CONFLICT(user_did, target_type, target_id) DO UPDATE SET reason = excluded.reason, dismissed_at = CURRENT_TIMESTAMP
28- `, userDID, articleURL, reason)
29- return err
30+ return e.Dismiss(ctx, userDID, "article", articleURL, reason)
3031 }
3132
3233 func (e *Engine) DismissPerson(ctx context.Context, userDID, targetDID, reason string) error {
33- _, err := e.db.ExecContext(ctx, `
34- INSERT INTO main.dismissed_recommendations (user_did, target_type, target_id, reason)
35- VALUES (?, 'person', ?, ?)
36- ON CONFLICT(user_did, target_type, target_id) DO UPDATE SET reason = excluded.reason, dismissed_at = CURRENT_TIMESTAMP
37- `, userDID, targetDID, reason)
38- return err
34+ return e.Dismiss(ctx, userDID, "person", targetDID, reason)
3935 }
4036
4137 func (e *Engine) RecordImpressions(ctx context.Context, userDID string, impressions []Impression) error {
@@ -11,31 +11,27 @@ type Impression struct {
11 TargetID string11 TargetID string
12 }12 }
13 13
14-func (e *Engine) DismissFeed(ctx context.Context, userDID, feedURL, reason string) error {14+// Dismiss records that the user dismissed a recommendation. targetType is one
15+// of "feed", "article", "person".
16+func (e *Engine) Dismiss(ctx context.Context, userDID, targetType, targetID, reason string) error {
15 _, err := e.db.ExecContext(ctx, `17 _, err := e.db.ExecContext(ctx, `
16 INSERT INTO main.dismissed_recommendations (user_did, target_type, target_id, reason)18 INSERT INTO main.dismissed_recommendations (user_did, target_type, target_id, reason)
17- VALUES (?, 'feed', ?, ?)19+ VALUES (?, ?, ?, ?)
18 ON CONFLICT(user_did, target_type, target_id) DO UPDATE SET reason = excluded.reason, dismissed_at = CURRENT_TIMESTAMP20 ON CONFLICT(user_did, target_type, target_id) DO UPDATE SET reason = excluded.reason, dismissed_at = CURRENT_TIMESTAMP
19- `, userDID, feedURL, reason)21+ `, userDID, targetType, targetID, reason)
20 return err22 return err
21 }23 }
22 24
25+func (e *Engine) DismissFeed(ctx context.Context, userDID, feedURL, reason string) error {
26+ return e.Dismiss(ctx, userDID, "feed", feedURL, reason)
27+}
28+
23 func (e *Engine) DismissArticle(ctx context.Context, userDID, articleURL, reason string) error {29 func (e *Engine) DismissArticle(ctx context.Context, userDID, articleURL, reason string) error {
24- _, err := e.db.ExecContext(ctx, `30+ return e.Dismiss(ctx, userDID, "article", articleURL, reason)
25- INSERT INTO main.dismissed_recommendations (user_did, target_type, target_id, reason)
26- VALUES (?, 'article', ?, ?)
27- ON CONFLICT(user_did, target_type, target_id) DO UPDATE SET reason = excluded.reason, dismissed_at = CURRENT_TIMESTAMP
28- `, userDID, articleURL, reason)
29- return err
30 }31 }
31 32
32 func (e *Engine) DismissPerson(ctx context.Context, userDID, targetDID, reason string) error {33 func (e *Engine) DismissPerson(ctx context.Context, userDID, targetDID, reason string) error {
33- _, err := e.db.ExecContext(ctx, `34+ return e.Dismiss(ctx, userDID, "person", targetDID, reason)
34- INSERT INTO main.dismissed_recommendations (user_did, target_type, target_id, reason)
35- VALUES (?, 'person', ?, ?)
36- ON CONFLICT(user_did, target_type, target_id) DO UPDATE SET reason = excluded.reason, dismissed_at = CURRENT_TIMESTAMP
37- `, userDID, targetDID, reason)
38- return err
39 }35 }
40 36
41 func (e *Engine) RecordImpressions(ctx context.Context, userDID string, impressions []Impression) error {37 func (e *Engine) RecordImpressions(ctx context.Context, userDID string, impressions []Impression) error {
modified internal/cluster/scoring.go +31 -65
@@ -4,7 +4,8 @@ import (
44 "context"
55 "database/sql"
66 "fmt"
7- "strings"
7+
8+ "pkg.rbrt.fr/glean/internal/db"
89 )
910
1011 type FeedRecommendation struct {
@@ -234,83 +235,48 @@ func (e *Engine) ComputeFeedRecommendationsOnDemand(ctx context.Context, userDID
234235 return results, rows.Err()
235236 }
236237
237-func buildLangFilter(languages []string, prefix string) (string, []any) {
238- if len(languages) == 0 {
239- return "", nil
240- }
241- ph := make([]string, len(languages))
242- args := make([]any, len(languages))
243- for i, l := range languages {
244- ph[i] = "?"
245- args[i] = l
246- }
247- return "AND (" + prefix + "language IN (" + strings.Join(ph, ",") + ") OR " + prefix + "language = '')", args
248-}
249-
250-func normalizeFeedScores(recs []*FeedRecommendation) {
251- if len(recs) < 2 {
238+func normalizeScores[T any](items []T, getScore func(T) float64, setScore func(T, float64)) {
239+ if len(items) < 2 {
252240 return
253241 }
254- min, max := recs[0].Score, recs[0].Score
255- for _, r := range recs[1:] {
256- if r.Score < min {
257- min = r.Score
242+ smin, smax := getScore(items[0]), getScore(items[0])
243+ for i := range items[1:] {
244+ s := getScore(items[i+1])
245+ if s < smin {
246+ smin = s
258247 }
259- if r.Score > max {
260- max = r.Score
248+ if s > smax {
249+ smax = s
261250 }
262251 }
263- if max == min {
252+ if smax == smin {
264253 return
265254 }
266- span := max - min
267- for _, r := range recs {
268- r.Score = (r.Score - min) / span
255+ span := smax - smin
256+ for i := range items {
257+ setScore(items[i], (getScore(items[i])-smin)/span)
269258 }
270259 }
271260
261+func normalizeFeedScores(recs []*FeedRecommendation) {
262+ normalizeScores(recs,
263+ func(r *FeedRecommendation) float64 { return r.Score },
264+ func(r *FeedRecommendation, v float64) { r.Score = v },
265+ )
266+}
267+
272268 func normalizeArticleScores(recs []*ArticleRecommendation) {
273- if len(recs) < 2 {
274- return
275- }
276- min, max := recs[0].Score, recs[0].Score
277- for _, r := range recs[1:] {
278- if r.Score < min {
279- min = r.Score
280- }
281- if r.Score > max {
282- max = r.Score
283- }
284- }
285- if max == min {
286- return
287- }
288- span := max - min
289- for _, r := range recs {
290- r.Score = (r.Score - min) / span
291- }
269+ normalizeScores(recs,
270+ func(r *ArticleRecommendation) float64 { return r.Score },
271+ func(r *ArticleRecommendation, v float64) { r.Score = v },
272+ )
292273 }
293274
294275 func normalizePersonScores(recs []*PersonRecommendation) {
295- if len(recs) < 2 {
296- return
297- }
298- min, max := recs[0].Jaccard, recs[0].Jaccard
299- for _, r := range recs[1:] {
300- if r.Jaccard < min {
301- min = r.Jaccard
302- }
303- if r.Jaccard > max {
304- max = r.Jaccard
305- }
306- }
307- if max == min {
308- return
309- }
310- span := max - min
311- for _, r := range recs {
312- r.Jaccard = (r.Jaccard - min) / span
313- }
276+ normalizeScores(recs,
277+ func(r *PersonRecommendation) float64 { return r.Jaccard },
278+ func(r *PersonRecommendation, v float64) { r.Jaccard = v },
279+ )
314280 }
315281
316282 func (e *Engine) coldStartFromEmbeddings(ctx context.Context, userDID string, limit int) ([]*FeedRecommendation, error) {
@@ -422,7 +388,7 @@ func (e *Engine) ComputeArticleRecommendationsOnDemand(ctx context.Context, user
422388 }
423389 }
424390
425- langFilter, langArgs := buildLangFilter(languages, "a.")
391+ langFilter, langArgs := db.BuildLangFilter(languages, "a.")
426392
427393 query := fmt.Sprintf(`
428394 WITH similar_users AS (
@@ -4,7 +4,8 @@ import (
4 "context"4 "context"
5 "database/sql"5 "database/sql"
6 "fmt"6 "fmt"
7- "strings"7+
8+ "pkg.rbrt.fr/glean/internal/db"
8 )9 )
9 10
10 type FeedRecommendation struct {11 type FeedRecommendation struct {
@@ -234,83 +235,48 @@ func (e *Engine) ComputeFeedRecommendationsOnDemand(ctx context.Context, userDID
234 return results, rows.Err()235 return results, rows.Err()
235 }236 }
236 237
237-func buildLangFilter(languages []string, prefix string) (string, []any) {238+func normalizeScores[T any](items []T, getScore func(T) float64, setScore func(T, float64)) {
238- if len(languages) == 0 {239+ if len(items) < 2 {
239- return "", nil
240- }
241- ph := make([]string, len(languages))
242- args := make([]any, len(languages))
243- for i, l := range languages {
244- ph[i] = "?"
245- args[i] = l
246- }
247- return "AND (" + prefix + "language IN (" + strings.Join(ph, ",") + ") OR " + prefix + "language = '')", args
248-}
249-
250-func normalizeFeedScores(recs []*FeedRecommendation) {
251- if len(recs) < 2 {
252 return240 return
253 }241 }
254- min, max := recs[0].Score, recs[0].Score242+ smin, smax := getScore(items[0]), getScore(items[0])
255- for _, r := range recs[1:] {243+ for i := range items[1:] {
256- if r.Score < min {244+ s := getScore(items[i+1])
257- min = r.Score245+ if s < smin {
246+ smin = s
258 }247 }
259- if r.Score > max {248+ if s > smax {
260- max = r.Score249+ smax = s
261 }250 }
262 }251 }
263- if max == min {252+ if smax == smin {
264 return253 return
265 }254 }
266- span := max - min255+ span := smax - smin
267- for _, r := range recs {256+ for i := range items {
268- r.Score = (r.Score - min) / span257+ setScore(items[i], (getScore(items[i])-smin)/span)
269 }258 }
270 }259 }
271 260
261+func normalizeFeedScores(recs []*FeedRecommendation) {
262+ normalizeScores(recs,
263+ func(r *FeedRecommendation) float64 { return r.Score },
264+ func(r *FeedRecommendation, v float64) { r.Score = v },
265+ )
266+}
267+
272 func normalizeArticleScores(recs []*ArticleRecommendation) {268 func normalizeArticleScores(recs []*ArticleRecommendation) {
273- if len(recs) < 2 {269+ normalizeScores(recs,
274- return270+ func(r *ArticleRecommendation) float64 { return r.Score },
275- }271+ func(r *ArticleRecommendation, v float64) { r.Score = v },
276- min, max := recs[0].Score, recs[0].Score272+ )
277- for _, r := range recs[1:] {
278- if r.Score < min {
279- min = r.Score
280- }
281- if r.Score > max {
282- max = r.Score
283- }
284- }
285- if max == min {
286- return
287- }
288- span := max - min
289- for _, r := range recs {
290- r.Score = (r.Score - min) / span
291- }
292 }273 }
293 274
294 func normalizePersonScores(recs []*PersonRecommendation) {275 func normalizePersonScores(recs []*PersonRecommendation) {
295- if len(recs) < 2 {276+ normalizeScores(recs,
296- return277+ func(r *PersonRecommendation) float64 { return r.Jaccard },
297- }278+ func(r *PersonRecommendation, v float64) { r.Jaccard = v },
298- min, max := recs[0].Jaccard, recs[0].Jaccard279+ )
299- for _, r := range recs[1:] {
300- if r.Jaccard < min {
301- min = r.Jaccard
302- }
303- if r.Jaccard > max {
304- max = r.Jaccard
305- }
306- }
307- if max == min {
308- return
309- }
310- span := max - min
311- for _, r := range recs {
312- r.Jaccard = (r.Jaccard - min) / span
313- }
314 }280 }
315 281
316 func (e *Engine) coldStartFromEmbeddings(ctx context.Context, userDID string, limit int) ([]*FeedRecommendation, error) {282 func (e *Engine) coldStartFromEmbeddings(ctx context.Context, userDID string, limit int) ([]*FeedRecommendation, error) {
@@ -422,7 +388,7 @@ func (e *Engine) ComputeArticleRecommendationsOnDemand(ctx context.Context, user
422 }388 }
423 }389 }
424 390
425- langFilter, langArgs := buildLangFilter(languages, "a.")391+ langFilter, langArgs := db.BuildLangFilter(languages, "a.")
426 392
427 query := fmt.Sprintf(`393 query := fmt.Sprintf(`
428 WITH similar_users AS (394 WITH similar_users AS (
modified internal/db/social.go +2 -2
@@ -9,7 +9,7 @@ import (
99
1010 var ErrDuplicateLike = errors.New("already liked this article")
1111
12-func buildLangFilter(languages []string, prefix string) (string, []any) {
12+func BuildLangFilter(languages []string, prefix string) (string, []any) {
1313 if len(languages) == 0 {
1414 return "", nil
1515 }
@@ -381,7 +381,7 @@ type TrendingItem struct {
381381 }
382382
383383 func (s *ArticleStore) ListTrendingArticlesForUser(ctx context.Context, userDID, since string, languages []string, limit, offset int) ([]*TrendingItem, error) {
384- langFilter, langArgs := buildLangFilter(languages, "ar.")
384+ langFilter, langArgs := BuildLangFilter(languages, "ar.")
385385
386386 rows, err := s.db.QueryContext(ctx, `
387387 SELECT ar.id, ar.title, COALESCE(ar.url, ''), COALESCE(ar.author, ''),
@@ -9,7 +9,7 @@ import (
9 9
10 var ErrDuplicateLike = errors.New("already liked this article")10 var ErrDuplicateLike = errors.New("already liked this article")
11 11
12-func buildLangFilter(languages []string, prefix string) (string, []any) {12+func BuildLangFilter(languages []string, prefix string) (string, []any) {
13 if len(languages) == 0 {13 if len(languages) == 0 {
14 return "", nil14 return "", nil
15 }15 }
@@ -381,7 +381,7 @@ type TrendingItem struct {
381 }381 }
382 382
383 func (s *ArticleStore) ListTrendingArticlesForUser(ctx context.Context, userDID, since string, languages []string, limit, offset int) ([]*TrendingItem, error) {383 func (s *ArticleStore) ListTrendingArticlesForUser(ctx context.Context, userDID, since string, languages []string, limit, offset int) ([]*TrendingItem, error) {
384- langFilter, langArgs := buildLangFilter(languages, "ar.")384+ langFilter, langArgs := BuildLangFilter(languages, "ar.")
385 385
386 rows, err := s.db.QueryContext(ctx, `386 rows, err := s.db.QueryContext(ctx, `
387 SELECT ar.id, ar.title, COALESCE(ar.url, ''), COALESCE(ar.author, ''),387 SELECT ar.id, ar.title, COALESCE(ar.url, ''), COALESCE(ar.author, ''),
modified internal/langdetect/langdetect.go +9 -5
@@ -45,13 +45,17 @@ var knownLanguages = []Language{
4545 {"vi", "Vietnamese"},
4646 }
4747
48-func IsKnown(code string) bool {
48+var knownSet map[string]bool
49+
50+func init() {
51+ knownSet = make(map[string]bool, len(knownLanguages))
4952 for _, l := range knownLanguages {
50- if l.Code == code {
51- return true
52- }
53+ knownSet[l.Code] = true
5354 }
54- return false
55+}
56+
57+func IsKnown(code string) bool {
58+ return knownSet[code]
5559 }
5660
5761 func KnownLanguages() []Language {
@@ -45,13 +45,17 @@ var knownLanguages = []Language{
45 {"vi", "Vietnamese"},45 {"vi", "Vietnamese"},
46 }46 }
47 47
48-func IsKnown(code string) bool {48+var knownSet map[string]bool
49+
50+func init() {
51+ knownSet = make(map[string]bool, len(knownLanguages))
49 for _, l := range knownLanguages {52 for _, l := range knownLanguages {
50- if l.Code == code {53+ knownSet[l.Code] = true
51- return true
52- }
53 }54 }
54- return false55+}
56+
57+func IsKnown(code string) bool {
58+ return knownSet[code]
55 }59 }
56 60
57 func KnownLanguages() []Language {61 func KnownLanguages() []Language {
modified internal/server/feeds_handler.go +8 -13
@@ -2,7 +2,6 @@ package server
22
33 import (
44 "context"
5- "database/sql"
65 "errors"
76 "net/http"
87 "net/url"
@@ -200,11 +199,11 @@ func (s *Server) handleAddFeed(w http.ResponseWriter, r *http.Request) {
200199
201200 f := &db.Feed{
202201 FeedURL: feedURL,
203- Title: nullString(feedTitle),
204- SiteURL: nullString(result.Feed.SiteURL),
205- Description: nullString(result.Feed.Description),
206- FeedType: nullString(result.Feed.Type),
207- FaviconURL: nullString(faviconURL),
202+ Title: db.NullStr(feedTitle),
203+ SiteURL: db.NullStr(result.Feed.SiteURL),
204+ Description: db.NullStr(result.Feed.Description),
205+ FeedType: db.NullStr(result.Feed.Type),
206+ FaviconURL: db.NullStr(faviconURL),
208207 }
209208 if err := s.dbs.Articles.UpsertFeed(r.Context(), f); err != nil {
210209 s.logger.Error("failed to upsert feed", "error", err)
@@ -351,9 +350,9 @@ func (s *Server) handleOPMLUpload(w http.ResponseWriter, r *http.Request) {
351350 for _, fu := range feedURLs {
352351 f := &db.Feed{
353352 FeedURL: fu.URL,
354- Title: nullString(fu.Title),
355- SiteURL: nullString(fu.SiteURL),
356- Description: nullString(fu.Description),
353+ Title: db.NullStr(fu.Title),
354+ SiteURL: db.NullStr(fu.SiteURL),
355+ Description: db.NullStr(fu.Description),
357356 }
358357 if upsertErr := s.dbs.Articles.UpsertFeed(r.Context(), f); upsertErr != nil {
359358 s.logger.Error("failed to upsert feed", "error", upsertErr)
@@ -524,7 +523,3 @@ func (s *Server) handleRetryFeed(w http.ResponseWriter, r *http.Request) {
524523 "DeadFeeds": deadFeeds,
525524 })
526525 }
527-
528-func nullString(s string) sql.NullString {
529- return sql.NullString{String: s, Valid: s != ""}
530-}
@@ -2,7 +2,6 @@ package server
2 2
3 import (3 import (
4 "context"4 "context"
5- "database/sql"
6 "errors"5 "errors"
7 "net/http"6 "net/http"
8 "net/url"7 "net/url"
@@ -200,11 +199,11 @@ func (s *Server) handleAddFeed(w http.ResponseWriter, r *http.Request) {
200 199
201 f := &db.Feed{200 f := &db.Feed{
202 FeedURL: feedURL,201 FeedURL: feedURL,
203- Title: nullString(feedTitle),202+ Title: db.NullStr(feedTitle),
204- SiteURL: nullString(result.Feed.SiteURL),203+ SiteURL: db.NullStr(result.Feed.SiteURL),
205- Description: nullString(result.Feed.Description),204+ Description: db.NullStr(result.Feed.Description),
206- FeedType: nullString(result.Feed.Type),205+ FeedType: db.NullStr(result.Feed.Type),
207- FaviconURL: nullString(faviconURL),206+ FaviconURL: db.NullStr(faviconURL),
208 }207 }
209 if err := s.dbs.Articles.UpsertFeed(r.Context(), f); err != nil {208 if err := s.dbs.Articles.UpsertFeed(r.Context(), f); err != nil {
210 s.logger.Error("failed to upsert feed", "error", err)209 s.logger.Error("failed to upsert feed", "error", err)
@@ -351,9 +350,9 @@ func (s *Server) handleOPMLUpload(w http.ResponseWriter, r *http.Request) {
351 for _, fu := range feedURLs {350 for _, fu := range feedURLs {
352 f := &db.Feed{351 f := &db.Feed{
353 FeedURL: fu.URL,352 FeedURL: fu.URL,
354- Title: nullString(fu.Title),353+ Title: db.NullStr(fu.Title),
355- SiteURL: nullString(fu.SiteURL),354+ SiteURL: db.NullStr(fu.SiteURL),
356- Description: nullString(fu.Description),355+ Description: db.NullStr(fu.Description),
357 }356 }
358 if upsertErr := s.dbs.Articles.UpsertFeed(r.Context(), f); upsertErr != nil {357 if upsertErr := s.dbs.Articles.UpsertFeed(r.Context(), f); upsertErr != nil {
359 s.logger.Error("failed to upsert feed", "error", upsertErr)358 s.logger.Error("failed to upsert feed", "error", upsertErr)
@@ -524,7 +523,3 @@ func (s *Server) handleRetryFeed(w http.ResponseWriter, r *http.Request) {
524 "DeadFeeds": deadFeeds,523 "DeadFeeds": deadFeeds,
525 })524 })
526 }525 }
527-
528-func nullString(s string) sql.NullString {
529- return sql.NullString{String: s, Valid: s != ""}
530-}
modified internal/server/recs_handler.go +11 -43
@@ -5,54 +5,22 @@ import (
55 )
66
77 func (s *Server) handleDismissFeedRecommendation(w http.ResponseWriter, r *http.Request) {
8- user := currentUser(r)
9- feedURL := r.FormValue("feed_url")
10- if feedURL == "" {
11- http.Error(w, "feed_url required", http.StatusBadRequest)
12- return
13- }
14-
15- reason := r.FormValue("reason")
16- if reason == "" {
17- reason = "not_interested"
18- }
19-
20- if err := s.engine.DismissFeed(r.Context(), user.DID, feedURL, reason); err != nil {
21- s.logger.Error("failed to dismiss feed recommendation", "error", err)
22- http.Error(w, err.Error(), http.StatusInternalServerError)
23- return
24- }
25-
26- w.WriteHeader(http.StatusOK)
8+ s.handleDismiss(w, r, "feed_url", "feed")
279 }
2810
2911 func (s *Server) handleDismissArticleRecommendation(w http.ResponseWriter, r *http.Request) {
30- user := currentUser(r)
31- articleURL := r.FormValue("article_url")
32- if articleURL == "" {
33- http.Error(w, "article_url required", http.StatusBadRequest)
34- return
35- }
36-
37- reason := r.FormValue("reason")
38- if reason == "" {
39- reason = "not_interested"
40- }
41-
42- if err := s.engine.DismissArticle(r.Context(), user.DID, articleURL, reason); err != nil {
43- s.logger.Error("failed to dismiss article recommendation", "error", err)
44- http.Error(w, err.Error(), http.StatusInternalServerError)
45- return
46- }
47-
48- w.WriteHeader(http.StatusOK)
12+ s.handleDismiss(w, r, "article_url", "article")
4913 }
5014
5115 func (s *Server) handleDismissPersonRecommendation(w http.ResponseWriter, r *http.Request) {
16+ s.handleDismiss(w, r, "target_did", "person")
17+}
18+
19+func (s *Server) handleDismiss(w http.ResponseWriter, r *http.Request, field, targetType string) {
5220 user := currentUser(r)
53- targetDID := r.FormValue("target_did")
54- if targetDID == "" {
55- http.Error(w, "target_did required", http.StatusBadRequest)
21+ targetID := r.FormValue(field)
22+ if targetID == "" {
23+ http.Error(w, field+" required", http.StatusBadRequest)
5624 return
5725 }
5826
@@ -61,8 +29,8 @@ func (s *Server) handleDismissPersonRecommendation(w http.ResponseWriter, r *htt
6129 reason = "not_interested"
6230 }
6331
64- if err := s.engine.DismissPerson(r.Context(), user.DID, targetDID, reason); err != nil {
65- s.logger.Error("failed to dismiss person recommendation", "error", err)
32+ if err := s.engine.Dismiss(r.Context(), user.DID, targetType, targetID, reason); err != nil {
33+ s.logger.Error("failed to dismiss recommendation", "error", err, "type", targetType)
6634 http.Error(w, err.Error(), http.StatusInternalServerError)
6735 return
6836 }
@@ -5,54 +5,22 @@ import (
5 )5 )
6 6
7 func (s *Server) handleDismissFeedRecommendation(w http.ResponseWriter, r *http.Request) {7 func (s *Server) handleDismissFeedRecommendation(w http.ResponseWriter, r *http.Request) {
8- user := currentUser(r)8+ s.handleDismiss(w, r, "feed_url", "feed")
9- feedURL := r.FormValue("feed_url")
10- if feedURL == "" {
11- http.Error(w, "feed_url required", http.StatusBadRequest)
12- return
13- }
14-
15- reason := r.FormValue("reason")
16- if reason == "" {
17- reason = "not_interested"
18- }
19-
20- if err := s.engine.DismissFeed(r.Context(), user.DID, feedURL, reason); err != nil {
21- s.logger.Error("failed to dismiss feed recommendation", "error", err)
22- http.Error(w, err.Error(), http.StatusInternalServerError)
23- return
24- }
25-
26- w.WriteHeader(http.StatusOK)
27 }9 }
28 10
29 func (s *Server) handleDismissArticleRecommendation(w http.ResponseWriter, r *http.Request) {11 func (s *Server) handleDismissArticleRecommendation(w http.ResponseWriter, r *http.Request) {
30- user := currentUser(r)12+ s.handleDismiss(w, r, "article_url", "article")
31- articleURL := r.FormValue("article_url")
32- if articleURL == "" {
33- http.Error(w, "article_url required", http.StatusBadRequest)
34- return
35- }
36-
37- reason := r.FormValue("reason")
38- if reason == "" {
39- reason = "not_interested"
40- }
41-
42- if err := s.engine.DismissArticle(r.Context(), user.DID, articleURL, reason); err != nil {
43- s.logger.Error("failed to dismiss article recommendation", "error", err)
44- http.Error(w, err.Error(), http.StatusInternalServerError)
45- return
46- }
47-
48- w.WriteHeader(http.StatusOK)
49 }13 }
50 14
51 func (s *Server) handleDismissPersonRecommendation(w http.ResponseWriter, r *http.Request) {15 func (s *Server) handleDismissPersonRecommendation(w http.ResponseWriter, r *http.Request) {
16+ s.handleDismiss(w, r, "target_did", "person")
17+}
18+
19+func (s *Server) handleDismiss(w http.ResponseWriter, r *http.Request, field, targetType string) {
52 user := currentUser(r)20 user := currentUser(r)
53- targetDID := r.FormValue("target_did")21+ targetID := r.FormValue(field)
54- if targetDID == "" {22+ if targetID == "" {
55- http.Error(w, "target_did required", http.StatusBadRequest)23+ http.Error(w, field+" required", http.StatusBadRequest)
56 return24 return
57 }25 }
58 26
@@ -61,8 +29,8 @@ func (s *Server) handleDismissPersonRecommendation(w http.ResponseWriter, r *htt
61 reason = "not_interested"29 reason = "not_interested"
62 }30 }
63 31
64- if err := s.engine.DismissPerson(r.Context(), user.DID, targetDID, reason); err != nil {32+ if err := s.engine.Dismiss(r.Context(), user.DID, targetType, targetID, reason); err != nil {
65- s.logger.Error("failed to dismiss person recommendation", "error", err)33+ s.logger.Error("failed to dismiss recommendation", "error", err, "type", targetType)
66 http.Error(w, err.Error(), http.StatusInternalServerError)34 http.Error(w, err.Error(), http.StatusInternalServerError)
67 return35 return
68 }36 }