2016-12-30 19:44:54 +03:00
// Copyright 2016 The Gitea Authors. All rights reserved.
2022-11-27 21:20:29 +03:00
// SPDX-License-Identifier: MIT
2016-12-30 19:44:54 +03:00
2022-08-25 05:31:57 +03:00
package activities
2016-12-30 19:44:54 +03:00
import (
2021-11-19 16:39:57 +03:00
"context"
2017-01-12 07:27:09 +03:00
"fmt"
2021-11-16 21:18:25 +03:00
"net/url"
2021-02-20 00:36:43 +03:00
"strconv"
2017-12-11 07:37:04 +03:00
2021-09-19 14:49:59 +03:00
"code.gitea.io/gitea/models/db"
2022-06-13 12:37:59 +03:00
issues_model "code.gitea.io/gitea/models/issues"
2022-03-29 09:29:02 +03:00
"code.gitea.io/gitea/models/organization"
2021-12-10 04:27:50 +03:00
repo_model "code.gitea.io/gitea/models/repo"
2021-11-24 12:49:20 +03:00
user_model "code.gitea.io/gitea/models/user"
2020-01-09 14:56:32 +03:00
"code.gitea.io/gitea/modules/setting"
2019-08-15 17:46:21 +03:00
"code.gitea.io/gitea/modules/timeutil"
2020-01-09 14:56:32 +03:00
"xorm.io/builder"
2024-11-13 21:17:54 +03:00
"xorm.io/xorm/schemas"
2016-12-30 19:44:54 +03:00
)
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
2017-01-12 07:27:09 +03:00
// NotificationStatusPinned represents a pinned notification
NotificationStatusPinned
2016-12-30 19:44:54 +03:00
)
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
2021-03-01 03:47:30 +03:00
// NotificationSourceRepository is a notification for a repository
NotificationSourceRepository
2016-12-30 19:44:54 +03:00
)
// Notification represents a notification
type Notification struct {
ID int64 ` xorm:"pk autoincr" `
2024-11-13 21:17:54 +03:00
UserID int64 ` xorm:"NOT NULL" `
RepoID int64 ` xorm:"NOT NULL" `
2016-12-30 19:44:54 +03:00
2024-11-13 21:17:54 +03:00
Status NotificationStatus ` xorm:"SMALLINT NOT NULL" `
Source NotificationSource ` xorm:"SMALLINT NOT NULL" `
2016-12-30 19:44:54 +03:00
2024-11-13 21:17:54 +03:00
IssueID int64 ` xorm:"NOT NULL" `
CommitID string
2019-11-12 11:33:34 +03:00
CommentID int64
2016-12-30 19:44:54 +03:00
2024-11-13 21:17:54 +03:00
UpdatedBy int64 ` xorm:"NOT NULL" `
2016-12-30 19:44:54 +03:00
2022-06-13 12:37:59 +03:00
Issue * issues_model . Issue ` xorm:"-" `
2021-12-10 04:27:50 +03:00
Repository * repo_model . Repository ` xorm:"-" `
2022-06-13 12:37:59 +03:00
Comment * issues_model . Comment ` xorm:"-" `
2021-12-10 04:27:50 +03:00
User * user_model . User ` xorm:"-" `
2016-12-30 19:44:54 +03:00
2024-11-13 21:17:54 +03:00
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
2016-12-30 19:44:54 +03:00
}
2021-09-19 14:49:59 +03:00
func init ( ) {
db . RegisterModel ( new ( Notification ) )
}
2021-03-01 03:47:30 +03:00
// CreateRepoTransferNotification creates notification for the user a repository was transferred to
2022-11-19 11:12:33 +03:00
func CreateRepoTransferNotification ( ctx context . Context , doer , newOwner * user_model . User , repo * repo_model . Repository ) error {
2023-01-08 04:34:58 +03:00
return db . WithTx ( ctx , func ( ctx context . Context ) error {
2022-11-19 11:12:33 +03:00
var notify [ ] * Notification
2021-03-01 03:47:30 +03:00
2022-11-19 11:12:33 +03:00
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 {
2023-01-30 13:12:45 +03:00
UserID : i ,
2022-11-19 11:12:33 +03:00
RepoID : repo . ID ,
Status : NotificationStatusUnread ,
UpdatedBy : doer . ID ,
Source : NotificationSourceRepository ,
} )
}
} else {
notify = [ ] * Notification { {
UserID : newOwner . ID ,
2021-03-01 03:47:30 +03:00
RepoID : repo . ID ,
Status : NotificationStatusUnread ,
UpdatedBy : doer . ID ,
Source : NotificationSourceRepository ,
2022-11-19 11:12:33 +03:00
} }
2021-03-01 03:47:30 +03:00
}
2022-11-19 11:12:33 +03:00
return db . Insert ( ctx , notify )
} )
2021-03-01 03:47:30 +03:00
}
2022-06-13 12:37:59 +03:00
func createIssueNotification ( ctx context . Context , userID int64 , issue * issues_model . Issue , commentID , updatedByID int64 ) error {
2016-12-30 19:44:54 +03:00
notification := & Notification {
UserID : userID ,
RepoID : issue . RepoID ,
Status : NotificationStatusUnread ,
IssueID : issue . ID ,
2019-11-12 11:33:34 +03:00
CommentID : commentID ,
2016-12-30 19:44:54 +03:00
UpdatedBy : updatedByID ,
}
if issue . IsPull {
notification . Source = NotificationSourcePullRequest
} else {
notification . Source = NotificationSourceIssue
}
2022-05-20 17:08:52 +03:00
return db . Insert ( ctx , notification )
2016-12-30 19:44:54 +03:00
}
2022-05-20 17:08:52 +03:00
func updateIssueNotification ( ctx context . Context , userID , issueID , commentID , updatedByID int64 ) error {
2023-08-05 05:40:27 +03:00
notification , err := GetIssueNotification ( ctx , userID , issueID )
2016-12-30 19:44:54 +03:00
if err != nil {
return err
}
2019-11-12 11:33:34 +03:00
// 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" }
}
2016-12-30 19:44:54 +03:00
2022-05-20 17:08:52 +03:00
_ , err = db . GetEngine ( ctx ) . ID ( notification . ID ) . Cols ( cols ... ) . Update ( notification )
2016-12-30 19:44:54 +03:00
return err
}
2023-08-05 05:40:27 +03:00
// GetIssueNotification return the notification about an issue
func GetIssueNotification ( ctx context . Context , userID , issueID int64 ) ( * Notification , error ) {
2016-12-30 19:44:54 +03:00
notification := new ( Notification )
2022-05-20 17:08:52 +03:00
_ , err := db . GetEngine ( ctx ) .
2016-12-30 19:44:54 +03:00
Where ( "user_id = ?" , userID ) .
And ( "issue_id = ?" , issueID ) .
Get ( notification )
return notification , err
}
2020-01-09 14:56:32 +03:00
// LoadAttributes load Repo Issue User and Comment if not loaded
2022-11-19 11:12:33 +03:00
func ( n * Notification ) LoadAttributes ( ctx context . Context ) ( err error ) {
2021-12-10 04:27:50 +03:00
if err = n . loadRepo ( ctx ) ; err != nil {
2023-07-09 14:58:06 +03:00
return err
2020-01-09 14:56:32 +03:00
}
2021-11-19 16:39:57 +03:00
if err = n . loadIssue ( ctx ) ; err != nil {
2023-07-09 14:58:06 +03:00
return err
2020-01-09 14:56:32 +03:00
}
2022-05-20 17:08:52 +03:00
if err = n . loadUser ( ctx ) ; err != nil {
2023-07-09 14:58:06 +03:00
return err
2020-01-09 14:56:32 +03:00
}
2022-05-20 17:08:52 +03:00
if err = n . loadComment ( ctx ) ; err != nil {
2023-07-09 14:58:06 +03:00
return err
2020-01-09 14:56:32 +03:00
}
2022-06-20 13:02:49 +03:00
return err
2020-01-09 14:56:32 +03:00
}
2021-12-10 04:27:50 +03:00
func ( n * Notification ) loadRepo ( ctx context . Context ) ( err error ) {
2020-01-09 14:56:32 +03:00
if n . Repository == nil {
2022-12-03 05:48:26 +03:00
n . Repository , err = repo_model . GetRepositoryByID ( ctx , n . RepoID )
2020-01-09 14:56:32 +03:00
if err != nil {
2022-10-24 22:29:17 +03:00
return fmt . Errorf ( "getRepositoryByID [%d]: %w" , n . RepoID , err )
2020-01-09 14:56:32 +03:00
}
}
return nil
}
2021-11-19 16:39:57 +03:00
func ( n * Notification ) loadIssue ( ctx context . Context ) ( err error ) {
2021-03-01 03:47:30 +03:00
if n . Issue == nil && n . IssueID != 0 {
2022-06-13 12:37:59 +03:00
n . Issue , err = issues_model . GetIssueByID ( ctx , n . IssueID )
2020-01-09 14:56:32 +03:00
if err != nil {
2022-10-24 22:29:17 +03:00
return fmt . Errorf ( "getIssueByID [%d]: %w" , n . IssueID , err )
2020-01-09 14:56:32 +03:00
}
2022-06-13 12:37:59 +03:00
return n . Issue . LoadAttributes ( ctx )
2020-01-09 14:56:32 +03:00
}
return nil
}
2022-05-20 17:08:52 +03:00
func ( n * Notification ) loadComment ( ctx context . Context ) ( err error ) {
2021-03-01 03:47:30 +03:00
if n . Comment == nil && n . CommentID != 0 {
2022-06-13 12:37:59 +03:00
n . Comment , err = issues_model . GetCommentByID ( ctx , n . CommentID )
2020-01-09 14:56:32 +03:00
if err != nil {
2022-06-13 12:37:59 +03:00
if issues_model . IsErrCommentNotExist ( err ) {
return issues_model . ErrCommentNotExist {
2021-11-10 08:48:45 +03:00
ID : n . CommentID ,
IssueID : n . IssueID ,
}
}
return err
2020-01-09 14:56:32 +03:00
}
}
return nil
}
2022-05-20 17:08:52 +03:00
func ( n * Notification ) loadUser ( ctx context . Context ) ( err error ) {
2020-01-09 14:56:32 +03:00
if n . User == nil {
2022-12-03 05:48:26 +03:00
n . User , err = user_model . GetUserByID ( ctx , n . UserID )
2020-01-09 14:56:32 +03:00
if err != nil {
2022-10-24 22:29:17 +03:00
return fmt . Errorf ( "getUserByID [%d]: %w" , n . UserID , err )
2020-01-09 14:56:32 +03:00
}
}
return nil
}
2016-12-30 19:44:54 +03:00
// GetRepo returns the repo of the notification
2023-09-29 15:12:54 +03:00
func ( n * Notification ) GetRepo ( ctx context . Context ) ( * repo_model . Repository , error ) {
return n . Repository , n . loadRepo ( ctx )
2016-12-30 19:44:54 +03:00
}
// GetIssue returns the issue of the notification
2023-09-29 15:12:54 +03:00
func ( n * Notification ) GetIssue ( ctx context . Context ) ( * issues_model . Issue , error ) {
return n . Issue , n . loadIssue ( ctx )
2016-12-30 19:44:54 +03:00
}
2019-11-12 11:33:34 +03:00
// HTMLURL formats a URL-string to the notification
2023-09-29 15:12:54 +03:00
func ( n * Notification ) HTMLURL ( ctx context . Context ) string {
2021-03-01 03:47:30 +03:00
switch n . Source {
case NotificationSourceIssue , NotificationSourcePullRequest :
if n . Comment != nil {
2023-09-29 15:12:54 +03:00
return n . Comment . HTMLURL ( ctx )
2021-03-01 03:47:30 +03:00
}
return n . Issue . HTMLURL ( )
case NotificationSourceCommit :
2021-11-16 21:18:25 +03:00
return n . Repository . HTMLURL ( ) + "/commit/" + url . PathEscape ( n . CommitID )
2021-03-01 03:47:30 +03:00
case NotificationSourceRepository :
return n . Repository . HTMLURL ( )
2019-11-12 11:33:34 +03:00
}
2021-03-01 03:47:30 +03:00
return ""
2019-11-12 11:33:34 +03:00
}
2023-02-06 21:09:18 +03:00
// Link formats a relative URL-string to the notification
2023-09-29 15:12:54 +03:00
func ( n * Notification ) Link ( ctx context . Context ) string {
2023-02-06 21:09:18 +03:00
switch n . Source {
case NotificationSourceIssue , NotificationSourcePullRequest :
if n . Comment != nil {
2023-09-29 15:12:54 +03:00
return n . Comment . Link ( ctx )
2023-02-06 21:09:18 +03:00
}
return n . Issue . Link ( )
case NotificationSourceCommit :
return n . Repository . Link ( ) + "/commit/" + url . PathEscape ( n . CommitID )
case NotificationSourceRepository :
return n . Repository . Link ( )
}
return ""
}
2020-01-09 14:56:32 +03:00
// APIURL formats a URL-string to the notification
func ( n * Notification ) APIURL ( ) string {
2021-02-20 00:36:43 +03:00
return setting . AppURL + "api/v1/notifications/threads/" + strconv . FormatInt ( n . ID , 10 )
2020-01-09 14:56:32 +03:00
}
2023-11-24 06:49:41 +03:00
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
}
2024-06-23 10:50:10 +03:00
// 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.
2023-11-24 06:49:41 +03:00
func GetUIDsAndNotificationCounts ( ctx context . Context , since , until timeutil . TimeStamp ) ( [ ] UserIDCount , error ) {
2024-06-23 10:50:10 +03:00
sql := ` SELECT user_id, sum(case when status= ? then 1 else 0 end) AS count FROM notification ` +
2023-11-24 06:49:41 +03:00
` WHERE user_id IN (SELECT user_id FROM notification WHERE updated_unix >= ? AND ` +
2024-06-23 10:50:10 +03:00
` updated_unix < ?) GROUP BY user_id `
2023-11-24 06:49:41 +03:00
var res [ ] UserIDCount
2024-06-23 10:50:10 +03:00
return res , db . GetEngine ( ctx ) . SQL ( sql , NotificationStatusUnread , since , until ) . Find ( & res )
2023-11-24 06:49:41 +03:00
}
2022-06-13 12:37:59 +03:00
// SetIssueReadBy sets issue to be read by given user.
func SetIssueReadBy ( ctx context . Context , issueID , userID int64 ) error {
2023-08-05 05:40:27 +03:00
if err := issues_model . UpdateIssueUserByRead ( ctx , userID , issueID ) ; err != nil {
2022-06-13 12:37:59 +03:00
return err
}
return setIssueNotificationStatusReadIfUnread ( ctx , userID , issueID )
}
2022-05-20 17:08:52 +03:00
func setIssueNotificationStatusReadIfUnread ( ctx context . Context , userID , issueID int64 ) error {
2023-08-05 05:40:27 +03:00
notification , err := GetIssueNotification ( ctx , userID , issueID )
2016-12-30 19:44:54 +03:00
// ignore if not exists
if err != nil {
return nil
}
2017-01-12 07:27:09 +03:00
if notification . Status != NotificationStatusUnread {
return nil
}
2016-12-30 19:44:54 +03:00
notification . Status = NotificationStatusRead
2023-08-05 05:40:27 +03:00
_ , err = db . GetEngine ( ctx ) . ID ( notification . ID ) . Cols ( "status" ) . Update ( notification )
2016-12-30 19:44:54 +03:00
return err
}
2017-01-12 07:27:09 +03:00
2022-05-20 17:08:52 +03:00
// 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 {
2021-03-01 03:47:30 +03:00
"user_id" : userID ,
"status" : NotificationStatusUnread ,
"source" : NotificationSourceRepository ,
"repo_id" : repoID ,
} ) . Cols ( "status" ) . Update ( & Notification { Status : NotificationStatusRead } )
return err
}
2017-01-12 07:27:09 +03:00
// SetNotificationStatus change the notification status
2022-11-19 11:12:33 +03:00
func SetNotificationStatus ( ctx context . Context , notificationID int64 , user * user_model . User , status NotificationStatus ) ( * Notification , error ) {
notification , err := GetNotificationByID ( ctx , notificationID )
2017-01-12 07:27:09 +03:00
if err != nil {
2021-09-18 02:40:50 +03:00
return notification , err
2017-01-12 07:27:09 +03:00
}
if notification . UserID != user . ID {
2021-09-18 02:40:50 +03:00
return nil , fmt . Errorf ( "Can't change notification of another user: %d, %d" , notification . UserID , user . ID )
2017-01-12 07:27:09 +03:00
}
notification . Status = status
2022-11-19 11:12:33 +03:00
_ , err = db . GetEngine ( ctx ) . ID ( notificationID ) . Update ( notification )
2021-09-18 02:40:50 +03:00
return notification , err
2017-01-12 07:27:09 +03:00
}
2020-01-09 14:56:32 +03:00
// GetNotificationByID return notification by ID
2022-11-19 11:12:33 +03:00
func GetNotificationByID ( ctx context . Context , notificationID int64 ) ( * Notification , error ) {
2017-01-12 07:27:09 +03:00
notification := new ( Notification )
2022-05-20 17:08:52 +03:00
ok , err := db . GetEngine ( ctx ) .
2017-01-12 07:27:09 +03:00
Where ( "id = ?" , notificationID ) .
Get ( notification )
if err != nil {
return nil , err
}
if ! ok {
2022-10-18 08:50:37 +03:00
return nil , db . ErrNotExist { Resource : "notification" , ID : notificationID }
2017-01-12 07:27:09 +03:00
}
return notification , nil
}
2017-12-07 08:52:57 +03:00
// UpdateNotificationStatuses updates the statuses of all of a user's notifications that are of the currentStatus type to the desiredStatus
2022-11-19 11:12:33 +03:00
func UpdateNotificationStatuses ( ctx context . Context , user * user_model . User , currentStatus , desiredStatus NotificationStatus ) error {
2017-12-07 08:52:57 +03:00
n := & Notification { Status : desiredStatus , UpdatedBy : user . ID }
2022-11-19 11:12:33 +03:00
_ , err := db . GetEngine ( ctx ) .
2017-12-07 08:52:57 +03:00
Where ( "user_id = ? AND status = ?" , user . ID , currentStatus ) .
Cols ( "status" , "updated_by" , "updated_unix" ) .
Update ( n )
return err
}