Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

notification_list.go 12KB

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