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>
tokarchuk/v1.17
David Schneiderbauer 7 years ago committed by Lunny Xiao
parent b496e3e1cc
commit d766d0c4e0
  1. 2
      models/fixtures/repository.yml
  2. 5
      models/fixtures/watch.yml
  3. 7
      models/issue.go
  4. 8
      models/issue_mail.go
  5. 3
      models/issue_test.go
  6. 5
      models/issue_watch.go
  7. 5
      models/issue_watch_test.go
  8. 2
      models/migrations/migrations.go
  9. 42
      models/migrations/v41.go
  10. 5
      models/notification_test.go
  11. 6
      models/repo_watch.go
  12. 14
      models/repo_watch_test.go
  13. 2
      models/user.go

@ -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

@ -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,21 +78,16 @@ func TestNotifyWatchers(t *testing.T) {
} }
assert.NoError(t, NotifyWatchers(action)) assert.NoError(t, NotifyWatchers(action))
// Two watchers are inactive, thus action is only created for user 8, 10
AssertExistsAndLoadBean(t, &Action{ AssertExistsAndLoadBean(t, &Action{
ActUserID: action.ActUserID, ActUserID: action.ActUserID,
UserID: 1, UserID: 8,
RepoID: action.RepoID,
OpType: action.OpType,
})
AssertExistsAndLoadBean(t, &Action{
ActUserID: action.ActUserID,
UserID: 4,
RepoID: action.RepoID, RepoID: action.RepoID,
OpType: action.OpType, OpType: action.OpType,
}) })
AssertExistsAndLoadBean(t, &Action{ AssertExistsAndLoadBean(t, &Action{
ActUserID: action.ActUserID, ActUserID: action.ActUserID,
UserID: 8, UserID: 10,
RepoID: action.RepoID, RepoID: action.RepoID,
OpType: action.OpType, 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…
Cancel
Save