From f4f4e18b14c3d772e9183e8f1d2b2df45712c496 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Tue, 14 May 2024 21:47:03 +0800
Subject: [PATCH] Filter out duplicate action(activity) items for a repository
 (#30957)

Fix #20986
---
 models/activities/action.go      | 11 ++++++++++-
 models/activities/action_test.go | 21 +++++++++++++++++++++
 2 files changed, 31 insertions(+), 1 deletion(-)

diff --git a/models/activities/action.go b/models/activities/action.go
index 7e2ef4c9ae..d23f2bd986 100644
--- a/models/activities/action.go
+++ b/models/activities/action.go
@@ -524,7 +524,12 @@ func activityQueryCondition(ctx context.Context, opts GetFeedsOptions) (builder.
 	}
 
 	if opts.RequestedRepo != nil {
-		cond = cond.And(builder.Eq{"repo_id": opts.RequestedRepo.ID})
+		// repo's actions could have duplicate items, see the comment of NotifyWatchers
+		// so here we only filter the "original items", aka: user_id == act_user_id
+		cond = cond.And(
+			builder.Eq{"`action`.repo_id": opts.RequestedRepo.ID},
+			builder.Expr("`action`.user_id = `action`.act_user_id"),
+		)
 	}
 
 	if opts.RequestedTeam != nil {
@@ -577,6 +582,10 @@ func DeleteOldActions(ctx context.Context, olderThan time.Duration) (err error)
 }
 
 // NotifyWatchers creates batch of actions for every watcher.
+// It could insert duplicate actions for a repository action, like this:
+// * Original action: UserID=1 (the real actor), ActUserID=1
+// * Organization action: UserID=100 (the repo's org), ActUserID=1
+// * Watcher action: UserID=20 (a user who is watching a repo), ActUserID=1
 func NotifyWatchers(ctx context.Context, actions ...*Action) error {
 	var watchers []*repo_model.Watch
 	var repo *repo_model.Repository
diff --git a/models/activities/action_test.go b/models/activities/action_test.go
index 5467bd35fb..557415dcda 100644
--- a/models/activities/action_test.go
+++ b/models/activities/action_test.go
@@ -318,3 +318,24 @@ func TestDeleteIssueActions(t *testing.T) {
 	assert.NoError(t, activities_model.DeleteIssueActions(db.DefaultContext, issue.RepoID, issue.ID, issue.Index))
 	unittest.AssertCount(t, &activities_model.Action{}, 0)
 }
+
+func TestRepoActions(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+	_ = db.TruncateBeans(db.DefaultContext, &activities_model.Action{})
+	for i := 0; i < 3; i++ {
+		_ = db.Insert(db.DefaultContext, &activities_model.Action{
+			UserID:    2 + int64(i),
+			ActUserID: 2,
+			RepoID:    repo.ID,
+			OpType:    activities_model.ActionCommentIssue,
+		})
+	}
+	count, _ := db.Count[activities_model.Action](db.DefaultContext, &db.ListOptions{})
+	assert.EqualValues(t, 3, count)
+	actions, _, err := activities_model.GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{
+		RequestedRepo: repo,
+	})
+	assert.NoError(t, err)
+	assert.Len(t, actions, 1)
+}