123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501 |
- // Copyright 2024 The Gitea Authors. All rights reserved.
- // SPDX-License-Identifier: MIT
-
- package activities
-
- import (
- "context"
-
- "code.gitea.io/gitea/models/db"
- issues_model "code.gitea.io/gitea/models/issues"
- 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/log"
- "code.gitea.io/gitea/modules/util"
-
- "xorm.io/builder"
- )
-
- // FindNotificationOptions represent the filters for notifications. If an ID is 0 it will be ignored.
- type FindNotificationOptions struct {
- db.ListOptions
- UserID int64
- RepoID int64
- IssueID int64
- Status []NotificationStatus
- Source []NotificationSource
- UpdatedAfterUnix int64
- UpdatedBeforeUnix int64
- }
-
- // ToCond will convert each condition into a xorm-Cond
- func (opts FindNotificationOptions) ToConds() builder.Cond {
- cond := builder.NewCond()
- if opts.UserID != 0 {
- cond = cond.And(builder.Eq{"notification.user_id": opts.UserID})
- }
- if opts.RepoID != 0 {
- cond = cond.And(builder.Eq{"notification.repo_id": opts.RepoID})
- }
- if opts.IssueID != 0 {
- cond = cond.And(builder.Eq{"notification.issue_id": opts.IssueID})
- }
- if len(opts.Status) > 0 {
- if len(opts.Status) == 1 {
- cond = cond.And(builder.Eq{"notification.status": opts.Status[0]})
- } else {
- cond = cond.And(builder.In("notification.status", opts.Status))
- }
- }
- if len(opts.Source) > 0 {
- cond = cond.And(builder.In("notification.source", opts.Source))
- }
- if opts.UpdatedAfterUnix != 0 {
- cond = cond.And(builder.Gte{"notification.updated_unix": opts.UpdatedAfterUnix})
- }
- if opts.UpdatedBeforeUnix != 0 {
- cond = cond.And(builder.Lte{"notification.updated_unix": opts.UpdatedBeforeUnix})
- }
- return cond
- }
-
- func (opts FindNotificationOptions) ToOrders() string {
- return "notification.updated_unix DESC"
- }
-
- // CreateOrUpdateIssueNotifications creates an issue notification
- // 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()
- }
-
- func createOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, notificationAuthorID, receiverID int64) error {
- // init
- var toNotify container.Set[int64]
- notifications, err := db.Find[Notification](ctx, FindNotificationOptions{
- IssueID: issueID,
- })
- if err != nil {
- return err
- }
-
- issue, err := issues_model.GetIssueByID(ctx, issueID)
- if err != nil {
- return err
- }
-
- if receiverID > 0 {
- toNotify = make(container.Set[int64], 1)
- toNotify.Add(receiverID)
- } else {
- toNotify = make(container.Set[int64], 32)
- issueWatches, err := issues_model.GetIssueWatchersIDs(ctx, issueID, true)
- if err != nil {
- return err
- }
- toNotify.AddMultiple(issueWatches...)
- if !(issue.IsPull && issues_model.HasWorkInProgressPrefix(issue.Title)) {
- repoWatches, err := repo_model.GetRepoWatchersIDs(ctx, issue.RepoID)
- if err != nil {
- return err
- }
- toNotify.AddMultiple(repoWatches...)
- }
- issueParticipants, err := issue.GetParticipantIDsByIssue(ctx)
- if err != nil {
- return err
- }
- toNotify.AddMultiple(issueParticipants...)
-
- // dont notify user who cause notification
- delete(toNotify, notificationAuthorID)
- // explicit unwatch on issue
- issueUnWatches, err := issues_model.GetIssueWatchersIDs(ctx, issueID, false)
- if err != nil {
- return err
- }
- for _, id := range issueUnWatches {
- toNotify.Remove(id)
- }
- }
-
- err = issue.LoadRepo(ctx)
- if err != nil {
- return err
- }
-
- // notify
- for userID := range toNotify {
- issue.Repo.Units = nil
- user, err := user_model.GetUserByID(ctx, userID)
- if err != nil {
- if user_model.IsErrUserNotExist(err) {
- continue
- }
-
- return err
- }
- if issue.IsPull && !access_model.CheckRepoUnitUser(ctx, issue.Repo, user, unit.TypePullRequests) {
- continue
- }
- if !issue.IsPull && !access_model.CheckRepoUnitUser(ctx, issue.Repo, user, unit.TypeIssues) {
- continue
- }
-
- if notificationExists(notifications, issue.ID, userID) {
- if err = updateIssueNotification(ctx, userID, issue.ID, commentID, notificationAuthorID); err != nil {
- return err
- }
- continue
- }
- if err = createIssueNotification(ctx, userID, issue, commentID, notificationAuthorID); err != nil {
- return err
- }
- }
- return nil
- }
-
- // NotificationList contains a list of notifications
- type NotificationList []*Notification
-
- // LoadAttributes load Repo Issue User and Comment if not loaded
- func (nl NotificationList) LoadAttributes(ctx context.Context) error {
- if _, _, err := nl.LoadRepos(ctx); err != nil {
- return err
- }
- if _, err := nl.LoadIssues(ctx); err != nil {
- return err
- }
- if _, err := nl.LoadUsers(ctx); err != nil {
- return err
- }
- if _, err := nl.LoadComments(ctx); err != nil {
- return err
- }
- return nil
- }
-
- func (nl NotificationList) getPendingRepoIDs() []int64 {
- ids := make(container.Set[int64], len(nl))
- for _, notification := range nl {
- if notification.Repository != nil {
- continue
- }
- ids.Add(notification.RepoID)
- }
- return ids.Values()
- }
-
- // LoadRepos loads repositories from database
- func (nl NotificationList) LoadRepos(ctx context.Context) (repo_model.RepositoryList, []int, error) {
- if len(nl) == 0 {
- return repo_model.RepositoryList{}, []int{}, nil
- }
-
- repoIDs := nl.getPendingRepoIDs()
- repos := make(map[int64]*repo_model.Repository, len(repoIDs))
- left := len(repoIDs)
- for left > 0 {
- limit := db.DefaultMaxInSize
- if left < limit {
- limit = left
- }
- rows, err := db.GetEngine(ctx).
- In("id", repoIDs[:limit]).
- Rows(new(repo_model.Repository))
- if err != nil {
- return nil, nil, err
- }
-
- for rows.Next() {
- var repo repo_model.Repository
- err = rows.Scan(&repo)
- if err != nil {
- rows.Close()
- return nil, nil, err
- }
-
- repos[repo.ID] = &repo
- }
- _ = rows.Close()
-
- left -= limit
- repoIDs = repoIDs[limit:]
- }
-
- failed := []int{}
-
- reposList := make(repo_model.RepositoryList, 0, len(repoIDs))
- for i, notification := range nl {
- if notification.Repository == nil {
- notification.Repository = repos[notification.RepoID]
- }
- if notification.Repository == nil {
- log.Error("Notification[%d]: RepoID: %d not found", notification.ID, notification.RepoID)
- failed = append(failed, i)
- continue
- }
- var found bool
- for _, r := range reposList {
- if r.ID == notification.RepoID {
- found = true
- break
- }
- }
- if !found {
- reposList = append(reposList, notification.Repository)
- }
- }
- return reposList, failed, nil
- }
-
- func (nl NotificationList) getPendingIssueIDs() []int64 {
- ids := make(container.Set[int64], len(nl))
- for _, notification := range nl {
- if notification.Issue != nil {
- continue
- }
- ids.Add(notification.IssueID)
- }
- return ids.Values()
- }
-
- // LoadIssues loads issues from database
- func (nl NotificationList) LoadIssues(ctx context.Context) ([]int, error) {
- if len(nl) == 0 {
- return []int{}, nil
- }
-
- issueIDs := nl.getPendingIssueIDs()
- issues := make(map[int64]*issues_model.Issue, len(issueIDs))
- left := len(issueIDs)
- for left > 0 {
- limit := db.DefaultMaxInSize
- if left < limit {
- limit = left
- }
- rows, err := db.GetEngine(ctx).
- In("id", issueIDs[:limit]).
- Rows(new(issues_model.Issue))
- if err != nil {
- return nil, err
- }
-
- for rows.Next() {
- var issue issues_model.Issue
- err = rows.Scan(&issue)
- if err != nil {
- rows.Close()
- return nil, err
- }
-
- issues[issue.ID] = &issue
- }
- _ = rows.Close()
-
- left -= limit
- issueIDs = issueIDs[limit:]
- }
-
- failures := []int{}
-
- for i, notification := range nl {
- if notification.Issue == nil {
- notification.Issue = issues[notification.IssueID]
- if notification.Issue == nil {
- if notification.IssueID != 0 {
- log.Error("Notification[%d]: IssueID: %d Not Found", notification.ID, notification.IssueID)
- failures = append(failures, i)
- }
- continue
- }
- notification.Issue.Repo = notification.Repository
- }
- }
- return failures, nil
- }
-
- // Without returns the notification list without the failures
- func (nl NotificationList) Without(failures []int) NotificationList {
- if len(failures) == 0 {
- return nl
- }
- remaining := make([]*Notification, 0, len(nl))
- last := -1
- var i int
- for _, i = range failures {
- remaining = append(remaining, nl[last+1:i]...)
- last = i
- }
- if len(nl) > i {
- remaining = append(remaining, nl[i+1:]...)
- }
- return remaining
- }
-
- func (nl NotificationList) getPendingCommentIDs() []int64 {
- ids := make(container.Set[int64], len(nl))
- for _, notification := range nl {
- if notification.CommentID == 0 || notification.Comment != nil {
- continue
- }
- ids.Add(notification.CommentID)
- }
- return ids.Values()
- }
-
- func (nl NotificationList) getUserIDs() []int64 {
- ids := make(container.Set[int64], len(nl))
- for _, notification := range nl {
- if notification.UserID == 0 || notification.User != nil {
- continue
- }
- ids.Add(notification.UserID)
- }
- return ids.Values()
- }
-
- // LoadUsers loads users from database
- func (nl NotificationList) LoadUsers(ctx context.Context) ([]int, error) {
- if len(nl) == 0 {
- return []int{}, nil
- }
-
- userIDs := nl.getUserIDs()
- users := make(map[int64]*user_model.User, len(userIDs))
- left := len(userIDs)
- for left > 0 {
- limit := db.DefaultMaxInSize
- if left < limit {
- limit = left
- }
- rows, err := db.GetEngine(ctx).
- In("id", userIDs[:limit]).
- Rows(new(user_model.User))
- if err != nil {
- return nil, err
- }
-
- for rows.Next() {
- var user user_model.User
- err = rows.Scan(&user)
- if err != nil {
- rows.Close()
- return nil, err
- }
-
- users[user.ID] = &user
- }
- _ = rows.Close()
-
- left -= limit
- userIDs = userIDs[limit:]
- }
-
- failures := []int{}
- for i, notification := range nl {
- if notification.UserID > 0 && notification.User == nil && users[notification.UserID] != nil {
- notification.User = users[notification.UserID]
- if notification.User == nil {
- log.Error("Notification[%d]: UserID[%d] failed to load", notification.ID, notification.UserID)
- failures = append(failures, i)
- continue
- }
- }
- }
- return failures, nil
- }
-
- // LoadComments loads comments from database
- func (nl NotificationList) LoadComments(ctx context.Context) ([]int, error) {
- if len(nl) == 0 {
- return []int{}, nil
- }
-
- commentIDs := nl.getPendingCommentIDs()
- comments := make(map[int64]*issues_model.Comment, len(commentIDs))
- left := len(commentIDs)
- for left > 0 {
- limit := db.DefaultMaxInSize
- if left < limit {
- limit = left
- }
- rows, err := db.GetEngine(ctx).
- In("id", commentIDs[:limit]).
- Rows(new(issues_model.Comment))
- if err != nil {
- return nil, err
- }
-
- for rows.Next() {
- var comment issues_model.Comment
- err = rows.Scan(&comment)
- if err != nil {
- rows.Close()
- return nil, err
- }
-
- comments[comment.ID] = &comment
- }
- _ = rows.Close()
-
- left -= limit
- commentIDs = commentIDs[limit:]
- }
-
- failures := []int{}
- for i, notification := range nl {
- if notification.CommentID > 0 && notification.Comment == nil && comments[notification.CommentID] != nil {
- notification.Comment = comments[notification.CommentID]
- if notification.Comment == nil {
- log.Error("Notification[%d]: CommentID[%d] failed to load", notification.ID, notification.CommentID)
- failures = append(failures, i)
- continue
- }
- notification.Comment.Issue = notification.Issue
- }
- }
- 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))
- for _, notification := range nl {
- if notification.Issue != nil && notification.Issue.IsPull && notification.Issue.PullRequest == nil {
- issues[notification.Issue.ID] = notification.Issue
- }
- }
-
- if len(issues) == 0 {
- return nil
- }
-
- pulls, err := issues_model.GetPullRequestByIssueIDs(ctx, util.KeysOfMap(issues))
- if err != nil {
- return err
- }
-
- for _, pull := range pulls {
- if issue := issues[pull.IssueID]; issue != nil {
- issue.PullRequest = pull
- issue.PullRequest.Issue = issue
- }
- }
-
- return nil
- }
|