diff --git a/models/activities/notification.go b/models/activities/notification.go index 6dde26fd53e..42af9502ccf 100644 --- a/models/activities/notification.go +++ b/models/activities/notification.go @@ -11,7 +11,6 @@ import ( "code.gitea.io/gitea/models/db" issues_model "code.gitea.io/gitea/models/issues" - "code.gitea.io/gitea/models/organization" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/setting" @@ -46,6 +45,8 @@ const ( NotificationSourceCommit // NotificationSourceRepository is a notification for a repository NotificationSourceRepository + // NotificationSourceRelease is a notification for a release + NotificationSourceRelease ) // Notification represents a notification @@ -60,6 +61,7 @@ type Notification struct { IssueID int64 `xorm:"NOT NULL"` CommitID string CommentID int64 + ReleaseID int64 UpdatedBy int64 `xorm:"NOT NULL"` @@ -67,6 +69,7 @@ type Notification struct { Repository *repo_model.Repository `xorm:"-"` Comment *issues_model.Comment `xorm:"-"` User *user_model.User `xorm:"-"` + Release *repo_model.Release `xorm:"-"` CreatedUnix timeutil.TimeStamp `xorm:"created NOT NULL"` UpdatedUnix timeutil.TimeStamp `xorm:"updated NOT NULL"` @@ -104,6 +107,10 @@ func (n *Notification) TableIndices() []*schemas.Index { commitIDIndex.AddColumn("commit_id") indices = append(indices, commitIDIndex) + releaseIDIndex := schemas.NewIndex("idx_notification_release_id", schemas.IndexType) + releaseIDIndex.AddColumn("release_id") + indices = append(indices, releaseIDIndex) + updatedByIndex := schemas.NewIndex("idx_notification_updated_by", schemas.IndexType) updatedByIndex.AddColumn("updated_by") indices = append(indices, updatedByIndex) @@ -116,36 +123,53 @@ func init() { } // CreateRepoTransferNotification creates notification for the user a repository was transferred to -func CreateRepoTransferNotification(ctx context.Context, doer, newOwner *user_model.User, repo *repo_model.Repository) error { - return db.WithTx(ctx, func(ctx context.Context) error { - var notify []*Notification +func CreateRepoTransferNotification(ctx context.Context, doerID, repoID, receiverID int64) error { + notify := &Notification{ + UserID: receiverID, + RepoID: repoID, + Status: NotificationStatusUnread, + UpdatedBy: doerID, + Source: NotificationSourceRepository, + } + return db.Insert(ctx, notify) +} - if newOwner.IsOrganization() { - users, err := organization.GetUsersWhoCanCreateOrgRepo(ctx, newOwner.ID) - if err != nil || len(users) == 0 { - return err - } - for i := range users { - notify = append(notify, &Notification{ - UserID: i, - RepoID: repo.ID, - Status: NotificationStatusUnread, - UpdatedBy: doer.ID, - Source: NotificationSourceRepository, - }) - } - } else { - notify = []*Notification{{ - UserID: newOwner.ID, - RepoID: repo.ID, - Status: NotificationStatusUnread, - UpdatedBy: doer.ID, - Source: NotificationSourceRepository, - }} - } +func CreateCommitNotifications(ctx context.Context, doerID, repoID int64, commitID string, receiverID int64) error { + notification := &Notification{ + Source: NotificationSourceCommit, + UserID: receiverID, + RepoID: repoID, + CommitID: commitID, + Status: NotificationStatusUnread, + UpdatedBy: doerID, + } - return db.Insert(ctx, notify) - }) + return db.Insert(ctx, notification) +} + +func CreateOrUpdateReleaseNotifications(ctx context.Context, doerID, releaseID, receiverID int64) error { + notification := new(Notification) + if _, err := db.GetEngine(ctx). + Where("user_id = ?", receiverID). + And("release_id = ?", releaseID). + Get(notification); err != nil { + return err + } + if notification.ID > 0 { + notification.Status = NotificationStatusUnread + notification.UpdatedBy = doerID + _, err := db.GetEngine(ctx).ID(notification.ID).Cols("status", "updated_by").Update(notification) + return err + } + + notification = &Notification{ + Source: NotificationSourceRelease, + UserID: receiverID, + Status: NotificationStatusUnread, + ReleaseID: releaseID, + UpdatedBy: doerID, + } + return db.Insert(ctx, notification) } func createIssueNotification(ctx context.Context, userID int64, issue *issues_model.Issue, commentID, updatedByID int64) error { @@ -213,6 +237,9 @@ func (n *Notification) LoadAttributes(ctx context.Context) (err error) { if err = n.loadComment(ctx); err != nil { return err } + if err = n.loadRelease(ctx); err != nil { + return err + } return err } @@ -253,6 +280,16 @@ func (n *Notification) loadComment(ctx context.Context) (err error) { return nil } +func (n *Notification) loadRelease(ctx context.Context) (err error) { + if n.Release == nil && n.ReleaseID != 0 { + n.Release, err = repo_model.GetReleaseByID(ctx, n.ReleaseID) + if err != nil { + return fmt.Errorf("GetReleaseByID [%d]: %w", n.ReleaseID, err) + } + } + return nil +} + func (n *Notification) loadUser(ctx context.Context) (err error) { if n.User == nil { n.User, err = user_model.GetUserByID(ctx, n.UserID) @@ -285,6 +322,8 @@ func (n *Notification) HTMLURL(ctx context.Context) string { return n.Repository.HTMLURL() + "/commit/" + url.PathEscape(n.CommitID) case NotificationSourceRepository: return n.Repository.HTMLURL() + case NotificationSourceRelease: + return n.Release.HTMLURL() } return "" } @@ -301,6 +340,8 @@ func (n *Notification) Link(ctx context.Context) string { return n.Repository.Link() + "/commit/" + url.PathEscape(n.CommitID) case NotificationSourceRepository: return n.Repository.Link() + case NotificationSourceRelease: + return n.Release.Link() } return "" } @@ -373,6 +414,17 @@ func SetRepoReadBy(ctx context.Context, userID, repoID int64) error { return err } +// SetReleaseReadBy sets issue to be read by given user. +func SetReleaseReadBy(ctx context.Context, releaseID, userID int64) error { + _, err := db.GetEngine(ctx).Where(builder.Eq{ + "user_id": userID, + "status": NotificationStatusUnread, + "source": NotificationSourceRelease, + "release_id": releaseID, + }).Cols("status").Update(&Notification{Status: NotificationStatusRead}) + return err +} + // SetNotificationStatus change the notification status func SetNotificationStatus(ctx context.Context, notificationID int64, user *user_model.User, status NotificationStatus) (*Notification, error) { notification, err := GetNotificationByID(ctx, notificationID) diff --git a/models/activities/notification_list.go b/models/activities/notification_list.go index b47f5dc4041..732a76feca8 100644 --- a/models/activities/notification_list.go +++ b/models/activities/notification_list.go @@ -25,6 +25,7 @@ type FindNotificationOptions struct { UserID int64 RepoID int64 IssueID int64 + ReleaseID int64 Status []NotificationStatus Source []NotificationSource UpdatedAfterUnix int64 @@ -43,6 +44,9 @@ func (opts FindNotificationOptions) ToConds() builder.Cond { if opts.IssueID != 0 { cond = cond.And(builder.Eq{"notification.issue_id": opts.IssueID}) } + if opts.ReleaseID != 0 { + cond = cond.And(builder.Eq{"notification.release_id": opts.ReleaseID}) + } if len(opts.Status) > 0 { if len(opts.Status) == 1 { cond = cond.And(builder.Eq{"notification.status": opts.Status[0]}) @@ -70,17 +74,9 @@ func (opts FindNotificationOptions) ToOrders() string { // for each watcher, or updates it if already exists // receiverID > 0 just send to receiver, else send to all watcher func CreateOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, notificationAuthorID, receiverID int64) error { - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return err - } - defer committer.Close() - - if err := createOrUpdateIssueNotifications(ctx, issueID, commentID, notificationAuthorID, receiverID); err != nil { - return err - } - - return committer.Commit() + return db.WithTx(ctx, func(ctx context.Context) error { + return createOrUpdateIssueNotifications(ctx, issueID, commentID, notificationAuthorID, receiverID) + }) } func createOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, notificationAuthorID, receiverID int64) error { @@ -186,6 +182,9 @@ func (nl NotificationList) LoadAttributes(ctx context.Context) error { if _, err := nl.LoadComments(ctx); err != nil { return err } + if _, err := nl.LoadReleases(ctx); err != nil { + return err + } return nil } @@ -458,6 +457,31 @@ func (nl NotificationList) LoadComments(ctx context.Context) ([]int, error) { return failures, nil } +func (nl NotificationList) LoadReleases(ctx context.Context) ([]int, error) { + if len(nl) == 0 { + return []int{}, nil + } + + releaseIDs := nl.getPendingCommentIDs() + releases := make(map[int64]*repo_model.Release, len(releaseIDs)) + if err := db.GetEngine(ctx).In("id", releaseIDs).Find(&releases); err != nil { + return nil, err + } + + failures := []int{} + for i, notification := range nl { + if notification.ReleaseID > 0 && notification.Release == nil && releases[notification.ReleaseID] != nil { + notification.Release = releases[notification.ReleaseID] + if notification.Release == nil { + log.Error("Notification[%d]: ReleaseID[%d] failed to load", notification.ID, notification.ReleaseID) + failures = append(failures, i) + continue + } + } + } + return failures, nil +} + // LoadIssuePullRequests loads all issues' pull requests if possible func (nl NotificationList) LoadIssuePullRequests(ctx context.Context) error { issues := make(map[int64]*issues_model.Issue, len(nl)) diff --git a/models/migrations/v1_25/v321.go b/models/migrations/v1_25/v321.go new file mode 100644 index 00000000000..781174b1ba0 --- /dev/null +++ b/models/migrations/v1_25/v321.go @@ -0,0 +1,81 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_25 //nolint + +import ( + "code.gitea.io/gitea/modules/timeutil" + "xorm.io/xorm" + "xorm.io/xorm/schemas" +) + +// NotificationV321 represents a notification +type NotificationV321 struct { + ID int64 `xorm:"pk autoincr"` + UserID int64 `xorm:"NOT NULL"` + RepoID int64 `xorm:"NOT NULL"` + + Status uint8 `xorm:"SMALLINT NOT NULL"` + Source uint8 `xorm:"SMALLINT NOT NULL"` + + IssueID int64 `xorm:"NOT NULL"` + CommitID string + CommentID int64 + ReleaseID int64 + + UpdatedBy int64 `xorm:"NOT NULL"` + + CreatedUnix timeutil.TimeStamp `xorm:"created NOT NULL"` + UpdatedUnix timeutil.TimeStamp `xorm:"updated NOT NULL"` +} + +func (n *NotificationV321) TableName() string { + return "notification" +} + +// TableIndices implements xorm's TableIndices interface +func (n *NotificationV321) TableIndices() []*schemas.Index { + indices := make([]*schemas.Index, 0, 8) + usuuIndex := schemas.NewIndex("u_s_uu", schemas.IndexType) + usuuIndex.AddColumn("user_id", "status", "updated_unix") + indices = append(indices, usuuIndex) + + // Add the individual indices that were previously defined in struct tags + userIDIndex := schemas.NewIndex("idx_notification_user_id", schemas.IndexType) + userIDIndex.AddColumn("user_id") + indices = append(indices, userIDIndex) + + repoIDIndex := schemas.NewIndex("idx_notification_repo_id", schemas.IndexType) + repoIDIndex.AddColumn("repo_id") + indices = append(indices, repoIDIndex) + + statusIndex := schemas.NewIndex("idx_notification_status", schemas.IndexType) + statusIndex.AddColumn("status") + indices = append(indices, statusIndex) + + sourceIndex := schemas.NewIndex("idx_notification_source", schemas.IndexType) + sourceIndex.AddColumn("source") + indices = append(indices, sourceIndex) + + issueIDIndex := schemas.NewIndex("idx_notification_issue_id", schemas.IndexType) + issueIDIndex.AddColumn("issue_id") + indices = append(indices, issueIDIndex) + + commitIDIndex := schemas.NewIndex("idx_notification_commit_id", schemas.IndexType) + commitIDIndex.AddColumn("commit_id") + indices = append(indices, commitIDIndex) + + releaseIDIndex := schemas.NewIndex("idx_notification_release_id", schemas.IndexType) + releaseIDIndex.AddColumn("release_id") + indices = append(indices, releaseIDIndex) + + updatedByIndex := schemas.NewIndex("idx_notification_updated_by", schemas.IndexType) + updatedByIndex.AddColumn("updated_by") + indices = append(indices, updatedByIndex) + + return indices +} + +func AddReleaseNotification(x *xorm.Engine) error { + return x.Sync(new(NotificationV321)) +} diff --git a/models/repo/release.go b/models/repo/release.go index 59f4caf5aa9..3c526e472b2 100644 --- a/models/repo/release.go +++ b/models/repo/release.go @@ -93,6 +93,22 @@ func init() { db.RegisterModel(new(Release)) } +func (r *Release) LoadPublisher(ctx context.Context) error { + if r.Publisher != nil { + return nil + } + var err error + r.Publisher, err = user_model.GetUserByID(ctx, r.PublisherID) + if err != nil { + if user_model.IsErrUserNotExist(err) { + r.Publisher = user_model.NewGhostUser() + } else { + return err + } + } + return nil +} + // LoadAttributes load repo and publisher attributes for a release func (r *Release) LoadAttributes(ctx context.Context) error { var err error @@ -102,15 +118,8 @@ func (r *Release) LoadAttributes(ctx context.Context) error { return err } } - if r.Publisher == nil { - r.Publisher, err = user_model.GetUserByID(ctx, r.PublisherID) - if err != nil { - if user_model.IsErrUserNotExist(err) { - r.Publisher = user_model.NewGhostUser() - } else { - return err - } - } + if err := r.LoadPublisher(ctx); err != nil { + return err } return GetReleaseAttachments(ctx, r) } diff --git a/models/user/list.go b/models/user/list.go index ca589d1e020..638a82e6aac 100644 --- a/models/user/list.go +++ b/models/user/list.go @@ -6,6 +6,7 @@ package user import ( "context" "fmt" + "strings" "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/db" @@ -81,3 +82,20 @@ func GetUsersByIDs(ctx context.Context, ids []int64) (UserList, error) { Find(&ous) return ous, err } + +// GetUsersByUsernames returns all resolved users from a list of user names. +func GetUsersByUsernames(ctx context.Context, userNames []string) (UserList, error) { + ous := make([]*User, 0, len(userNames)) + if len(userNames) == 0 { + return ous, nil + } + for i, name := range userNames { + userNames[i] = strings.ToLower(name) + } + + err := db.GetEngine(ctx). + Where("`type` = ?", UserTypeIndividual). + In("lower_name", userNames). + Find(&ous) + return ous, err +} diff --git a/modules/structs/notifications.go b/modules/structs/notifications.go index 7fbf4cb46d8..20c0c02b4c3 100644 --- a/modules/structs/notifications.go +++ b/modules/structs/notifications.go @@ -46,4 +46,6 @@ const ( NotifySubjectCommit NotifySubjectType = "Commit" // NotifySubjectRepository an repository is subject of an notification NotifySubjectRepository NotifySubjectType = "Repository" + // NotifySubjectRelease an release is subject of an notification + NotifySubjectRelease NotifySubjectType = "Release" ) diff --git a/routers/api/v1/notify/notifications.go b/routers/api/v1/notify/notifications.go index 4e4c7dc6ddd..ff6cfc6d740 100644 --- a/routers/api/v1/notify/notifications.go +++ b/routers/api/v1/notify/notifications.go @@ -71,6 +71,8 @@ func subjectToSource(value []string) (result []activities_model.NotificationSour result = append(result, activities_model.NotificationSourceCommit) case "repository": result = append(result, activities_model.NotificationSourceRepository) + case "release": + result = append(result, activities_model.NotificationSourceRelease) } } return result diff --git a/routers/api/v1/notify/repo.go b/routers/api/v1/notify/repo.go index e87054e26cd..12bae5dd84b 100644 --- a/routers/api/v1/notify/repo.go +++ b/routers/api/v1/notify/repo.go @@ -80,7 +80,7 @@ func ListRepoNotifications(ctx *context.APIContext) { // collectionFormat: multi // items: // type: string - // enum: [issue,pull,commit,repository] + // enum: [issue,pull,commit,repository,release] // - name: since // in: query // description: Only show notifications updated after the given time. This is a timestamp in RFC 3339 format diff --git a/routers/api/v1/notify/user.go b/routers/api/v1/notify/user.go index 3ebb6788351..9e43b0fc754 100644 --- a/routers/api/v1/notify/user.go +++ b/routers/api/v1/notify/user.go @@ -42,7 +42,7 @@ func ListNotifications(ctx *context.APIContext) { // collectionFormat: multi // items: // type: string - // enum: [issue,pull,commit,repository] + // enum: [issue,pull,commit,repository,release] // - name: since // in: query // description: Only show notifications updated after the given time. This is a timestamp in RFC 3339 format diff --git a/routers/web/repo/release.go b/routers/web/repo/release.go index 36ea20c23e6..74ec80727b7 100644 --- a/routers/web/repo/release.go +++ b/routers/web/repo/release.go @@ -31,6 +31,7 @@ import ( "code.gitea.io/gitea/services/context/upload" "code.gitea.io/gitea/services/forms" release_service "code.gitea.io/gitea/services/release" + activities_model "code.gitea.io/gitea/models/activities" ) const ( @@ -298,6 +299,14 @@ func SingleRelease(ctx *context.Context) { release.Title = release.TagName } + if ctx.IsSigned && !release.IsTag { + err = activities_model.SetReleaseReadBy(ctx, release.ID, ctx.Doer.ID) + if err != nil { + ctx.ServerError("SetReleaseReadBy", err) + return + } + } + ctx.Data["PageIsSingleTag"] = release.IsTag ctx.Data["SingleReleaseTagName"] = release.TagName if release.IsTag { diff --git a/services/convert/notification.go b/services/convert/notification.go index 41063cf399f..e32f6997795 100644 --- a/services/convert/notification.go +++ b/services/convert/notification.go @@ -83,6 +83,13 @@ func ToNotificationThread(ctx context.Context, n *activities_model.Notification) URL: n.Repository.Link(), HTMLURL: n.Repository.HTMLURL(), } + case activities_model.NotificationSourceRelease: + result.Subject = &api.NotificationSubject{ + Type: api.NotifySubjectRelease, + Title: n.Release.Title, + URL: n.Release.Link(), + HTMLURL: n.Release.HTMLURL(), + } } return result diff --git a/services/uinotification/notify.go b/services/uinotification/notify.go index be5f7019a2e..47353d20211 100644 --- a/services/uinotification/notify.go +++ b/services/uinotification/notify.go @@ -9,24 +9,34 @@ import ( activities_model "code.gitea.io/gitea/models/activities" "code.gitea.io/gitea/models/db" issues_model "code.gitea.io/gitea/models/issues" + "code.gitea.io/gitea/models/organization" + access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/queue" + "code.gitea.io/gitea/modules/references" + "code.gitea.io/gitea/modules/repository" + "code.gitea.io/gitea/modules/util" notify_service "code.gitea.io/gitea/services/notify" ) type ( notificationService struct { notify_service.NullNotifier - issueQueue *queue.WorkerPoolQueue[issueNotificationOpts] + queue *queue.WorkerPoolQueue[notificationOpts] } - issueNotificationOpts struct { + notificationOpts struct { + Source activities_model.NotificationSource IssueID int64 CommentID int64 + CommitID string // commit ID for commit notifications + RepoID int64 + ReleaseID int64 NotificationAuthorID int64 ReceiverID int64 // 0 -- ALL Watcher } @@ -43,39 +53,57 @@ var _ notify_service.Notifier = ¬ificationService{} // NewNotifier create a new notificationService notifier func NewNotifier() notify_service.Notifier { ns := ¬ificationService{} - ns.issueQueue = queue.CreateSimpleQueue(graceful.GetManager().ShutdownContext(), "notification-service", handler) - if ns.issueQueue == nil { + ns.queue = queue.CreateSimpleQueue(graceful.GetManager().ShutdownContext(), "notification-service", handler) + if ns.queue == nil { log.Fatal("Unable to create notification-service queue") } return ns } -func handler(items ...issueNotificationOpts) []issueNotificationOpts { +func handler(items ...notificationOpts) []notificationOpts { for _, opts := range items { - if err := activities_model.CreateOrUpdateIssueNotifications(db.DefaultContext, opts.IssueID, opts.CommentID, opts.NotificationAuthorID, opts.ReceiverID); err != nil { - log.Error("Was unable to create issue notification: %v", err) + switch opts.Source { + case activities_model.NotificationSourceRepository: + if err := activities_model.CreateRepoTransferNotification(db.DefaultContext, opts.NotificationAuthorID, opts.RepoID, opts.ReceiverID); err != nil { + log.Error("CreateRepoTransferNotification: %v", err) + } + case activities_model.NotificationSourceCommit: + if err := activities_model.CreateCommitNotifications(db.DefaultContext, opts.RepoID, opts.NotificationAuthorID, opts.CommitID, opts.ReceiverID); err != nil { + log.Error("Was unable to create commit notification: %v", err) + } + case activities_model.NotificationSourceRelease: + if err := activities_model.CreateOrUpdateReleaseNotifications(db.DefaultContext, opts.NotificationAuthorID, opts.ReleaseID, opts.ReceiverID); err != nil { + log.Error("Was unable to create release notification: %v", err) + } + case activities_model.NotificationSourceIssue, activities_model.NotificationSourcePullRequest: + fallthrough + default: + if err := activities_model.CreateOrUpdateIssueNotifications(db.DefaultContext, opts.IssueID, opts.CommentID, opts.NotificationAuthorID, opts.ReceiverID); err != nil { + log.Error("Was unable to create issue notification: %v", err) + } } } return nil } func (ns *notificationService) Run() { - go graceful.GetManager().RunWithCancel(ns.issueQueue) // TODO: using "go" here doesn't seem right, just leave it as old code + go graceful.GetManager().RunWithCancel(ns.queue) // TODO: using "go" here doesn't seem right, just leave it as old code } func (ns *notificationService) CreateIssueComment(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, issue *issues_model.Issue, comment *issues_model.Comment, mentions []*user_model.User, ) { - opts := issueNotificationOpts{ + opts := notificationOpts{ + Source: util.Iif(issue.IsPull, activities_model.NotificationSourcePullRequest, activities_model.NotificationSourceIssue), IssueID: issue.ID, NotificationAuthorID: doer.ID, } if comment != nil { opts.CommentID = comment.ID } - _ = ns.issueQueue.Push(opts) + _ = ns.queue.Push(opts) for _, mention := range mentions { - opts := issueNotificationOpts{ + opts := notificationOpts{ IssueID: issue.ID, NotificationAuthorID: doer.ID, ReceiverID: mention.ID, @@ -83,17 +111,18 @@ func (ns *notificationService) CreateIssueComment(ctx context.Context, doer *use if comment != nil { opts.CommentID = comment.ID } - _ = ns.issueQueue.Push(opts) + _ = ns.queue.Push(opts) } } func (ns *notificationService) NewIssue(ctx context.Context, issue *issues_model.Issue, mentions []*user_model.User) { - _ = ns.issueQueue.Push(issueNotificationOpts{ + _ = ns.queue.Push(notificationOpts{ + Source: activities_model.NotificationSourceIssue, IssueID: issue.ID, NotificationAuthorID: issue.Poster.ID, }) for _, mention := range mentions { - _ = ns.issueQueue.Push(issueNotificationOpts{ + _ = ns.queue.Push(notificationOpts{ IssueID: issue.ID, NotificationAuthorID: issue.Poster.ID, ReceiverID: mention.ID, @@ -102,7 +131,8 @@ func (ns *notificationService) NewIssue(ctx context.Context, issue *issues_model } func (ns *notificationService) IssueChangeStatus(ctx context.Context, doer *user_model.User, commitID string, issue *issues_model.Issue, actionComment *issues_model.Comment, isClosed bool) { - _ = ns.issueQueue.Push(issueNotificationOpts{ + _ = ns.queue.Push(notificationOpts{ + Source: util.Iif(issue.IsPull, activities_model.NotificationSourcePullRequest, activities_model.NotificationSourceIssue), IssueID: issue.ID, NotificationAuthorID: doer.ID, CommentID: actionComment.ID, @@ -115,7 +145,8 @@ func (ns *notificationService) IssueChangeTitle(ctx context.Context, doer *user_ return } if issue.IsPull && issues_model.HasWorkInProgressPrefix(oldTitle) && !issue.PullRequest.IsWorkInProgress(ctx) { - _ = ns.issueQueue.Push(issueNotificationOpts{ + _ = ns.queue.Push(notificationOpts{ + Source: util.Iif(issue.IsPull, activities_model.NotificationSourcePullRequest, activities_model.NotificationSourceIssue), IssueID: issue.ID, NotificationAuthorID: doer.ID, }) @@ -123,7 +154,8 @@ func (ns *notificationService) IssueChangeTitle(ctx context.Context, doer *user_ } func (ns *notificationService) MergePullRequest(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest) { - _ = ns.issueQueue.Push(issueNotificationOpts{ + _ = ns.queue.Push(notificationOpts{ + Source: activities_model.NotificationSourcePullRequest, IssueID: pr.Issue.ID, NotificationAuthorID: doer.ID, }) @@ -160,7 +192,8 @@ func (ns *notificationService) NewPullRequest(ctx context.Context, pr *issues_mo toNotify.Add(mention.ID) } for receiverID := range toNotify { - _ = ns.issueQueue.Push(issueNotificationOpts{ + _ = ns.queue.Push(notificationOpts{ + Source: activities_model.NotificationSourcePullRequest, IssueID: pr.Issue.ID, NotificationAuthorID: pr.Issue.PosterID, ReceiverID: receiverID, @@ -169,16 +202,17 @@ func (ns *notificationService) NewPullRequest(ctx context.Context, pr *issues_mo } func (ns *notificationService) PullRequestReview(ctx context.Context, pr *issues_model.PullRequest, r *issues_model.Review, c *issues_model.Comment, mentions []*user_model.User) { - opts := issueNotificationOpts{ + opts := notificationOpts{ + Source: activities_model.NotificationSourcePullRequest, IssueID: pr.Issue.ID, NotificationAuthorID: r.Reviewer.ID, } if c != nil { opts.CommentID = c.ID } - _ = ns.issueQueue.Push(opts) + _ = ns.queue.Push(opts) for _, mention := range mentions { - opts := issueNotificationOpts{ + opts := notificationOpts{ IssueID: pr.Issue.ID, NotificationAuthorID: r.Reviewer.ID, ReceiverID: mention.ID, @@ -186,13 +220,14 @@ func (ns *notificationService) PullRequestReview(ctx context.Context, pr *issues if c != nil { opts.CommentID = c.ID } - _ = ns.issueQueue.Push(opts) + _ = ns.queue.Push(opts) } } func (ns *notificationService) PullRequestCodeComment(ctx context.Context, pr *issues_model.PullRequest, c *issues_model.Comment, mentions []*user_model.User) { for _, mention := range mentions { - _ = ns.issueQueue.Push(issueNotificationOpts{ + _ = ns.queue.Push(notificationOpts{ + Source: activities_model.NotificationSourcePullRequest, IssueID: pr.Issue.ID, NotificationAuthorID: c.Poster.ID, CommentID: c.ID, @@ -202,26 +237,29 @@ func (ns *notificationService) PullRequestCodeComment(ctx context.Context, pr *i } func (ns *notificationService) PullRequestPushCommits(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest, comment *issues_model.Comment) { - opts := issueNotificationOpts{ + opts := notificationOpts{ + Source: activities_model.NotificationSourcePullRequest, IssueID: pr.IssueID, NotificationAuthorID: doer.ID, CommentID: comment.ID, } - _ = ns.issueQueue.Push(opts) + _ = ns.queue.Push(opts) } func (ns *notificationService) PullReviewDismiss(ctx context.Context, doer *user_model.User, review *issues_model.Review, comment *issues_model.Comment) { - opts := issueNotificationOpts{ + opts := notificationOpts{ + Source: activities_model.NotificationSourcePullRequest, IssueID: review.IssueID, NotificationAuthorID: doer.ID, CommentID: comment.ID, } - _ = ns.issueQueue.Push(opts) + _ = ns.queue.Push(opts) } func (ns *notificationService) IssueChangeAssignee(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, assignee *user_model.User, removed bool, comment *issues_model.Comment) { if !removed && doer.ID != assignee.ID { - opts := issueNotificationOpts{ + opts := notificationOpts{ + Source: activities_model.NotificationSourceIssue, IssueID: issue.ID, NotificationAuthorID: doer.ID, ReceiverID: assignee.ID, @@ -231,13 +269,14 @@ func (ns *notificationService) IssueChangeAssignee(ctx context.Context, doer *us opts.CommentID = comment.ID } - _ = ns.issueQueue.Push(opts) + _ = ns.queue.Push(opts) } } func (ns *notificationService) PullRequestReviewRequest(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, reviewer *user_model.User, isRequest bool, comment *issues_model.Comment) { if isRequest { - opts := issueNotificationOpts{ + opts := notificationOpts{ + Source: activities_model.NotificationSourcePullRequest, IssueID: issue.ID, NotificationAuthorID: doer.ID, ReceiverID: reviewer.ID, @@ -247,15 +286,103 @@ func (ns *notificationService) PullRequestReviewRequest(ctx context.Context, doe opts.CommentID = comment.ID } - _ = ns.issueQueue.Push(opts) + _ = ns.queue.Push(opts) } } func (ns *notificationService) RepoPendingTransfer(ctx context.Context, doer, newOwner *user_model.User, repo *repo_model.Repository) { - err := db.WithTx(ctx, func(ctx context.Context) error { - return activities_model.CreateRepoTransferNotification(ctx, doer, newOwner, repo) - }) - if err != nil { - log.Error("CreateRepoTransferNotification: %v", err) + opts := notificationOpts{ + Source: activities_model.NotificationSourceRepository, + RepoID: repo.ID, + NotificationAuthorID: doer.ID, + } + + if newOwner.IsOrganization() { + users, err := organization.GetUsersWhoCanCreateOrgRepo(ctx, newOwner.ID) + if err != nil { + log.Error("GetUsersWhoCanCreateOrgRepo: %v", err) + return + } + for i := range users { + opts.ReceiverID = users[i].ID + _ = ns.queue.Push(opts) + } + } else { + opts.ReceiverID = newOwner.ID + _ = ns.queue.Push(opts) + } +} + +func (ns *notificationService) PushCommits(ctx context.Context, pusher *user_model.User, repo *repo_model.Repository, opts *repository.PushUpdateOptions, commits *repository.PushCommits) { + if len(commits.Commits) == 0 { + return + } + + for _, commit := range commits.Commits { + mentions := references.FindAllMentionsMarkdown(commit.Message) + receivers, err := user_model.GetUsersByUsernames(ctx, mentions) + if err != nil { + log.Error("GetUserIDsByNames: %v", err) + return + } + + notBlocked := make([]*user_model.User, 0, len(mentions)) + for _, user := range receivers { + if !user_model.IsUserBlockedBy(ctx, repo.Owner, user.ID) { + notBlocked = append(notBlocked, user) + } + } + receivers = notBlocked + + for _, receiver := range receivers { + perm, err := access_model.GetUserRepoPermission(ctx, repo, receiver) + if err != nil { + log.Error("GetUserRepoPermission [%d]: %w", receiver.ID, err) + return + } + if !perm.CanRead(unit.TypeCode) { + continue + } + + opts := notificationOpts{ + Source: activities_model.NotificationSourceCommit, + RepoID: repo.ID, + CommitID: commit.Sha1, + NotificationAuthorID: pusher.ID, + ReceiverID: receiver.ID, + } + if err := ns.queue.Push(opts); err != nil { + log.Error("PushCommits: %v", err) + } + } + } +} + +func (ns *notificationService) NewRelease(ctx context.Context, rel *repo_model.Release) { + _ = rel.LoadPublisher(ctx) + ns.UpdateRelease(ctx, rel.Publisher, rel) +} + +func (ns *notificationService) UpdateRelease(ctx context.Context, doer *user_model.User, rel *repo_model.Release) { + opts := notificationOpts{ + Source: activities_model.NotificationSourceRelease, + ReleaseID: rel.ID, + NotificationAuthorID: rel.PublisherID, + } + + repoWatcherIDs, err := repo_model.GetRepoWatchersIDs(ctx, rel.RepoID) + if err != nil { + log.Error("GetRepoWatchersIDs: %v", err) + return + } + + for _, watcherID := range repoWatcherIDs { + if watcherID == doer.ID { + // Do not notify the publisher of the release + continue + } + + opts.ReceiverID = watcherID + _ = ns.queue.Push(opts) } }