Handle duplicate likes from PDSUnverified
5c03e2f parent: 1756815 modified
internal/atproto/stream_handler.go +5 -1 | @@ -94,7 +94,7 @@ func (h *StreamDBHandler) handleLike(ctx context.Context, event *Event) error { | ||
| 94 | 94 | } |
| 95 | 95 | |
| 96 | 96 | t, _ := time.Parse(time.RFC3339, rec.CreatedAt) |
| 97 | - return h.db.CreateLike(ctx, &db.Like{ | |
| 97 | + err = h.db.CreateLike(ctx, &db.Like{ | |
| 98 | 98 | URI: event.URI, |
| 99 | 99 | AuthorDID: event.DID, |
| 100 | 100 | FeedURL: rec.FeedURL, |
| @@ -102,6 +102,10 @@ func (h *StreamDBHandler) handleLike(ctx context.Context, event *Event) error { | ||
| 102 | 102 | CreatedAt: sql.NullTime{Time: t, Valid: true}, |
| 103 | 103 | CID: sql.NullString{String: event.CID, Valid: event.CID != ""}, |
| 104 | 104 | }) |
| 105 | + if errors.Is(err, db.ErrDuplicateLike) { | |
| 106 | + return nil | |
| 107 | + } | |
| 108 | + return err | |
| 105 | 109 | |
| 106 | 110 | case "delete": |
| 107 | 111 | return h.db.DeleteLike(ctx, event.URI) |
| @@ -94,7 +94,7 @@ func (h *StreamDBHandler) handleLike(ctx context.Context, event *Event) error { | |||
| 94 | } | 94 | } |
| 95 | 95 | ||
| 96 | t, _ := time.Parse(time.RFC3339, rec.CreatedAt) | 96 | t, _ := time.Parse(time.RFC3339, rec.CreatedAt) |
| 97 | - return h.db.CreateLike(ctx, &db.Like{ | 97 | + err = h.db.CreateLike(ctx, &db.Like{ |
| 98 | URI: event.URI, | 98 | URI: event.URI, |
| 99 | AuthorDID: event.DID, | 99 | AuthorDID: event.DID, |
| 100 | FeedURL: rec.FeedURL, | 100 | FeedURL: rec.FeedURL, |
| @@ -102,6 +102,10 @@ func (h *StreamDBHandler) handleLike(ctx context.Context, event *Event) error { | |||
| 102 | CreatedAt: sql.NullTime{Time: t, Valid: true}, | 102 | CreatedAt: sql.NullTime{Time: t, Valid: true}, |
| 103 | CID: sql.NullString{String: event.CID, Valid: event.CID != ""}, | 103 | CID: sql.NullString{String: event.CID, Valid: event.CID != ""}, |
| 104 | }) | 104 | }) |
| 105 | + if errors.Is(err, db.ErrDuplicateLike) { | ||
| 106 | + return nil | ||
| 107 | + } | ||
| 108 | + return err | ||
| 105 | 109 | ||
| 106 | case "delete": | 110 | case "delete": |
| 107 | return h.db.DeleteLike(ctx, event.URI) | 111 | return h.db.DeleteLike(ctx, event.URI) |
modified
internal/atproto/sync.go +5 -1 | @@ -132,7 +132,11 @@ func (s *Sync) reconcileLike(ctx context.Context, userDID, uri, cid string, valu | ||
| 132 | 132 | CreatedAt: db.NullTime(t), |
| 133 | 133 | CID: db.NullStr(cid), |
| 134 | 134 | } |
| 135 | - return s.db.CreateLike(ctx, like) | |
| 135 | + err = s.db.CreateLike(ctx, like) | |
| 136 | + if errors.Is(err, db.ErrDuplicateLike) { | |
| 137 | + return nil | |
| 138 | + } | |
| 139 | + return err | |
| 136 | 140 | } |
| 137 | 141 | |
| 138 | 142 | func (s *Sync) reconcileAnnotation(ctx context.Context, userDID, uri, cid string, value json.RawMessage) error { |
| @@ -132,7 +132,11 @@ func (s *Sync) reconcileLike(ctx context.Context, userDID, uri, cid string, valu | |||
| 132 | CreatedAt: db.NullTime(t), | 132 | CreatedAt: db.NullTime(t), |
| 133 | CID: db.NullStr(cid), | 133 | CID: db.NullStr(cid), |
| 134 | } | 134 | } |
| 135 | - return s.db.CreateLike(ctx, like) | 135 | + err = s.db.CreateLike(ctx, like) |
| 136 | + if errors.Is(err, db.ErrDuplicateLike) { | ||
| 137 | + return nil | ||
| 138 | + } | ||
| 139 | + return err | ||
| 136 | } | 140 | } |
| 137 | 141 | ||
| 138 | func (s *Sync) reconcileAnnotation(ctx context.Context, userDID, uri, cid string, value json.RawMessage) error { | 142 | func (s *Sync) reconcileAnnotation(ctx context.Context, userDID, uri, cid string, value json.RawMessage) error { |
modified
internal/db/social.go +13 -3 | @@ -3,9 +3,12 @@ package db | ||
| 3 | 3 | import ( |
| 4 | 4 | "context" |
| 5 | 5 | "database/sql" |
| 6 | + "errors" | |
| 6 | 7 | "strings" |
| 7 | 8 | ) |
| 8 | 9 | |
| 10 | +var ErrDuplicateLike = errors.New("already liked this article") | |
| 11 | + | |
| 9 | 12 | type Annotation struct { |
| 10 | 13 | ID int64 |
| 11 | 14 | URI string |
| @@ -116,11 +119,18 @@ func (db *DB) ListAnnotations(ctx context.Context, feedURL, articleURL, authorDI | ||
| 116 | 119 | } |
| 117 | 120 | |
| 118 | 121 | func (db *DB) CreateLike(ctx context.Context, l *Like) error { |
| 119 | - _, err := db.ExecContext(ctx, ` | |
| 120 | - INSERT INTO likes (uri, author_did, feed_url, article_url, created_at, cid) | |
| 122 | + result, err := db.ExecContext(ctx, ` | |
| 123 | + INSERT OR IGNORE INTO likes (uri, author_did, feed_url, article_url, created_at, cid) | |
| 121 | 124 | VALUES (?, ?, ?, ?, ?, ?) |
| 122 | 125 | `, l.URI, l.AuthorDID, l.FeedURL, l.ArticleURL, l.CreatedAt, l.CID) |
| 123 | - return err | |
| 126 | + if err != nil { | |
| 127 | + return err | |
| 128 | + } | |
| 129 | + n, _ := result.RowsAffected() | |
| 130 | + if n == 0 { | |
| 131 | + return ErrDuplicateLike | |
| 132 | + } | |
| 133 | + return nil | |
| 124 | 134 | } |
| 125 | 135 | |
| 126 | 136 | func (db *DB) DeleteLike(ctx context.Context, uri string) error { |
| @@ -3,9 +3,12 @@ package db | |||
| 3 | import ( | 3 | import ( |
| 4 | "context" | 4 | "context" |
| 5 | "database/sql" | 5 | "database/sql" |
| 6 | + "errors" | ||
| 6 | "strings" | 7 | "strings" |
| 7 | ) | 8 | ) |
| 8 | 9 | ||
| 10 | +var ErrDuplicateLike = errors.New("already liked this article") | ||
| 11 | + | ||
| 9 | type Annotation struct { | 12 | type Annotation struct { |
| 10 | ID int64 | 13 | ID int64 |
| 11 | URI string | 14 | URI string |
| @@ -116,11 +119,18 @@ func (db *DB) ListAnnotations(ctx context.Context, feedURL, articleURL, authorDI | |||
| 116 | } | 119 | } |
| 117 | 120 | ||
| 118 | func (db *DB) CreateLike(ctx context.Context, l *Like) error { | 121 | func (db *DB) CreateLike(ctx context.Context, l *Like) error { |
| 119 | - _, err := db.ExecContext(ctx, ` | 122 | + result, err := db.ExecContext(ctx, ` |
| 120 | - INSERT INTO likes (uri, author_did, feed_url, article_url, created_at, cid) | 123 | + INSERT OR IGNORE INTO likes (uri, author_did, feed_url, article_url, created_at, cid) |
| 121 | VALUES (?, ?, ?, ?, ?, ?) | 124 | VALUES (?, ?, ?, ?, ?, ?) |
| 122 | `, l.URI, l.AuthorDID, l.FeedURL, l.ArticleURL, l.CreatedAt, l.CID) | 125 | `, l.URI, l.AuthorDID, l.FeedURL, l.ArticleURL, l.CreatedAt, l.CID) |
| 123 | - return err | 126 | + if err != nil { |
| 127 | + return err | ||
| 128 | + } | ||
| 129 | + n, _ := result.RowsAffected() | ||
| 130 | + if n == 0 { | ||
| 131 | + return ErrDuplicateLike | ||
| 132 | + } | ||
| 133 | + return nil | ||
| 124 | } | 134 | } |
| 125 | 135 | ||
| 126 | func (db *DB) DeleteLike(ctx context.Context, uri string) error { | 136 | func (db *DB) DeleteLike(ctx context.Context, uri string) error { |
modified
internal/server/articles_handler.go +3 -2 | @@ -2,6 +2,7 @@ package server | ||
| 2 | 2 | |
| 3 | 3 | import ( |
| 4 | 4 | "database/sql" |
| 5 | + "errors" | |
| 5 | 6 | "fmt" |
| 6 | 7 | "net/http" |
| 7 | 8 | "strconv" |
| @@ -244,7 +245,7 @@ func (s *Server) handleLikeArticle(w http.ResponseWriter, r *http.Request) { | ||
| 244 | 245 | ArticleURL: article.URL.String, |
| 245 | 246 | CreatedAt: sql.NullTime{Time: time.Now(), Valid: true}, |
| 246 | 247 | } |
| 247 | - if err := s.db.CreateLike(r.Context(), like); err != nil { | |
| 248 | + if err := s.db.CreateLike(r.Context(), like); err != nil && !errors.Is(err, db.ErrDuplicateLike) { | |
| 248 | 249 | http.Error(w, err.Error(), http.StatusInternalServerError) |
| 249 | 250 | return |
| 250 | 251 | } |
| @@ -256,7 +257,7 @@ func (s *Server) handleLikeArticle(w http.ResponseWriter, r *http.Request) { | ||
| 256 | 257 | ArticleURL: article.URL.String, |
| 257 | 258 | CreatedAt: sql.NullTime{Time: time.Now(), Valid: true}, |
| 258 | 259 | } |
| 259 | - if err := s.db.CreateLike(r.Context(), like); err != nil { | |
| 260 | + if err := s.db.CreateLike(r.Context(), like); err != nil && !errors.Is(err, db.ErrDuplicateLike) { | |
| 260 | 261 | http.Error(w, err.Error(), http.StatusInternalServerError) |
| 261 | 262 | return |
| 262 | 263 | } |
| @@ -2,6 +2,7 @@ package server | |||
| 2 | 2 | ||
| 3 | import ( | 3 | import ( |
| 4 | "database/sql" | 4 | "database/sql" |
| 5 | + "errors" | ||
| 5 | "fmt" | 6 | "fmt" |
| 6 | "net/http" | 7 | "net/http" |
| 7 | "strconv" | 8 | "strconv" |
| @@ -244,7 +245,7 @@ func (s *Server) handleLikeArticle(w http.ResponseWriter, r *http.Request) { | |||
| 244 | ArticleURL: article.URL.String, | 245 | ArticleURL: article.URL.String, |
| 245 | CreatedAt: sql.NullTime{Time: time.Now(), Valid: true}, | 246 | CreatedAt: sql.NullTime{Time: time.Now(), Valid: true}, |
| 246 | } | 247 | } |
| 247 | - if err := s.db.CreateLike(r.Context(), like); err != nil { | 248 | + if err := s.db.CreateLike(r.Context(), like); err != nil && !errors.Is(err, db.ErrDuplicateLike) { |
| 248 | http.Error(w, err.Error(), http.StatusInternalServerError) | 249 | http.Error(w, err.Error(), http.StatusInternalServerError) |
| 249 | return | 250 | return |
| 250 | } | 251 | } |
| @@ -256,7 +257,7 @@ func (s *Server) handleLikeArticle(w http.ResponseWriter, r *http.Request) { | |||
| 256 | ArticleURL: article.URL.String, | 257 | ArticleURL: article.URL.String, |
| 257 | CreatedAt: sql.NullTime{Time: time.Now(), Valid: true}, | 258 | CreatedAt: sql.NullTime{Time: time.Now(), Valid: true}, |
| 258 | } | 259 | } |
| 259 | - if err := s.db.CreateLike(r.Context(), like); err != nil { | 260 | + if err := s.db.CreateLike(r.Context(), like); err != nil && !errors.Is(err, db.ErrDuplicateLike) { |
| 260 | http.Error(w, err.Error(), http.StatusInternalServerError) | 261 | http.Error(w, err.Error(), http.StatusInternalServerError) |
| 261 | return | 262 | return |
| 262 | } | 263 | } |