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 }