code.gitea.io/gitea@v1.22.3/models/activities/notification.go (about)

     1  // Copyright 2016 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package activities
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"net/url"
    10  	"strconv"
    11  
    12  	"code.gitea.io/gitea/models/db"
    13  	issues_model "code.gitea.io/gitea/models/issues"
    14  	"code.gitea.io/gitea/models/organization"
    15  	repo_model "code.gitea.io/gitea/models/repo"
    16  	user_model "code.gitea.io/gitea/models/user"
    17  	"code.gitea.io/gitea/modules/setting"
    18  	"code.gitea.io/gitea/modules/timeutil"
    19  
    20  	"xorm.io/builder"
    21  )
    22  
    23  type (
    24  	// NotificationStatus is the status of the notification (read or unread)
    25  	NotificationStatus uint8
    26  	// NotificationSource is the source of the notification (issue, PR, commit, etc)
    27  	NotificationSource uint8
    28  )
    29  
    30  const (
    31  	// NotificationStatusUnread represents an unread notification
    32  	NotificationStatusUnread NotificationStatus = iota + 1
    33  	// NotificationStatusRead represents a read notification
    34  	NotificationStatusRead
    35  	// NotificationStatusPinned represents a pinned notification
    36  	NotificationStatusPinned
    37  )
    38  
    39  const (
    40  	// NotificationSourceIssue is a notification of an issue
    41  	NotificationSourceIssue NotificationSource = iota + 1
    42  	// NotificationSourcePullRequest is a notification of a pull request
    43  	NotificationSourcePullRequest
    44  	// NotificationSourceCommit is a notification of a commit
    45  	NotificationSourceCommit
    46  	// NotificationSourceRepository is a notification for a repository
    47  	NotificationSourceRepository
    48  )
    49  
    50  // Notification represents a notification
    51  type Notification struct {
    52  	ID     int64 `xorm:"pk autoincr"`
    53  	UserID int64 `xorm:"INDEX NOT NULL"`
    54  	RepoID int64 `xorm:"INDEX NOT NULL"`
    55  
    56  	Status NotificationStatus `xorm:"SMALLINT INDEX NOT NULL"`
    57  	Source NotificationSource `xorm:"SMALLINT INDEX NOT NULL"`
    58  
    59  	IssueID   int64  `xorm:"INDEX NOT NULL"`
    60  	CommitID  string `xorm:"INDEX"`
    61  	CommentID int64
    62  
    63  	UpdatedBy int64 `xorm:"INDEX NOT NULL"`
    64  
    65  	Issue      *issues_model.Issue    `xorm:"-"`
    66  	Repository *repo_model.Repository `xorm:"-"`
    67  	Comment    *issues_model.Comment  `xorm:"-"`
    68  	User       *user_model.User       `xorm:"-"`
    69  
    70  	CreatedUnix timeutil.TimeStamp `xorm:"created INDEX NOT NULL"`
    71  	UpdatedUnix timeutil.TimeStamp `xorm:"updated INDEX NOT NULL"`
    72  }
    73  
    74  func init() {
    75  	db.RegisterModel(new(Notification))
    76  }
    77  
    78  // CreateRepoTransferNotification creates  notification for the user a repository was transferred to
    79  func CreateRepoTransferNotification(ctx context.Context, doer, newOwner *user_model.User, repo *repo_model.Repository) error {
    80  	return db.WithTx(ctx, func(ctx context.Context) error {
    81  		var notify []*Notification
    82  
    83  		if newOwner.IsOrganization() {
    84  			users, err := organization.GetUsersWhoCanCreateOrgRepo(ctx, newOwner.ID)
    85  			if err != nil || len(users) == 0 {
    86  				return err
    87  			}
    88  			for i := range users {
    89  				notify = append(notify, &Notification{
    90  					UserID:    i,
    91  					RepoID:    repo.ID,
    92  					Status:    NotificationStatusUnread,
    93  					UpdatedBy: doer.ID,
    94  					Source:    NotificationSourceRepository,
    95  				})
    96  			}
    97  		} else {
    98  			notify = []*Notification{{
    99  				UserID:    newOwner.ID,
   100  				RepoID:    repo.ID,
   101  				Status:    NotificationStatusUnread,
   102  				UpdatedBy: doer.ID,
   103  				Source:    NotificationSourceRepository,
   104  			}}
   105  		}
   106  
   107  		return db.Insert(ctx, notify)
   108  	})
   109  }
   110  
   111  func createIssueNotification(ctx context.Context, userID int64, issue *issues_model.Issue, commentID, updatedByID int64) error {
   112  	notification := &Notification{
   113  		UserID:    userID,
   114  		RepoID:    issue.RepoID,
   115  		Status:    NotificationStatusUnread,
   116  		IssueID:   issue.ID,
   117  		CommentID: commentID,
   118  		UpdatedBy: updatedByID,
   119  	}
   120  
   121  	if issue.IsPull {
   122  		notification.Source = NotificationSourcePullRequest
   123  	} else {
   124  		notification.Source = NotificationSourceIssue
   125  	}
   126  
   127  	return db.Insert(ctx, notification)
   128  }
   129  
   130  func updateIssueNotification(ctx context.Context, userID, issueID, commentID, updatedByID int64) error {
   131  	notification, err := GetIssueNotification(ctx, userID, issueID)
   132  	if err != nil {
   133  		return err
   134  	}
   135  
   136  	// NOTICE: Only update comment id when the before notification on this issue is read, otherwise you may miss some old comments.
   137  	// But we need update update_by so that the notification will be reorder
   138  	var cols []string
   139  	if notification.Status == NotificationStatusRead {
   140  		notification.Status = NotificationStatusUnread
   141  		notification.CommentID = commentID
   142  		cols = []string{"status", "update_by", "comment_id"}
   143  	} else {
   144  		notification.UpdatedBy = updatedByID
   145  		cols = []string{"update_by"}
   146  	}
   147  
   148  	_, err = db.GetEngine(ctx).ID(notification.ID).Cols(cols...).Update(notification)
   149  	return err
   150  }
   151  
   152  // GetIssueNotification return the notification about an issue
   153  func GetIssueNotification(ctx context.Context, userID, issueID int64) (*Notification, error) {
   154  	notification := new(Notification)
   155  	_, err := db.GetEngine(ctx).
   156  		Where("user_id = ?", userID).
   157  		And("issue_id = ?", issueID).
   158  		Get(notification)
   159  	return notification, err
   160  }
   161  
   162  // LoadAttributes load Repo Issue User and Comment if not loaded
   163  func (n *Notification) LoadAttributes(ctx context.Context) (err error) {
   164  	if err = n.loadRepo(ctx); err != nil {
   165  		return err
   166  	}
   167  	if err = n.loadIssue(ctx); err != nil {
   168  		return err
   169  	}
   170  	if err = n.loadUser(ctx); err != nil {
   171  		return err
   172  	}
   173  	if err = n.loadComment(ctx); err != nil {
   174  		return err
   175  	}
   176  	return err
   177  }
   178  
   179  func (n *Notification) loadRepo(ctx context.Context) (err error) {
   180  	if n.Repository == nil {
   181  		n.Repository, err = repo_model.GetRepositoryByID(ctx, n.RepoID)
   182  		if err != nil {
   183  			return fmt.Errorf("getRepositoryByID [%d]: %w", n.RepoID, err)
   184  		}
   185  	}
   186  	return nil
   187  }
   188  
   189  func (n *Notification) loadIssue(ctx context.Context) (err error) {
   190  	if n.Issue == nil && n.IssueID != 0 {
   191  		n.Issue, err = issues_model.GetIssueByID(ctx, n.IssueID)
   192  		if err != nil {
   193  			return fmt.Errorf("getIssueByID [%d]: %w", n.IssueID, err)
   194  		}
   195  		return n.Issue.LoadAttributes(ctx)
   196  	}
   197  	return nil
   198  }
   199  
   200  func (n *Notification) loadComment(ctx context.Context) (err error) {
   201  	if n.Comment == nil && n.CommentID != 0 {
   202  		n.Comment, err = issues_model.GetCommentByID(ctx, n.CommentID)
   203  		if err != nil {
   204  			if issues_model.IsErrCommentNotExist(err) {
   205  				return issues_model.ErrCommentNotExist{
   206  					ID:      n.CommentID,
   207  					IssueID: n.IssueID,
   208  				}
   209  			}
   210  			return err
   211  		}
   212  	}
   213  	return nil
   214  }
   215  
   216  func (n *Notification) loadUser(ctx context.Context) (err error) {
   217  	if n.User == nil {
   218  		n.User, err = user_model.GetUserByID(ctx, n.UserID)
   219  		if err != nil {
   220  			return fmt.Errorf("getUserByID [%d]: %w", n.UserID, err)
   221  		}
   222  	}
   223  	return nil
   224  }
   225  
   226  // GetRepo returns the repo of the notification
   227  func (n *Notification) GetRepo(ctx context.Context) (*repo_model.Repository, error) {
   228  	return n.Repository, n.loadRepo(ctx)
   229  }
   230  
   231  // GetIssue returns the issue of the notification
   232  func (n *Notification) GetIssue(ctx context.Context) (*issues_model.Issue, error) {
   233  	return n.Issue, n.loadIssue(ctx)
   234  }
   235  
   236  // HTMLURL formats a URL-string to the notification
   237  func (n *Notification) HTMLURL(ctx context.Context) string {
   238  	switch n.Source {
   239  	case NotificationSourceIssue, NotificationSourcePullRequest:
   240  		if n.Comment != nil {
   241  			return n.Comment.HTMLURL(ctx)
   242  		}
   243  		return n.Issue.HTMLURL()
   244  	case NotificationSourceCommit:
   245  		return n.Repository.HTMLURL() + "/commit/" + url.PathEscape(n.CommitID)
   246  	case NotificationSourceRepository:
   247  		return n.Repository.HTMLURL()
   248  	}
   249  	return ""
   250  }
   251  
   252  // Link formats a relative URL-string to the notification
   253  func (n *Notification) Link(ctx context.Context) string {
   254  	switch n.Source {
   255  	case NotificationSourceIssue, NotificationSourcePullRequest:
   256  		if n.Comment != nil {
   257  			return n.Comment.Link(ctx)
   258  		}
   259  		return n.Issue.Link()
   260  	case NotificationSourceCommit:
   261  		return n.Repository.Link() + "/commit/" + url.PathEscape(n.CommitID)
   262  	case NotificationSourceRepository:
   263  		return n.Repository.Link()
   264  	}
   265  	return ""
   266  }
   267  
   268  // APIURL formats a URL-string to the notification
   269  func (n *Notification) APIURL() string {
   270  	return setting.AppURL + "api/v1/notifications/threads/" + strconv.FormatInt(n.ID, 10)
   271  }
   272  
   273  func notificationExists(notifications []*Notification, issueID, userID int64) bool {
   274  	for _, notification := range notifications {
   275  		if notification.IssueID == issueID && notification.UserID == userID {
   276  			return true
   277  		}
   278  	}
   279  
   280  	return false
   281  }
   282  
   283  // UserIDCount is a simple coalition of UserID and Count
   284  type UserIDCount struct {
   285  	UserID int64
   286  	Count  int64
   287  }
   288  
   289  // GetUIDsAndNotificationCounts returns the unread counts for every user between the two provided times.
   290  // It must return all user IDs which appear during the period, including count=0 for users who have read all.
   291  func GetUIDsAndNotificationCounts(ctx context.Context, since, until timeutil.TimeStamp) ([]UserIDCount, error) {
   292  	sql := `SELECT user_id, sum(case when status= ? then 1 else 0 end) AS count FROM notification ` +
   293  		`WHERE user_id IN (SELECT user_id FROM notification WHERE updated_unix >= ? AND ` +
   294  		`updated_unix < ?) GROUP BY user_id`
   295  	var res []UserIDCount
   296  	return res, db.GetEngine(ctx).SQL(sql, NotificationStatusUnread, since, until).Find(&res)
   297  }
   298  
   299  // SetIssueReadBy sets issue to be read by given user.
   300  func SetIssueReadBy(ctx context.Context, issueID, userID int64) error {
   301  	if err := issues_model.UpdateIssueUserByRead(ctx, userID, issueID); err != nil {
   302  		return err
   303  	}
   304  
   305  	return setIssueNotificationStatusReadIfUnread(ctx, userID, issueID)
   306  }
   307  
   308  func setIssueNotificationStatusReadIfUnread(ctx context.Context, userID, issueID int64) error {
   309  	notification, err := GetIssueNotification(ctx, userID, issueID)
   310  	// ignore if not exists
   311  	if err != nil {
   312  		return nil
   313  	}
   314  
   315  	if notification.Status != NotificationStatusUnread {
   316  		return nil
   317  	}
   318  
   319  	notification.Status = NotificationStatusRead
   320  
   321  	_, err = db.GetEngine(ctx).ID(notification.ID).Cols("status").Update(notification)
   322  	return err
   323  }
   324  
   325  // SetRepoReadBy sets repo to be visited by given user.
   326  func SetRepoReadBy(ctx context.Context, userID, repoID int64) error {
   327  	_, err := db.GetEngine(ctx).Where(builder.Eq{
   328  		"user_id": userID,
   329  		"status":  NotificationStatusUnread,
   330  		"source":  NotificationSourceRepository,
   331  		"repo_id": repoID,
   332  	}).Cols("status").Update(&Notification{Status: NotificationStatusRead})
   333  	return err
   334  }
   335  
   336  // SetNotificationStatus change the notification status
   337  func SetNotificationStatus(ctx context.Context, notificationID int64, user *user_model.User, status NotificationStatus) (*Notification, error) {
   338  	notification, err := GetNotificationByID(ctx, notificationID)
   339  	if err != nil {
   340  		return notification, err
   341  	}
   342  
   343  	if notification.UserID != user.ID {
   344  		return nil, fmt.Errorf("Can't change notification of another user: %d, %d", notification.UserID, user.ID)
   345  	}
   346  
   347  	notification.Status = status
   348  
   349  	_, err = db.GetEngine(ctx).ID(notificationID).Update(notification)
   350  	return notification, err
   351  }
   352  
   353  // GetNotificationByID return notification by ID
   354  func GetNotificationByID(ctx context.Context, notificationID int64) (*Notification, error) {
   355  	notification := new(Notification)
   356  	ok, err := db.GetEngine(ctx).
   357  		Where("id = ?", notificationID).
   358  		Get(notification)
   359  	if err != nil {
   360  		return nil, err
   361  	}
   362  
   363  	if !ok {
   364  		return nil, db.ErrNotExist{Resource: "notification", ID: notificationID}
   365  	}
   366  
   367  	return notification, nil
   368  }
   369  
   370  // UpdateNotificationStatuses updates the statuses of all of a user's notifications that are of the currentStatus type to the desiredStatus
   371  func UpdateNotificationStatuses(ctx context.Context, user *user_model.User, currentStatus, desiredStatus NotificationStatus) error {
   372  	n := &Notification{Status: desiredStatus, UpdatedBy: user.ID}
   373  	_, err := db.GetEngine(ctx).
   374  		Where("user_id = ? AND status = ?", user.ID, currentStatus).
   375  		Cols("status", "updated_by", "updated_unix").
   376  		Update(n)
   377  	return err
   378  }