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_closed_pulls: 0 | ||||
|   num_milestones: 2 | ||||
|   num_watches: 2 | ||||
|   num_watches: 3 | ||||
| 
 | ||||
| - | ||||
|   id: 2 | ||||
|  |  | |||
|  | @ -7,3 +7,8 @@ | |||
|   id: 2 | ||||
|   user_id: 4 | ||||
|   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) { | ||||
| 	userIDs := make([]int64, 0, 5) | ||||
| 	if err := e.Table("comment").Cols("poster_id"). | ||||
| 		Where("issue_id = ?", issueID). | ||||
| 		And("type = ?", CommentTypeComment). | ||||
| 		Where("`comment`.issue_id = ?", issueID). | ||||
| 		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"). | ||||
| 		Find(&userIDs); err != nil { | ||||
| 		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) | ||||
| 	} | ||||
| 
 | ||||
| 	// 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.
 | ||||
| 	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) | ||||
| 	} | ||||
| 
 | ||||
|  |  | |||
|  | @ -80,7 +80,8 @@ func TestGetParticipantsByIssueID(t *testing.T) { | |||
| 	// User 1 is issue1 poster (see fixtures/issue.yml)
 | ||||
| 	// User 2 only labeled issue1 (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) { | ||||
|  |  | |||
|  | @ -90,7 +90,10 @@ func GetIssueWatchers(issueID int64) ([]*IssueWatch, error) { | |||
| 
 | ||||
| func getIssueWatchers(e Engine, issueID int64) (watches []*IssueWatch, err error) { | ||||
| 	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) | ||||
| 	return | ||||
| } | ||||
|  |  | |||
|  | @ -43,6 +43,11 @@ func TestGetIssueWatchers(t *testing.T) { | |||
| 
 | ||||
| 	iws, err := GetIssueWatchers(1) | ||||
| 	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)) | ||||
| 
 | ||||
| 	iws, err = GetIssueWatchers(5) | ||||
|  |  | |||
|  | @ -130,6 +130,8 @@ var migrations = []Migration{ | |||
| 	NewMigration("adds time tracking and stopwatches", addTimetracking), | ||||
| 	// v40 -> v41
 | ||||
| 	NewMigration("migrate protected branch struct", migrateProtectedBranchStruct), | ||||
| 	// v41 -> v42
 | ||||
| 	NewMigration("add default value to user prohibit_login", addDefaultValueToUserProhibitLogin), | ||||
| } | ||||
| 
 | ||||
| // 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)) | ||||
| 
 | ||||
| 	notf := AssertExistsAndLoadBean(t, &Notification{UserID: 1, IssueID: issue.ID}).(*Notification) | ||||
| 	assert.Equal(t, NotificationStatusUnread, notf.Status) | ||||
| 	notf = AssertExistsAndLoadBean(t, &Notification{UserID: 4, IssueID: issue.ID}).(*Notification) | ||||
| 	// Two watchers are inactive, thus only notification for user 10 is created
 | ||||
| 	notf := AssertExistsAndLoadBean(t, &Notification{UserID: 10, IssueID: issue.ID}).(*Notification) | ||||
| 	assert.Equal(t, NotificationStatusUnread, notf.Status) | ||||
| 	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) { | ||||
| 	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.
 | ||||
|  |  | |||
|  | @ -40,7 +40,8 @@ func TestGetWatchers(t *testing.T) { | |||
| 	repo := AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository) | ||||
| 	watches, err := GetWatchers(repo.ID) | ||||
| 	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 { | ||||
| 		assert.EqualValues(t, repo.ID, watch.RepoID) | ||||
| 	} | ||||
|  | @ -77,22 +78,17 @@ func TestNotifyWatchers(t *testing.T) { | |||
| 	} | ||||
| 	assert.NoError(t, NotifyWatchers(action)) | ||||
| 
 | ||||
| 	AssertExistsAndLoadBean(t, &Action{ | ||||
| 		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, | ||||
| 	}) | ||||
| 	// Two watchers are inactive, thus action is only created for user 8, 10
 | ||||
| 	AssertExistsAndLoadBean(t, &Action{ | ||||
| 		ActUserID: action.ActUserID, | ||||
| 		UserID:    8, | ||||
| 		RepoID:    action.RepoID, | ||||
| 		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 | ||||
| 	AllowImportLocal        bool // Allow migrate repository by local path
 | ||||
| 	AllowCreateOrganization bool `xorm:"DEFAULT true"` | ||||
| 	ProhibitLogin           bool | ||||
| 	ProhibitLogin           bool `xorm:"NOT NULL DEFAULT false"` | ||||
| 
 | ||||
| 	// Avatar
 | ||||
| 	Avatar          string `xorm:"VARCHAR(2048) NOT NULL"` | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue