gitforge/services/mailer/mail_release.go
forgejo-backport-action 30b5ea66bc [v16.0/forgejo] feat: granular settings for watched repo units (#13385)
**Backport:** https://codeberg.org/forgejo/forgejo/pulls/10927

Continuation of #10598
Closes #7254

Implement repo granular watch selection

I adopted a lot of the frontend from #10598 while redoing the entire back-end. I propose this new model:
- Remove the WatchMode enum.
- Add the WatchSelection struct, which represents the granular selection. Notice that there is no `watching` bool. I tried very hard to keep the structure as simple and redundancy-free as possible. Therefore, people not watching a repo at all either don't have a watch record or one with an entirely unselected WatchSelection struct.
- Add the WatchSource enum. It replaces the WatchMode enum and has a single purpose: determine whether a watch was explicitly or automatically initiated.

Notice that replacing this
```go
		And("`watch`.mode<>?", WatchModeDont).
```
with this is correct:
```go
		And(
			builder.Or(
				builder.Eq{"`watch`.watch_selection_issues": true},
				builder.Eq{"`watch`.watch_selection_pull_requests": true},
				builder.Eq{"`watch`.watch_selection_releases": true},
			),
		).
```
That's because there are four modes: dont, none, auto and normal. When `<>` with dont, we look for auto and normal, because there are no records with none. Therefore, the old code looks for records that indicate watching. The code I replaced this with does so, too, just more granular.

Also notice that I've prepared a future `user preset` in a few places. See below for a little more info on that.

## Next PR
I plan to continue working on this. I want to implement a `user preset` option. The user sets that `user preset` in her settings and may use them in any repo.
<details>
- rename account settings to account and notifications
- user preset (always use this preset for newly accessible repos (according to AutoWatchOnChanges and AutoWatchNewRepos))
</details>

## Further PRs
- make api able to granular watch
- move (email) notifications to new notifications tab

Co-authored-by: 0ko <0ko@noreply.codeberg.org>
Co-authored-by: Gusted <postmaster@gusted.xyz>
Co-authored-by: pat-s <patrick.schratz@gmail.com>
Reviewed-on: #10927
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
(cherry picked from commit 8770ffc848)
2026-07-09 20:56:52 +02:00

107 lines
3 KiB
Go

// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package mailer
import (
"bytes"
"context"
"slices"
access_model "forgejo.org/models/perm/access"
repo_model "forgejo.org/models/repo"
"forgejo.org/models/unit"
user_model "forgejo.org/models/user"
"forgejo.org/modules/base"
"forgejo.org/modules/log"
"forgejo.org/modules/markup"
"forgejo.org/modules/markup/markdown"
"forgejo.org/modules/setting"
"forgejo.org/modules/translation"
)
const (
tplNewReleaseMail base.TplName = "release"
)
// MailNewRelease send new release notify to all repo watchers.
func MailNewRelease(ctx context.Context, rel *repo_model.Release) {
if setting.MailService == nil {
// No mail service configured
return
}
watcherIDList, err := repo_model.GetSelectWatcherIDs(ctx, rel.RepoID, repo_model.WatchSelection{Issues: false, PullRequests: false, Releases: true})
if err != nil {
log.Error("GetRepoWatchersIDs(%d): %v", rel.RepoID, err)
return
}
recipients, err := user_model.GetMaileableUsersByIDs(ctx, watcherIDList, false)
if err != nil {
log.Error("user_model.GetMaileableUsersByIDs: %v", err)
return
}
// Users are not eligible to receive this mail if they are not active or
// they don't have permissions to read releases.
recipients = slices.DeleteFunc(recipients, func(u *user_model.User) bool {
return !u.IsActive || !access_model.CheckRepoUnitUser(ctx, rel.Repo, u, unit.TypeReleases)
})
langMap := make(map[string][]*user_model.User)
for _, user := range recipients {
if user.ID != rel.PublisherID {
langMap[user.Language] = append(langMap[user.Language], user)
}
}
for lang, tos := range langMap {
mailNewRelease(ctx, lang, tos, rel)
}
}
func mailNewRelease(ctx context.Context, lang string, tos []*user_model.User, rel *repo_model.Release) {
locale := translation.NewLocale(lang)
var err error
rel.RenderedNote, err = markdown.RenderString(&markup.RenderContext{
Ctx: ctx,
Links: markup.Links{
Base: rel.Repo.HTMLURL(),
},
Metas: rel.Repo.ComposeMetas(ctx),
}, rel.Note)
if err != nil {
log.Error("markdown.RenderString(%d): %v", rel.RepoID, err)
return
}
subject := locale.TrString("mail.release.new.subject", rel.TagName, rel.Repo.FullName())
mailMeta := map[string]any{
"locale": locale,
"Release": rel,
"Subject": subject,
"Language": locale.Language(),
"Link": rel.HTMLURL(),
}
var mailBody bytes.Buffer
if err := bodyTemplates.ExecuteTemplate(&mailBody, string(tplNewReleaseMail), mailMeta); err != nil {
log.Error("ExecuteTemplate [%s]: %v", string(tplNewReleaseMail)+"/body", err)
return
}
msgs := make([]*Message, 0, len(tos))
publisherName := fromDisplayName(rel.Publisher)
msgID := createMessageIDForRelease(rel)
for _, to := range tos {
msg := NewMessageFrom(to.EmailTo(), publisherName, setting.MailService.FromEmail, subject, mailBody.String())
msg.Info = subject
msg.SetHeader("Message-ID", msgID)
msgs = append(msgs, msg)
}
SendAsync(msgs...)
}