mirror of https://github.com/go-gitea/gitea.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
418 lines
13 KiB
418 lines
13 KiB
// 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 |
|
}
|
|
|