code.gitea.io/gitea@v1.22.3/models/activities/notification_list.go (about) 1 // Copyright 2024 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package activities 5 6 import ( 7 "context" 8 9 "code.gitea.io/gitea/models/db" 10 issues_model "code.gitea.io/gitea/models/issues" 11 access_model "code.gitea.io/gitea/models/perm/access" 12 repo_model "code.gitea.io/gitea/models/repo" 13 "code.gitea.io/gitea/models/unit" 14 user_model "code.gitea.io/gitea/models/user" 15 "code.gitea.io/gitea/modules/container" 16 "code.gitea.io/gitea/modules/log" 17 "code.gitea.io/gitea/modules/util" 18 19 "xorm.io/builder" 20 ) 21 22 // FindNotificationOptions represent the filters for notifications. If an ID is 0 it will be ignored. 23 type FindNotificationOptions struct { 24 db.ListOptions 25 UserID int64 26 RepoID int64 27 IssueID int64 28 Status []NotificationStatus 29 Source []NotificationSource 30 UpdatedAfterUnix int64 31 UpdatedBeforeUnix int64 32 } 33 34 // ToCond will convert each condition into a xorm-Cond 35 func (opts FindNotificationOptions) ToConds() builder.Cond { 36 cond := builder.NewCond() 37 if opts.UserID != 0 { 38 cond = cond.And(builder.Eq{"notification.user_id": opts.UserID}) 39 } 40 if opts.RepoID != 0 { 41 cond = cond.And(builder.Eq{"notification.repo_id": opts.RepoID}) 42 } 43 if opts.IssueID != 0 { 44 cond = cond.And(builder.Eq{"notification.issue_id": opts.IssueID}) 45 } 46 if len(opts.Status) > 0 { 47 if len(opts.Status) == 1 { 48 cond = cond.And(builder.Eq{"notification.status": opts.Status[0]}) 49 } else { 50 cond = cond.And(builder.In("notification.status", opts.Status)) 51 } 52 } 53 if len(opts.Source) > 0 { 54 cond = cond.And(builder.In("notification.source", opts.Source)) 55 } 56 if opts.UpdatedAfterUnix != 0 { 57 cond = cond.And(builder.Gte{"notification.updated_unix": opts.UpdatedAfterUnix}) 58 } 59 if opts.UpdatedBeforeUnix != 0 { 60 cond = cond.And(builder.Lte{"notification.updated_unix": opts.UpdatedBeforeUnix}) 61 } 62 return cond 63 } 64 65 func (opts FindNotificationOptions) ToOrders() string { 66 return "notification.updated_unix DESC" 67 } 68 69 // CreateOrUpdateIssueNotifications creates an issue notification 70 // for each watcher, or updates it if already exists 71 // receiverID > 0 just send to receiver, else send to all watcher 72 func CreateOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, notificationAuthorID, receiverID int64) error { 73 ctx, committer, err := db.TxContext(ctx) 74 if err != nil { 75 return err 76 } 77 defer committer.Close() 78 79 if err := createOrUpdateIssueNotifications(ctx, issueID, commentID, notificationAuthorID, receiverID); err != nil { 80 return err 81 } 82 83 return committer.Commit() 84 } 85 86 func createOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, notificationAuthorID, receiverID int64) error { 87 // init 88 var toNotify container.Set[int64] 89 notifications, err := db.Find[Notification](ctx, FindNotificationOptions{ 90 IssueID: issueID, 91 }) 92 if err != nil { 93 return err 94 } 95 96 issue, err := issues_model.GetIssueByID(ctx, issueID) 97 if err != nil { 98 return err 99 } 100 101 if receiverID > 0 { 102 toNotify = make(container.Set[int64], 1) 103 toNotify.Add(receiverID) 104 } else { 105 toNotify = make(container.Set[int64], 32) 106 issueWatches, err := issues_model.GetIssueWatchersIDs(ctx, issueID, true) 107 if err != nil { 108 return err 109 } 110 toNotify.AddMultiple(issueWatches...) 111 if !(issue.IsPull && issues_model.HasWorkInProgressPrefix(issue.Title)) { 112 repoWatches, err := repo_model.GetRepoWatchersIDs(ctx, issue.RepoID) 113 if err != nil { 114 return err 115 } 116 toNotify.AddMultiple(repoWatches...) 117 } 118 issueParticipants, err := issue.GetParticipantIDsByIssue(ctx) 119 if err != nil { 120 return err 121 } 122 toNotify.AddMultiple(issueParticipants...) 123 124 // dont notify user who cause notification 125 delete(toNotify, notificationAuthorID) 126 // explicit unwatch on issue 127 issueUnWatches, err := issues_model.GetIssueWatchersIDs(ctx, issueID, false) 128 if err != nil { 129 return err 130 } 131 for _, id := range issueUnWatches { 132 toNotify.Remove(id) 133 } 134 } 135 136 err = issue.LoadRepo(ctx) 137 if err != nil { 138 return err 139 } 140 141 // notify 142 for userID := range toNotify { 143 issue.Repo.Units = nil 144 user, err := user_model.GetUserByID(ctx, userID) 145 if err != nil { 146 if user_model.IsErrUserNotExist(err) { 147 continue 148 } 149 150 return err 151 } 152 if issue.IsPull && !access_model.CheckRepoUnitUser(ctx, issue.Repo, user, unit.TypePullRequests) { 153 continue 154 } 155 if !issue.IsPull && !access_model.CheckRepoUnitUser(ctx, issue.Repo, user, unit.TypeIssues) { 156 continue 157 } 158 159 if notificationExists(notifications, issue.ID, userID) { 160 if err = updateIssueNotification(ctx, userID, issue.ID, commentID, notificationAuthorID); err != nil { 161 return err 162 } 163 continue 164 } 165 if err = createIssueNotification(ctx, userID, issue, commentID, notificationAuthorID); err != nil { 166 return err 167 } 168 } 169 return nil 170 } 171 172 // NotificationList contains a list of notifications 173 type NotificationList []*Notification 174 175 // LoadAttributes load Repo Issue User and Comment if not loaded 176 func (nl NotificationList) LoadAttributes(ctx context.Context) error { 177 if _, _, err := nl.LoadRepos(ctx); err != nil { 178 return err 179 } 180 if _, err := nl.LoadIssues(ctx); err != nil { 181 return err 182 } 183 if _, err := nl.LoadUsers(ctx); err != nil { 184 return err 185 } 186 if _, err := nl.LoadComments(ctx); err != nil { 187 return err 188 } 189 return nil 190 } 191 192 func (nl NotificationList) getPendingRepoIDs() []int64 { 193 return container.FilterSlice(nl, func(n *Notification) (int64, bool) { 194 if n.Repository != nil { 195 return 0, false 196 } 197 return n.RepoID, true 198 }) 199 } 200 201 // LoadRepos loads repositories from database 202 func (nl NotificationList) LoadRepos(ctx context.Context) (repo_model.RepositoryList, []int, error) { 203 if len(nl) == 0 { 204 return repo_model.RepositoryList{}, []int{}, nil 205 } 206 207 repoIDs := nl.getPendingRepoIDs() 208 repos := make(map[int64]*repo_model.Repository, len(repoIDs)) 209 left := len(repoIDs) 210 for left > 0 { 211 limit := db.DefaultMaxInSize 212 if left < limit { 213 limit = left 214 } 215 rows, err := db.GetEngine(ctx). 216 In("id", repoIDs[:limit]). 217 Rows(new(repo_model.Repository)) 218 if err != nil { 219 return nil, nil, err 220 } 221 222 for rows.Next() { 223 var repo repo_model.Repository 224 err = rows.Scan(&repo) 225 if err != nil { 226 rows.Close() 227 return nil, nil, err 228 } 229 230 repos[repo.ID] = &repo 231 } 232 _ = rows.Close() 233 234 left -= limit 235 repoIDs = repoIDs[limit:] 236 } 237 238 failed := []int{} 239 240 reposList := make(repo_model.RepositoryList, 0, len(repoIDs)) 241 for i, notification := range nl { 242 if notification.Repository == nil { 243 notification.Repository = repos[notification.RepoID] 244 } 245 if notification.Repository == nil { 246 log.Error("Notification[%d]: RepoID: %d not found", notification.ID, notification.RepoID) 247 failed = append(failed, i) 248 continue 249 } 250 var found bool 251 for _, r := range reposList { 252 if r.ID == notification.RepoID { 253 found = true 254 break 255 } 256 } 257 if !found { 258 reposList = append(reposList, notification.Repository) 259 } 260 } 261 return reposList, failed, nil 262 } 263 264 func (nl NotificationList) getPendingIssueIDs() []int64 { 265 ids := make(container.Set[int64], len(nl)) 266 for _, notification := range nl { 267 if notification.Issue != nil { 268 continue 269 } 270 ids.Add(notification.IssueID) 271 } 272 return ids.Values() 273 } 274 275 // LoadIssues loads issues from database 276 func (nl NotificationList) LoadIssues(ctx context.Context) ([]int, error) { 277 if len(nl) == 0 { 278 return []int{}, nil 279 } 280 281 issueIDs := nl.getPendingIssueIDs() 282 issues := make(map[int64]*issues_model.Issue, len(issueIDs)) 283 left := len(issueIDs) 284 for left > 0 { 285 limit := db.DefaultMaxInSize 286 if left < limit { 287 limit = left 288 } 289 rows, err := db.GetEngine(ctx). 290 In("id", issueIDs[:limit]). 291 Rows(new(issues_model.Issue)) 292 if err != nil { 293 return nil, err 294 } 295 296 for rows.Next() { 297 var issue issues_model.Issue 298 err = rows.Scan(&issue) 299 if err != nil { 300 rows.Close() 301 return nil, err 302 } 303 304 issues[issue.ID] = &issue 305 } 306 _ = rows.Close() 307 308 left -= limit 309 issueIDs = issueIDs[limit:] 310 } 311 312 failures := []int{} 313 314 for i, notification := range nl { 315 if notification.Issue == nil { 316 notification.Issue = issues[notification.IssueID] 317 if notification.Issue == nil { 318 if notification.IssueID != 0 { 319 log.Error("Notification[%d]: IssueID: %d Not Found", notification.ID, notification.IssueID) 320 failures = append(failures, i) 321 } 322 continue 323 } 324 notification.Issue.Repo = notification.Repository 325 } 326 } 327 return failures, nil 328 } 329 330 // Without returns the notification list without the failures 331 func (nl NotificationList) Without(failures []int) NotificationList { 332 if len(failures) == 0 { 333 return nl 334 } 335 remaining := make([]*Notification, 0, len(nl)) 336 last := -1 337 var i int 338 for _, i = range failures { 339 remaining = append(remaining, nl[last+1:i]...) 340 last = i 341 } 342 if len(nl) > i { 343 remaining = append(remaining, nl[i+1:]...) 344 } 345 return remaining 346 } 347 348 func (nl NotificationList) getPendingCommentIDs() []int64 { 349 ids := make(container.Set[int64], len(nl)) 350 for _, notification := range nl { 351 if notification.CommentID == 0 || notification.Comment != nil { 352 continue 353 } 354 ids.Add(notification.CommentID) 355 } 356 return ids.Values() 357 } 358 359 func (nl NotificationList) getUserIDs() []int64 { 360 ids := make(container.Set[int64], len(nl)) 361 for _, notification := range nl { 362 if notification.UserID == 0 || notification.User != nil { 363 continue 364 } 365 ids.Add(notification.UserID) 366 } 367 return ids.Values() 368 } 369 370 // LoadUsers loads users from database 371 func (nl NotificationList) LoadUsers(ctx context.Context) ([]int, error) { 372 if len(nl) == 0 { 373 return []int{}, nil 374 } 375 376 userIDs := nl.getUserIDs() 377 users := make(map[int64]*user_model.User, len(userIDs)) 378 left := len(userIDs) 379 for left > 0 { 380 limit := db.DefaultMaxInSize 381 if left < limit { 382 limit = left 383 } 384 rows, err := db.GetEngine(ctx). 385 In("id", userIDs[:limit]). 386 Rows(new(user_model.User)) 387 if err != nil { 388 return nil, err 389 } 390 391 for rows.Next() { 392 var user user_model.User 393 err = rows.Scan(&user) 394 if err != nil { 395 rows.Close() 396 return nil, err 397 } 398 399 users[user.ID] = &user 400 } 401 _ = rows.Close() 402 403 left -= limit 404 userIDs = userIDs[limit:] 405 } 406 407 failures := []int{} 408 for i, notification := range nl { 409 if notification.UserID > 0 && notification.User == nil && users[notification.UserID] != nil { 410 notification.User = users[notification.UserID] 411 if notification.User == nil { 412 log.Error("Notification[%d]: UserID[%d] failed to load", notification.ID, notification.UserID) 413 failures = append(failures, i) 414 continue 415 } 416 } 417 } 418 return failures, nil 419 } 420 421 // LoadComments loads comments from database 422 func (nl NotificationList) LoadComments(ctx context.Context) ([]int, error) { 423 if len(nl) == 0 { 424 return []int{}, nil 425 } 426 427 commentIDs := nl.getPendingCommentIDs() 428 comments := make(map[int64]*issues_model.Comment, len(commentIDs)) 429 left := len(commentIDs) 430 for left > 0 { 431 limit := db.DefaultMaxInSize 432 if left < limit { 433 limit = left 434 } 435 rows, err := db.GetEngine(ctx). 436 In("id", commentIDs[:limit]). 437 Rows(new(issues_model.Comment)) 438 if err != nil { 439 return nil, err 440 } 441 442 for rows.Next() { 443 var comment issues_model.Comment 444 err = rows.Scan(&comment) 445 if err != nil { 446 rows.Close() 447 return nil, err 448 } 449 450 comments[comment.ID] = &comment 451 } 452 _ = rows.Close() 453 454 left -= limit 455 commentIDs = commentIDs[limit:] 456 } 457 458 failures := []int{} 459 for i, notification := range nl { 460 if notification.CommentID > 0 && notification.Comment == nil && comments[notification.CommentID] != nil { 461 notification.Comment = comments[notification.CommentID] 462 if notification.Comment == nil { 463 log.Error("Notification[%d]: CommentID[%d] failed to load", notification.ID, notification.CommentID) 464 failures = append(failures, i) 465 continue 466 } 467 notification.Comment.Issue = notification.Issue 468 } 469 } 470 return failures, nil 471 } 472 473 // LoadIssuePullRequests loads all issues' pull requests if possible 474 func (nl NotificationList) LoadIssuePullRequests(ctx context.Context) error { 475 issues := make(map[int64]*issues_model.Issue, len(nl)) 476 for _, notification := range nl { 477 if notification.Issue != nil && notification.Issue.IsPull && notification.Issue.PullRequest == nil { 478 issues[notification.Issue.ID] = notification.Issue 479 } 480 } 481 482 if len(issues) == 0 { 483 return nil 484 } 485 486 pulls, err := issues_model.GetPullRequestByIssueIDs(ctx, util.KeysOfMap(issues)) 487 if err != nil { 488 return err 489 } 490 491 for _, pull := range pulls { 492 if issue := issues[pull.IssueID]; issue != nil { 493 issue.PullRequest = pull 494 issue.PullRequest.Issue = issue 495 } 496 } 497 498 return nil 499 }