| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419 |
- // Copyright 2016 The Gitea Authors. All rights reserved.
- // SPDX-License-Identifier: MIT
-
- package activities
-
- import (
- "context"
- "fmt"
- "net/url"
- "strconv"
-
- "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"
- "code.gitea.io/gitea/modules/timeutil"
-
- "xorm.io/builder"
- "xorm.io/xorm/schemas"
- )
-
- type (
- // NotificationStatus is the status of the notification (read or unread)
- NotificationStatus uint8
- // NotificationSource is the source of the notification (issue, PR, commit, etc)
- NotificationSource uint8
- )
-
- const (
- // NotificationStatusUnread represents an unread notification
- NotificationStatusUnread NotificationStatus = iota + 1
- // NotificationStatusRead represents a read notification
- NotificationStatusRead
- // NotificationStatusPinned represents a pinned notification
- NotificationStatusPinned
- )
-
- const (
- // NotificationSourceIssue is a notification of an issue
- NotificationSourceIssue NotificationSource = iota + 1
- // NotificationSourcePullRequest is a notification of a pull request
- NotificationSourcePullRequest
- // NotificationSourceCommit is a notification of a commit
- NotificationSourceCommit
- // NotificationSourceRepository is a notification for a repository
- NotificationSourceRepository
- )
-
- // Notification represents a notification
- type Notification struct {
- ID int64 `xorm:"pk autoincr"`
- UserID int64 `xorm:"NOT NULL"`
- RepoID int64 `xorm:"NOT NULL"`
-
- Status NotificationStatus `xorm:"SMALLINT NOT NULL"`
- Source NotificationSource `xorm:"SMALLINT NOT NULL"`
-
- IssueID int64 `xorm:"NOT NULL"`
- CommitID string
- CommentID int64
-
- UpdatedBy int64 `xorm:"NOT NULL"`
-
- Issue *issues_model.Issue `xorm:"-"`
- Repository *repo_model.Repository `xorm:"-"`
- Comment *issues_model.Comment `xorm:"-"`
- User *user_model.User `xorm:"-"`
-
- CreatedUnix timeutil.TimeStamp `xorm:"created NOT NULL"`
- UpdatedUnix timeutil.TimeStamp `xorm:"updated NOT NULL"`
- }
-
- // TableIndices implements xorm's TableIndices interface
- func (n *Notification) 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)
-
- updatedByIndex := schemas.NewIndex("idx_notification_updated_by", schemas.IndexType)
- updatedByIndex.AddColumn("updated_by")
- indices = append(indices, updatedByIndex)
-
- return indices
- }
-
- func init() {
- db.RegisterModel(new(Notification))
- }
-
- // 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
-
- 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,
- }}
- }
-
- return db.Insert(ctx, notify)
- })
- }
-
- func createIssueNotification(ctx context.Context, userID int64, issue *issues_model.Issue, commentID, updatedByID int64) error {
- notification := &Notification{
- UserID: userID,
- RepoID: issue.RepoID,
- Status: NotificationStatusUnread,
- IssueID: issue.ID,
- CommentID: commentID,
- UpdatedBy: updatedByID,
- }
-
- if issue.IsPull {
- notification.Source = NotificationSourcePullRequest
- } else {
- notification.Source = NotificationSourceIssue
- }
-
- return db.Insert(ctx, notification)
- }
-
- func updateIssueNotification(ctx context.Context, userID, issueID, commentID, updatedByID int64) error {
- notification, err := GetIssueNotification(ctx, userID, issueID)
- if err != nil {
- return err
- }
-
- // NOTICE: Only update comment id when the before notification on this issue is read, otherwise you may miss some old comments.
- // But we need update update_by so that the notification will be reorder
- var cols []string
- if notification.Status == NotificationStatusRead {
- notification.Status = NotificationStatusUnread
- notification.CommentID = commentID
- cols = []string{"status", "update_by", "comment_id"}
- } else {
- notification.UpdatedBy = updatedByID
- cols = []string{"update_by"}
- }
-
- _, err = db.GetEngine(ctx).ID(notification.ID).Cols(cols...).Update(notification)
- return err
- }
-
- // GetIssueNotification return the notification about an issue
- func GetIssueNotification(ctx context.Context, userID, issueID int64) (*Notification, error) {
- notification := new(Notification)
- _, err := db.GetEngine(ctx).
- Where("user_id = ?", userID).
- And("issue_id = ?", issueID).
- Get(notification)
- return notification, err
- }
-
- // LoadAttributes load Repo Issue User and Comment if not loaded
- func (n *Notification) LoadAttributes(ctx context.Context) (err error) {
- if err = n.loadRepo(ctx); err != nil {
- return err
- }
- if err = n.loadIssue(ctx); err != nil {
- return err
- }
- if err = n.loadUser(ctx); err != nil {
- return err
- }
- if err = n.loadComment(ctx); err != nil {
- return err
- }
- return err
- }
-
- func (n *Notification) loadRepo(ctx context.Context) (err error) {
- if n.Repository == nil {
- n.Repository, err = repo_model.GetRepositoryByID(ctx, n.RepoID)
- if err != nil {
- return fmt.Errorf("getRepositoryByID [%d]: %w", n.RepoID, err)
- }
- }
- return nil
- }
-
- func (n *Notification) loadIssue(ctx context.Context) (err error) {
- if n.Issue == nil && n.IssueID != 0 {
- n.Issue, err = issues_model.GetIssueByID(ctx, n.IssueID)
- if err != nil {
- return fmt.Errorf("getIssueByID [%d]: %w", n.IssueID, err)
- }
- return n.Issue.LoadAttributes(ctx)
- }
- return nil
- }
-
- func (n *Notification) loadComment(ctx context.Context) (err error) {
- if n.Comment == nil && n.CommentID != 0 {
- n.Comment, err = issues_model.GetCommentByID(ctx, n.CommentID)
- if err != nil {
- if issues_model.IsErrCommentNotExist(err) {
- return issues_model.ErrCommentNotExist{
- ID: n.CommentID,
- IssueID: n.IssueID,
- }
- }
- return 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)
- if err != nil {
- return fmt.Errorf("getUserByID [%d]: %w", n.UserID, err)
- }
- }
- return nil
- }
-
- // GetRepo returns the repo of the notification
- func (n *Notification) GetRepo(ctx context.Context) (*repo_model.Repository, error) {
- return n.Repository, n.loadRepo(ctx)
- }
-
- // GetIssue returns the issue of the notification
- func (n *Notification) GetIssue(ctx context.Context) (*issues_model.Issue, error) {
- return n.Issue, n.loadIssue(ctx)
- }
-
- // HTMLURL formats a URL-string to the notification
- func (n *Notification) HTMLURL(ctx context.Context) string {
- switch n.Source {
- case NotificationSourceIssue, NotificationSourcePullRequest:
- if n.Comment != nil {
- return n.Comment.HTMLURL(ctx)
- }
- return n.Issue.HTMLURL(ctx)
- case NotificationSourceCommit:
- return n.Repository.HTMLURL(ctx) + "/commit/" + url.PathEscape(n.CommitID)
- case NotificationSourceRepository:
- return n.Repository.HTMLURL(ctx)
- }
- return ""
- }
-
- // Link formats a relative URL-string to the notification
- func (n *Notification) Link(ctx context.Context) string {
- switch n.Source {
- case NotificationSourceIssue, NotificationSourcePullRequest:
- if n.Comment != nil {
- return n.Comment.Link(ctx)
- }
- return n.Issue.Link()
- case NotificationSourceCommit:
- return n.Repository.Link() + "/commit/" + url.PathEscape(n.CommitID)
- case NotificationSourceRepository:
- return n.Repository.Link()
- }
- return ""
- }
-
- // APIURL formats a URL-string to the notification
- func (n *Notification) APIURL() string {
- return setting.AppURL + "api/v1/notifications/threads/" + strconv.FormatInt(n.ID, 10)
- }
-
- func notificationExists(notifications []*Notification, issueID, userID int64) bool {
- for _, notification := range notifications {
- if notification.IssueID == issueID && notification.UserID == userID {
- return true
- }
- }
-
- return false
- }
-
- // UserIDCount is a simple coalition of UserID and Count
- type UserIDCount struct {
- UserID int64
- Count int64
- }
-
- // GetUIDsAndNotificationCounts returns the unread counts for every user between the two provided times.
- // It must return all user IDs which appear during the period, including count=0 for users who have read all.
- func GetUIDsAndNotificationCounts(ctx context.Context, since, until timeutil.TimeStamp) ([]UserIDCount, error) {
- sql := `SELECT user_id, sum(case when status= ? then 1 else 0 end) AS count FROM notification ` +
- `WHERE user_id IN (SELECT user_id FROM notification WHERE updated_unix >= ? AND ` +
- `updated_unix < ?) GROUP BY user_id`
- var res []UserIDCount
- return res, db.GetEngine(ctx).SQL(sql, NotificationStatusUnread, since, until).Find(&res)
- }
-
- // SetIssueReadBy sets issue to be read by given user.
- func SetIssueReadBy(ctx context.Context, issueID, userID int64) error {
- if err := issues_model.UpdateIssueUserByRead(ctx, userID, issueID); err != nil {
- return err
- }
-
- return setIssueNotificationStatusReadIfUnread(ctx, userID, issueID)
- }
-
- func setIssueNotificationStatusReadIfUnread(ctx context.Context, userID, issueID int64) error {
- notification, err := GetIssueNotification(ctx, userID, issueID)
- // ignore if not exists
- if err != nil {
- return nil
- }
-
- if notification.Status != NotificationStatusUnread {
- return nil
- }
-
- notification.Status = NotificationStatusRead
-
- _, err = db.GetEngine(ctx).ID(notification.ID).Cols("status").Update(notification)
- return err
- }
-
- // SetRepoReadBy sets repo to be visited by given user.
- func SetRepoReadBy(ctx context.Context, userID, repoID int64) error {
- _, err := db.GetEngine(ctx).Where(builder.Eq{
- "user_id": userID,
- "status": NotificationStatusUnread,
- "source": NotificationSourceRepository,
- "repo_id": repoID,
- }).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)
- if err != nil {
- return notification, err
- }
-
- if notification.UserID != user.ID {
- return nil, fmt.Errorf("Can't change notification of another user: %d, %d", notification.UserID, user.ID)
- }
-
- notification.Status = status
-
- _, err = db.GetEngine(ctx).ID(notificationID).Update(notification)
- return notification, err
- }
-
- // GetNotificationByID return notification by ID
- func GetNotificationByID(ctx context.Context, notificationID int64) (*Notification, error) {
- notification := new(Notification)
- ok, err := db.GetEngine(ctx).
- Where("id = ?", notificationID).
- Get(notification)
- if err != nil {
- return nil, err
- }
-
- if !ok {
- return nil, db.ErrNotExist{Resource: "notification", ID: notificationID}
- }
-
- return notification, nil
- }
-
- // UpdateNotificationStatuses updates the statuses of all of a user's notifications that are of the currentStatus type to the desiredStatus
- func UpdateNotificationStatuses(ctx context.Context, user *user_model.User, currentStatus, desiredStatus NotificationStatus) error {
- n := &Notification{Status: desiredStatus, UpdatedBy: user.ID}
- _, err := db.GetEngine(ctx).
- Where("user_id = ? AND status = ?", user.ID, currentStatus).
- Cols("status", "updated_by", "updated_unix").
- Update(n)
- return err
- }
|