mirror of
https://github.com/go-gitea/gitea.git
synced 2026-10-26 06:16:56 +09:00
enhance: allow custom watches to bypass on-mention email restriction (#39652)
Users with "Only Email on Mention" can now still receive email for the events they picked in a custom repository watch, covering issues, pull requests and releases. The setting is renamed to "Only Email on Mention and Custom Watches" to make this explicit, matching GitHub's "Participating, @mentions and custom". "All activity" watches don't bypass the setting, because they are also created automatically by `AUTO_WATCH_NEW_REPOS` for repository creators and team members. To keep picking every event distinct from "All activity", the custom watch dialog now stores a new `WatchModeCustom`. Existing custom watches keep working without a migration. ## Screenshots ### Before <img width="1650" height="675" alt="grafik" src="https://github.com/user-attachments/assets/ecc56e20-cd05-41e9-a840-607b352c50a8" /> ### After <img width="1657" height="671" alt="grafik" src="https://github.com/user-attachments/assets/a406db94-3ac4-4587-b840-0cd472b135c5" /> Addresses https://github.com/openSUSE/openSUSE-git/issues/183 Ref: https://github.com/go-gitea/gitea/issues/35492 --------- Co-authored-by: bircni <bircni@icloud.com> Co-authored-by: Giteabot <teabot@gitea.io>
This commit is contained in:
11 files changed
+123
-22
No files matched your search
@@ -54,7 +54,7 @@ func MailMentionsComment(ctx context.Context, pr *issues_model.PullRequest, c *i
|
||||
ActionType: activities_model.ActionCommentPull,
|
||||
Content: c.Content,
|
||||
Comment: c,
|
||||
}, mentions, visited, true); err != nil {
|
||||
}, mentions, visited, true, nil); err != nil {
|
||||
log.Error("mailIssueCommentBatch: %v", err)
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -66,6 +66,7 @@ func mailIssueCommentToParticipants(ctx context.Context, comment *mailComment, m
|
||||
|
||||
// =========== Repo watchers ===========
|
||||
// Make repo watchers last, since it's likely the list with the most users
|
||||
var customWatchers container.Set[int64]
|
||||
if !(comment.Issue.IsPull && comment.Issue.PullRequest.IsWorkInProgress(ctx) && comment.ActionType != activities_model.ActionCreatePullRequest) {
|
||||
watchType := util.Iif(comment.Issue.IsPull, repo_model.WatchPullRequests, repo_model.WatchIssues)
|
||||
ids, err = repo_model.GetRepoWatchersIDs(ctx, comment.Issue.RepoID, watchType)
|
||||
@@ -73,6 +74,12 @@ func mailIssueCommentToParticipants(ctx context.Context, comment *mailComment, m
|
||||
return fmt.Errorf("GetRepoWatchersIDs(%d): %w", comment.Issue.RepoID, err)
|
||||
}
|
||||
unfiltered = append(ids, unfiltered...)
|
||||
|
||||
ids, err = repo_model.GetRepoCustomWatchersIDs(ctx, comment.Issue.RepoID, watchType)
|
||||
if err != nil {
|
||||
return fmt.Errorf("GetRepoCustomWatchersIDs(%d): %w", comment.Issue.RepoID, err)
|
||||
}
|
||||
customWatchers = container.SetOf(ids...)
|
||||
}
|
||||
|
||||
visited := make(container.Set[int64], len(unfiltered)+len(mentions)+1)
|
||||
@@ -90,7 +97,7 @@ func mailIssueCommentToParticipants(ctx context.Context, comment *mailComment, m
|
||||
}
|
||||
|
||||
// =========== Mentions ===========
|
||||
if err = mailIssueCommentBatch(ctx, comment, mentions, visited, true); err != nil {
|
||||
if err = mailIssueCommentBatch(ctx, comment, mentions, visited, true, nil); err != nil {
|
||||
return fmt.Errorf("mailIssueCommentBatch() mentions: %w", err)
|
||||
}
|
||||
|
||||
@@ -101,18 +108,18 @@ func mailIssueCommentToParticipants(ctx context.Context, comment *mailComment, m
|
||||
}
|
||||
visited.AddMultiple(ids...)
|
||||
|
||||
unfilteredUsers, err := user_model.GetMailableUsersByIDs(ctx, unfiltered, false)
|
||||
unfilteredUsers, err := user_model.GetMailableUsersByIDs(ctx, unfiltered, len(customWatchers) > 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = mailIssueCommentBatch(ctx, comment, unfilteredUsers, visited, false); err != nil {
|
||||
if err = mailIssueCommentBatch(ctx, comment, unfilteredUsers, visited, false, customWatchers); err != nil {
|
||||
return fmt.Errorf("mailIssueCommentBatch(): %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func mailIssueCommentBatch(ctx context.Context, comment *mailComment, users []*user_model.User, visited container.Set[int64], fromMention bool) error {
|
||||
func mailIssueCommentBatch(ctx context.Context, comment *mailComment, users []*user_model.User, visited container.Set[int64], fromMention bool, customWatchers container.Set[int64]) error {
|
||||
checkUnit := unit.TypeIssues
|
||||
if comment.Issue.IsPull {
|
||||
checkUnit = unit.TypePullRequests
|
||||
@@ -124,10 +131,10 @@ func mailIssueCommentBatch(ctx context.Context, comment *mailComment, users []*u
|
||||
continue
|
||||
}
|
||||
// At this point we exclude:
|
||||
// user that don't have all mails enabled or users only get mail on mention and this is one ...
|
||||
// user that don't have all mails enabled or users only get mail on mention or custom watch and this is neither ...
|
||||
if !(user.EmailNotificationsPreference == user_model.EmailNotificationsEnabled ||
|
||||
user.EmailNotificationsPreference == user_model.EmailNotificationsAndYourOwn ||
|
||||
fromMention && user.EmailNotificationsPreference == user_model.EmailNotificationsOnMention) {
|
||||
user.EmailNotificationsPreference == user_model.EmailNotificationsOnMention && (fromMention || customWatchers.Contains(user.ID))) {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unit"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/container"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/markup/markdown"
|
||||
"gitea.dev/modules/setting"
|
||||
@@ -41,7 +42,14 @@ func MailNewRelease(ctx context.Context, rel *repo_model.Release) {
|
||||
return
|
||||
}
|
||||
|
||||
recipients, err := user_model.GetMailableUsersByIDs(ctx, watcherIDList, false)
|
||||
customWatcherIDs, err := repo_model.GetRepoCustomWatchersIDs(ctx, rel.RepoID, repo_model.WatchReleases)
|
||||
if err != nil {
|
||||
log.Error("GetRepoCustomWatchersIDs(%d): %v", rel.RepoID, err)
|
||||
return
|
||||
}
|
||||
customWatchers := container.SetOf(customWatcherIDs...)
|
||||
|
||||
recipients, err := user_model.GetMailableUsersByIDs(ctx, watcherIDList, len(customWatchers) > 0)
|
||||
if err != nil {
|
||||
log.Error("user_model.GetMailableUsersByIDs: %v", err)
|
||||
return
|
||||
@@ -52,9 +60,11 @@ func MailNewRelease(ctx context.Context, rel *repo_model.Release) {
|
||||
return
|
||||
}
|
||||
|
||||
// delete publisher or any users with no permission
|
||||
// delete publisher, on-mention users without a custom watch or any users with no permission
|
||||
recipients = slices.DeleteFunc(recipients, func(u *user_model.User) bool {
|
||||
return u.ID == rel.PublisherID || !access_model.CheckRepoUnitUser(ctx, rel.Repo, u, unit.TypeReleases)
|
||||
return u.ID == rel.PublisherID ||
|
||||
u.EmailNotificationsPreference == user_model.EmailNotificationsOnMention && !customWatchers.Contains(u.ID) ||
|
||||
!access_model.CheckRepoUnitUser(ctx, rel.Repo, u, unit.TypeReleases)
|
||||
})
|
||||
|
||||
langMap := make(map[string][]*user_model.User)
|
||||
|
||||
@@ -61,3 +61,29 @@ func TestMailNewReleaseFiltersUnauthorizedWatchers(t *testing.T) {
|
||||
assert.Equal(t, admin.EmailTo(), sent[0].To)
|
||||
assert.NotEqual(t, unauthorized.EmailTo(), sent[0].To)
|
||||
}
|
||||
|
||||
func TestMailNewReleaseOnMentionCustomWatch(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
defer test.MockVariableValue(&setting.MailService, &setting.Mailer{FromEmail: "noreply@example.com"})()
|
||||
defer mockMailTemplates(string(tplNewReleaseMail), "{{.Subject}}", "<p>{{.Release.TagName}}</p>")()
|
||||
|
||||
// users 4 and 9 only get mail on mention and watch repo 1 with all activity
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
customWatcher := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
||||
allWatcher := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 9})
|
||||
require.NoError(t, repo_model.WatchRepoWithOptions(t.Context(), customWatcher, repo, repo_model.WatchOptions{Mode: repo_model.WatchModeCustom, WatchReleases: true}))
|
||||
|
||||
rel := unittest.AssertExistsAndLoadBean(t, &repo_model.Release{ID: 1, RepoID: repo.ID})
|
||||
rel.Publisher = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: rel.PublisherID})
|
||||
|
||||
var recipients []string
|
||||
defer test.MockVariableValue(&SendAsync, func(msgs ...*sender_service.Message) {
|
||||
for _, msg := range msgs {
|
||||
recipients = append(recipients, msg.To)
|
||||
}
|
||||
})()
|
||||
|
||||
MailNewRelease(t.Context(), rel)
|
||||
assert.Contains(t, recipients, customWatcher.EmailTo())
|
||||
assert.NotContains(t, recipients, allWatcher.EmailTo())
|
||||
}
|
||||
@@ -167,6 +167,28 @@ func TestMailMentionsComment(t *testing.T) {
|
||||
assert.Equal(t, 3, mails)
|
||||
}
|
||||
|
||||
func TestMailParticipantsOnMentionCustomWatch(t *testing.T) {
|
||||
doer, repo, issue, comment := prepareMailerTest(t)
|
||||
comment.Poster = doer
|
||||
defer mockMailTemplates("mail/repo/issue/comment", subjectTpl, bodyTpl)()
|
||||
|
||||
// users 4 and 9 only get mail on mention and watch repo 1 with all activity
|
||||
customWatcher := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
||||
allWatcher := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 9})
|
||||
require.NoError(t, repo_model.WatchRepoWithOptions(t.Context(), customWatcher, repo, repo_model.WatchOptions{Mode: repo_model.WatchModeCustom, WatchPullRequests: true, WatchIssues: true, WatchReleases: true}))
|
||||
|
||||
var recipients []string
|
||||
defer test.MockVariableValue(&SendAsync, func(msgs ...*sender_service.Message) {
|
||||
for _, msg := range msgs {
|
||||
recipients = append(recipients, msg.To)
|
||||
}
|
||||
})()
|
||||
|
||||
require.NoError(t, MailParticipantsComment(t.Context(), comment, activities_model.ActionCommentIssue, issue, nil))
|
||||
assert.Contains(t, recipients, customWatcher.Email)
|
||||
assert.NotContains(t, recipients, allWatcher.Email)
|
||||
}
|
||||
|
||||
func TestMailsSkipBots(t *testing.T) {
|
||||
doer, repo, issue, comment := prepareMailerTest(t)
|
||||
comment.Poster = doer
|
||||
|
||||
Reference in new issue
Block a user