Prevent sending emails and notifications to inactive users (#2384)
* Filter inactive users before sending emails or creating browser notifications Signed-off-by: David Schneiderbauer <dschneiderbauer@gmail.com> * fix formatting issues Signed-off-by: David Schneiderbauer <dschneiderbauer@gmail.com> * included requested changes Signed-off-by: David Schneiderbauer <dschneiderbauer@gmail.com> * optimized database queries * rebasing new master and add tablenames for clarification in xorm queries * remove escaped quotationmarks using backticks Signed-off-by: David Schneiderbauer <dschneiderbauer@gmail.com>
This commit is contained in:
		
							parent
							
								
									b496e3e1cc
								
							
						
					
					
						commit
						d766d0c4e0
					
				
					 13 changed files with 89 additions and 25 deletions
				
			
		|  | @ -9,7 +9,7 @@ | ||||||
|   num_pulls: 2 |   num_pulls: 2 | ||||||
|   num_closed_pulls: 0 |   num_closed_pulls: 0 | ||||||
|   num_milestones: 2 |   num_milestones: 2 | ||||||
|   num_watches: 2 |   num_watches: 3 | ||||||
| 
 | 
 | ||||||
| - | - | ||||||
|   id: 2 |   id: 2 | ||||||
|  |  | ||||||
|  | @ -7,3 +7,8 @@ | ||||||
|   id: 2 |   id: 2 | ||||||
|   user_id: 4 |   user_id: 4 | ||||||
|   repo_id: 1 |   repo_id: 1 | ||||||
|  | 
 | ||||||
|  | - | ||||||
|  |   id: 3 | ||||||
|  |   user_id: 10 | ||||||
|  |   repo_id: 1 | ||||||
|  |  | ||||||
|  | @ -1204,8 +1204,11 @@ func GetParticipantsByIssueID(issueID int64) ([]*User, error) { | ||||||
| func getParticipantsByIssueID(e Engine, issueID int64) ([]*User, error) { | func getParticipantsByIssueID(e Engine, issueID int64) ([]*User, error) { | ||||||
| 	userIDs := make([]int64, 0, 5) | 	userIDs := make([]int64, 0, 5) | ||||||
| 	if err := e.Table("comment").Cols("poster_id"). | 	if err := e.Table("comment").Cols("poster_id"). | ||||||
| 		Where("issue_id = ?", issueID). | 		Where("`comment`.issue_id = ?", issueID). | ||||||
| 		And("type = ?", CommentTypeComment). | 		And("`comment`.type = ?", CommentTypeComment). | ||||||
|  | 		And("`user`.is_active = ?", true). | ||||||
|  | 		And("`user`.prohibit_login = ?", false). | ||||||
|  | 		Join("INNER", "user", "`user`.id = `comment`.poster_id"). | ||||||
| 		Distinct("poster_id"). | 		Distinct("poster_id"). | ||||||
| 		Find(&userIDs); err != nil { | 		Find(&userIDs); err != nil { | ||||||
| 		return nil, fmt.Errorf("get poster IDs: %v", err) | 		return nil, fmt.Errorf("get poster IDs: %v", err) | ||||||
|  |  | ||||||
|  | @ -36,9 +36,13 @@ func mailIssueCommentToParticipants(e Engine, issue *Issue, doer *User, comment | ||||||
| 		return fmt.Errorf("getParticipantsByIssueID [issue_id: %d]: %v", issue.ID, err) | 		return fmt.Errorf("getParticipantsByIssueID [issue_id: %d]: %v", issue.ID, err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// In case the issue poster is not watching the repository,
 | 	// In case the issue poster is not watching the repository and is active,
 | ||||||
| 	// even if we have duplicated in watchers, can be safely filtered out.
 | 	// even if we have duplicated in watchers, can be safely filtered out.
 | ||||||
| 	if issue.PosterID != doer.ID { | 	poster, err := GetUserByID(issue.PosterID) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("GetUserByID [%d]: %v", issue.PosterID, err) | ||||||
|  | 	} | ||||||
|  | 	if issue.PosterID != doer.ID && poster.IsActive && !poster.ProhibitLogin { | ||||||
| 		participants = append(participants, issue.Poster) | 		participants = append(participants, issue.Poster) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -80,7 +80,8 @@ func TestGetParticipantsByIssueID(t *testing.T) { | ||||||
| 	// User 1 is issue1 poster (see fixtures/issue.yml)
 | 	// User 1 is issue1 poster (see fixtures/issue.yml)
 | ||||||
| 	// User 2 only labeled issue1 (see fixtures/comment.yml)
 | 	// User 2 only labeled issue1 (see fixtures/comment.yml)
 | ||||||
| 	// Users 3 and 5 made actual comments (see fixtures/comment.yml)
 | 	// Users 3 and 5 made actual comments (see fixtures/comment.yml)
 | ||||||
| 	checkParticipants(1, []int{3, 5}) | 	// User 3 is inactive, thus not active participant
 | ||||||
|  | 	checkParticipants(1, []int{5}) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func TestIssue_AddLabel(t *testing.T) { | func TestIssue_AddLabel(t *testing.T) { | ||||||
|  |  | ||||||
|  | @ -90,7 +90,10 @@ func GetIssueWatchers(issueID int64) ([]*IssueWatch, error) { | ||||||
| 
 | 
 | ||||||
| func getIssueWatchers(e Engine, issueID int64) (watches []*IssueWatch, err error) { | func getIssueWatchers(e Engine, issueID int64) (watches []*IssueWatch, err error) { | ||||||
| 	err = e. | 	err = e. | ||||||
| 		Where("issue_id = ?", issueID). | 		Where("`issue_watch`.issue_id = ?", issueID). | ||||||
|  | 		And("`user`.is_active = ?", true). | ||||||
|  | 		And("`user`.prohibit_login = ?", false). | ||||||
|  | 		Join("INNER", "user", "`user`.id = `issue_watch`.user_id"). | ||||||
| 		Find(&watches) | 		Find(&watches) | ||||||
| 	return | 	return | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -43,6 +43,11 @@ func TestGetIssueWatchers(t *testing.T) { | ||||||
| 
 | 
 | ||||||
| 	iws, err := GetIssueWatchers(1) | 	iws, err := GetIssueWatchers(1) | ||||||
| 	assert.NoError(t, err) | 	assert.NoError(t, err) | ||||||
|  | 	// Watcher is inactive, thus 0
 | ||||||
|  | 	assert.Equal(t, 0, len(iws)) | ||||||
|  | 
 | ||||||
|  | 	iws, err = GetIssueWatchers(2) | ||||||
|  | 	assert.NoError(t, err) | ||||||
| 	assert.Equal(t, 1, len(iws)) | 	assert.Equal(t, 1, len(iws)) | ||||||
| 
 | 
 | ||||||
| 	iws, err = GetIssueWatchers(5) | 	iws, err = GetIssueWatchers(5) | ||||||
|  |  | ||||||
|  | @ -130,6 +130,8 @@ var migrations = []Migration{ | ||||||
| 	NewMigration("adds time tracking and stopwatches", addTimetracking), | 	NewMigration("adds time tracking and stopwatches", addTimetracking), | ||||||
| 	// v40 -> v41
 | 	// v40 -> v41
 | ||||||
| 	NewMigration("migrate protected branch struct", migrateProtectedBranchStruct), | 	NewMigration("migrate protected branch struct", migrateProtectedBranchStruct), | ||||||
|  | 	// v41 -> v42
 | ||||||
|  | 	NewMigration("add default value to user prohibit_login", addDefaultValueToUserProhibitLogin), | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Migrate database to current version
 | // Migrate database to current version
 | ||||||
|  |  | ||||||
							
								
								
									
										42
									
								
								models/migrations/v41.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								models/migrations/v41.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,42 @@ | ||||||
|  | // Copyright 2017 The Gitea Authors. All rights reserved.
 | ||||||
|  | // Use of this source code is governed by a MIT-style
 | ||||||
|  | // license that can be found in the LICENSE file.
 | ||||||
|  | 
 | ||||||
|  | package migrations | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 
 | ||||||
|  | 	"code.gitea.io/gitea/models" | ||||||
|  | 
 | ||||||
|  | 	"github.com/go-xorm/xorm" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func addDefaultValueToUserProhibitLogin(x *xorm.Engine) (err error) { | ||||||
|  | 	user := &models.User{ | ||||||
|  | 		ProhibitLogin: false, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if _, err := x.Where("`prohibit_login` IS NULL").Cols("prohibit_login").Update(user); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	dialect := x.Dialect().DriverName() | ||||||
|  | 
 | ||||||
|  | 	switch dialect { | ||||||
|  | 	case "mysql": | ||||||
|  | 		_, err = x.Exec("ALTER TABLE user MODIFY `prohibit_login` tinyint(1) NOT NULL DEFAULT 0") | ||||||
|  | 	case "postgres": | ||||||
|  | 		_, err = x.Exec("ALTER TABLE \"user\" ALTER COLUMN `prohibit_login` SET NOT NULL, ALTER COLUMN `prohibit_login` SET DEFAULT false") | ||||||
|  | 	case "mssql": | ||||||
|  | 		// xorm already set DEFAULT 0 for data type BIT in mssql
 | ||||||
|  | 		_, err = x.Exec(`ALTER TABLE [user] ALTER COLUMN "prohibit_login" BIT NOT NULL`) | ||||||
|  | 	case "sqlite3": | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("Error changing user prohibit_login column definition: %v", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return err | ||||||
|  | } | ||||||
|  | @ -16,9 +16,8 @@ func TestCreateOrUpdateIssueNotifications(t *testing.T) { | ||||||
| 
 | 
 | ||||||
| 	assert.NoError(t, CreateOrUpdateIssueNotifications(issue, 2)) | 	assert.NoError(t, CreateOrUpdateIssueNotifications(issue, 2)) | ||||||
| 
 | 
 | ||||||
| 	notf := AssertExistsAndLoadBean(t, &Notification{UserID: 1, IssueID: issue.ID}).(*Notification) | 	// Two watchers are inactive, thus only notification for user 10 is created
 | ||||||
| 	assert.Equal(t, NotificationStatusUnread, notf.Status) | 	notf := AssertExistsAndLoadBean(t, &Notification{UserID: 10, IssueID: issue.ID}).(*Notification) | ||||||
| 	notf = AssertExistsAndLoadBean(t, &Notification{UserID: 4, IssueID: issue.ID}).(*Notification) |  | ||||||
| 	assert.Equal(t, NotificationStatusUnread, notf.Status) | 	assert.Equal(t, NotificationStatusUnread, notf.Status) | ||||||
| 	CheckConsistencyFor(t, &Issue{ID: issue.ID}) | 	CheckConsistencyFor(t, &Issue{ID: issue.ID}) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -51,7 +51,11 @@ func WatchRepo(userID, repoID int64, watch bool) (err error) { | ||||||
| 
 | 
 | ||||||
| func getWatchers(e Engine, repoID int64) ([]*Watch, error) { | func getWatchers(e Engine, repoID int64) ([]*Watch, error) { | ||||||
| 	watches := make([]*Watch, 0, 10) | 	watches := make([]*Watch, 0, 10) | ||||||
| 	return watches, e.Find(&watches, &Watch{RepoID: repoID}) | 	return watches, e.Where("`watch`.repo_id=?", repoID). | ||||||
|  | 		And("`user`.is_active=?", true). | ||||||
|  | 		And("`user`.prohibit_login=?", false). | ||||||
|  | 		Join("INNER", "user", "`user`.id = `watch`.user_id"). | ||||||
|  | 		Find(&watches) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // GetWatchers returns all watchers of given repository.
 | // GetWatchers returns all watchers of given repository.
 | ||||||
|  |  | ||||||
|  | @ -40,7 +40,8 @@ func TestGetWatchers(t *testing.T) { | ||||||
| 	repo := AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository) | 	repo := AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository) | ||||||
| 	watches, err := GetWatchers(repo.ID) | 	watches, err := GetWatchers(repo.ID) | ||||||
| 	assert.NoError(t, err) | 	assert.NoError(t, err) | ||||||
| 	assert.Len(t, watches, repo.NumWatches) | 	// Two watchers are inactive, thus minus 2
 | ||||||
|  | 	assert.Len(t, watches, repo.NumWatches-2) | ||||||
| 	for _, watch := range watches { | 	for _, watch := range watches { | ||||||
| 		assert.EqualValues(t, repo.ID, watch.RepoID) | 		assert.EqualValues(t, repo.ID, watch.RepoID) | ||||||
| 	} | 	} | ||||||
|  | @ -77,22 +78,17 @@ func TestNotifyWatchers(t *testing.T) { | ||||||
| 	} | 	} | ||||||
| 	assert.NoError(t, NotifyWatchers(action)) | 	assert.NoError(t, NotifyWatchers(action)) | ||||||
| 
 | 
 | ||||||
| 	AssertExistsAndLoadBean(t, &Action{ | 	// Two watchers are inactive, thus action is only created for user 8, 10
 | ||||||
| 		ActUserID: action.ActUserID, |  | ||||||
| 		UserID:    1, |  | ||||||
| 		RepoID:    action.RepoID, |  | ||||||
| 		OpType:    action.OpType, |  | ||||||
| 	}) |  | ||||||
| 	AssertExistsAndLoadBean(t, &Action{ |  | ||||||
| 		ActUserID: action.ActUserID, |  | ||||||
| 		UserID:    4, |  | ||||||
| 		RepoID:    action.RepoID, |  | ||||||
| 		OpType:    action.OpType, |  | ||||||
| 	}) |  | ||||||
| 	AssertExistsAndLoadBean(t, &Action{ | 	AssertExistsAndLoadBean(t, &Action{ | ||||||
| 		ActUserID: action.ActUserID, | 		ActUserID: action.ActUserID, | ||||||
| 		UserID:    8, | 		UserID:    8, | ||||||
| 		RepoID:    action.RepoID, | 		RepoID:    action.RepoID, | ||||||
| 		OpType:    action.OpType, | 		OpType:    action.OpType, | ||||||
| 	}) | 	}) | ||||||
|  | 	AssertExistsAndLoadBean(t, &Action{ | ||||||
|  | 		ActUserID: action.ActUserID, | ||||||
|  | 		UserID:    10, | ||||||
|  | 		RepoID:    action.RepoID, | ||||||
|  | 		OpType:    action.OpType, | ||||||
|  | 	}) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -111,7 +111,7 @@ type User struct { | ||||||
| 	AllowGitHook            bool | 	AllowGitHook            bool | ||||||
| 	AllowImportLocal        bool // Allow migrate repository by local path
 | 	AllowImportLocal        bool // Allow migrate repository by local path
 | ||||||
| 	AllowCreateOrganization bool `xorm:"DEFAULT true"` | 	AllowCreateOrganization bool `xorm:"DEFAULT true"` | ||||||
| 	ProhibitLogin           bool | 	ProhibitLogin           bool `xorm:"NOT NULL DEFAULT false"` | ||||||
| 
 | 
 | ||||||
| 	// Avatar
 | 	// Avatar
 | ||||||
| 	Avatar          string `xorm:"VARCHAR(2048) NOT NULL"` | 	Avatar          string `xorm:"VARCHAR(2048) NOT NULL"` | ||||||
|  |  | ||||||
		Loading…
	
		Reference in a new issue