gitea源码

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480
  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. return db.WithTx(ctx, func(ctx context.Context) error {
  66. return createOrUpdateIssueNotifications(ctx, issueID, commentID, notificationAuthorID, receiverID)
  67. })
  68. }
  69. func createOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, notificationAuthorID, receiverID int64) error {
  70. // init
  71. var toNotify container.Set[int64]
  72. notifications, err := db.Find[Notification](ctx, FindNotificationOptions{
  73. IssueID: issueID,
  74. })
  75. if err != nil {
  76. return err
  77. }
  78. issue, err := issues_model.GetIssueByID(ctx, issueID)
  79. if err != nil {
  80. return err
  81. }
  82. if receiverID > 0 {
  83. toNotify = make(container.Set[int64], 1)
  84. toNotify.Add(receiverID)
  85. } else {
  86. toNotify = make(container.Set[int64], 32)
  87. issueWatches, err := issues_model.GetIssueWatchersIDs(ctx, issueID, true)
  88. if err != nil {
  89. return err
  90. }
  91. toNotify.AddMultiple(issueWatches...)
  92. if !(issue.IsPull && issues_model.HasWorkInProgressPrefix(issue.Title)) {
  93. repoWatches, err := repo_model.GetRepoWatchersIDs(ctx, issue.RepoID)
  94. if err != nil {
  95. return err
  96. }
  97. toNotify.AddMultiple(repoWatches...)
  98. }
  99. issueParticipants, err := issue.GetParticipantIDsByIssue(ctx)
  100. if err != nil {
  101. return err
  102. }
  103. toNotify.AddMultiple(issueParticipants...)
  104. // dont notify user who cause notification
  105. delete(toNotify, notificationAuthorID)
  106. // explicit unwatch on issue
  107. issueUnWatches, err := issues_model.GetIssueWatchersIDs(ctx, issueID, false)
  108. if err != nil {
  109. return err
  110. }
  111. for _, id := range issueUnWatches {
  112. toNotify.Remove(id)
  113. }
  114. }
  115. err = issue.LoadRepo(ctx)
  116. if err != nil {
  117. return err
  118. }
  119. // notify
  120. for userID := range toNotify {
  121. issue.Repo.Units = nil
  122. user, err := user_model.GetUserByID(ctx, userID)
  123. if err != nil {
  124. if user_model.IsErrUserNotExist(err) {
  125. continue
  126. }
  127. return err
  128. }
  129. if issue.IsPull && !access_model.CheckRepoUnitUser(ctx, issue.Repo, user, unit.TypePullRequests) {
  130. continue
  131. }
  132. if !issue.IsPull && !access_model.CheckRepoUnitUser(ctx, issue.Repo, user, unit.TypeIssues) {
  133. continue
  134. }
  135. if notificationExists(notifications, issue.ID, userID) {
  136. if err = updateIssueNotification(ctx, userID, issue.ID, commentID, notificationAuthorID); err != nil {
  137. return err
  138. }
  139. continue
  140. }
  141. if err = createIssueNotification(ctx, userID, issue, commentID, notificationAuthorID); err != nil {
  142. return err
  143. }
  144. }
  145. return nil
  146. }
  147. // NotificationList contains a list of notifications
  148. type NotificationList []*Notification
  149. // LoadAttributes load Repo Issue User and Comment if not loaded
  150. func (nl NotificationList) LoadAttributes(ctx context.Context) error {
  151. if _, _, err := nl.LoadRepos(ctx); err != nil {
  152. return err
  153. }
  154. if _, err := nl.LoadIssues(ctx); err != nil {
  155. return err
  156. }
  157. if _, err := nl.LoadUsers(ctx); err != nil {
  158. return err
  159. }
  160. if _, err := nl.LoadComments(ctx); err != nil {
  161. return err
  162. }
  163. return nil
  164. }
  165. func (nl NotificationList) getPendingRepoIDs() []int64 {
  166. return container.FilterSlice(nl, func(n *Notification) (int64, bool) {
  167. if n.Repository != nil {
  168. return 0, false
  169. }
  170. return n.RepoID, true
  171. })
  172. }
  173. // LoadRepos loads repositories from database
  174. func (nl NotificationList) LoadRepos(ctx context.Context) (repo_model.RepositoryList, []int, error) {
  175. if len(nl) == 0 {
  176. return repo_model.RepositoryList{}, []int{}, nil
  177. }
  178. repoIDs := nl.getPendingRepoIDs()
  179. repos := make(map[int64]*repo_model.Repository, len(repoIDs))
  180. left := len(repoIDs)
  181. for left > 0 {
  182. limit := min(left, db.DefaultMaxInSize)
  183. rows, err := db.GetEngine(ctx).
  184. In("id", repoIDs[:limit]).
  185. Rows(new(repo_model.Repository))
  186. if err != nil {
  187. return nil, nil, err
  188. }
  189. for rows.Next() {
  190. var repo repo_model.Repository
  191. err = rows.Scan(&repo)
  192. if err != nil {
  193. rows.Close()
  194. return nil, nil, err
  195. }
  196. repos[repo.ID] = &repo
  197. }
  198. _ = rows.Close()
  199. left -= limit
  200. repoIDs = repoIDs[limit:]
  201. }
  202. failed := []int{}
  203. reposList := make(repo_model.RepositoryList, 0, len(repoIDs))
  204. for i, notification := range nl {
  205. if notification.Repository == nil {
  206. notification.Repository = repos[notification.RepoID]
  207. }
  208. if notification.Repository == nil {
  209. log.Error("Notification[%d]: RepoID: %d not found", notification.ID, notification.RepoID)
  210. failed = append(failed, i)
  211. continue
  212. }
  213. var found bool
  214. for _, r := range reposList {
  215. if r.ID == notification.RepoID {
  216. found = true
  217. break
  218. }
  219. }
  220. if !found {
  221. reposList = append(reposList, notification.Repository)
  222. }
  223. }
  224. return reposList, failed, nil
  225. }
  226. func (nl NotificationList) getPendingIssueIDs() []int64 {
  227. ids := make(container.Set[int64], len(nl))
  228. for _, notification := range nl {
  229. if notification.Issue != nil {
  230. continue
  231. }
  232. ids.Add(notification.IssueID)
  233. }
  234. return ids.Values()
  235. }
  236. // LoadIssues loads issues from database
  237. func (nl NotificationList) LoadIssues(ctx context.Context) ([]int, error) {
  238. if len(nl) == 0 {
  239. return []int{}, nil
  240. }
  241. issueIDs := nl.getPendingIssueIDs()
  242. issues := make(map[int64]*issues_model.Issue, len(issueIDs))
  243. left := len(issueIDs)
  244. for left > 0 {
  245. limit := min(left, db.DefaultMaxInSize)
  246. rows, err := db.GetEngine(ctx).
  247. In("id", issueIDs[:limit]).
  248. Rows(new(issues_model.Issue))
  249. if err != nil {
  250. return nil, err
  251. }
  252. for rows.Next() {
  253. var issue issues_model.Issue
  254. err = rows.Scan(&issue)
  255. if err != nil {
  256. rows.Close()
  257. return nil, err
  258. }
  259. issues[issue.ID] = &issue
  260. }
  261. _ = rows.Close()
  262. left -= limit
  263. issueIDs = issueIDs[limit:]
  264. }
  265. failures := []int{}
  266. for i, notification := range nl {
  267. if notification.Issue == nil {
  268. notification.Issue = issues[notification.IssueID]
  269. if notification.Issue == nil {
  270. if notification.IssueID != 0 {
  271. log.Error("Notification[%d]: IssueID: %d Not Found", notification.ID, notification.IssueID)
  272. failures = append(failures, i)
  273. }
  274. continue
  275. }
  276. notification.Issue.Repo = notification.Repository
  277. }
  278. }
  279. return failures, nil
  280. }
  281. // Without returns the notification list without the failures
  282. func (nl NotificationList) Without(failures []int) NotificationList {
  283. if len(failures) == 0 {
  284. return nl
  285. }
  286. remaining := make([]*Notification, 0, len(nl))
  287. last := -1
  288. var i int
  289. for _, i = range failures {
  290. remaining = append(remaining, nl[last+1:i]...)
  291. last = i
  292. }
  293. if len(nl) > i {
  294. remaining = append(remaining, nl[i+1:]...)
  295. }
  296. return remaining
  297. }
  298. func (nl NotificationList) getPendingCommentIDs() []int64 {
  299. ids := make(container.Set[int64], len(nl))
  300. for _, notification := range nl {
  301. if notification.CommentID == 0 || notification.Comment != nil {
  302. continue
  303. }
  304. ids.Add(notification.CommentID)
  305. }
  306. return ids.Values()
  307. }
  308. func (nl NotificationList) getUserIDs() []int64 {
  309. ids := make(container.Set[int64], len(nl))
  310. for _, notification := range nl {
  311. if notification.UserID == 0 || notification.User != nil {
  312. continue
  313. }
  314. ids.Add(notification.UserID)
  315. }
  316. return ids.Values()
  317. }
  318. // LoadUsers loads users from database
  319. func (nl NotificationList) LoadUsers(ctx context.Context) ([]int, error) {
  320. if len(nl) == 0 {
  321. return []int{}, nil
  322. }
  323. userIDs := nl.getUserIDs()
  324. users := make(map[int64]*user_model.User, len(userIDs))
  325. left := len(userIDs)
  326. for left > 0 {
  327. limit := min(left, db.DefaultMaxInSize)
  328. rows, err := db.GetEngine(ctx).
  329. In("id", userIDs[:limit]).
  330. Rows(new(user_model.User))
  331. if err != nil {
  332. return nil, err
  333. }
  334. for rows.Next() {
  335. var user user_model.User
  336. err = rows.Scan(&user)
  337. if err != nil {
  338. rows.Close()
  339. return nil, err
  340. }
  341. users[user.ID] = &user
  342. }
  343. _ = rows.Close()
  344. left -= limit
  345. userIDs = userIDs[limit:]
  346. }
  347. failures := []int{}
  348. for i, notification := range nl {
  349. if notification.UserID > 0 && notification.User == nil && users[notification.UserID] != nil {
  350. notification.User = users[notification.UserID]
  351. if notification.User == nil {
  352. log.Error("Notification[%d]: UserID[%d] failed to load", notification.ID, notification.UserID)
  353. failures = append(failures, i)
  354. continue
  355. }
  356. }
  357. }
  358. return failures, nil
  359. }
  360. // LoadComments loads comments from database
  361. func (nl NotificationList) LoadComments(ctx context.Context) ([]int, error) {
  362. if len(nl) == 0 {
  363. return []int{}, nil
  364. }
  365. commentIDs := nl.getPendingCommentIDs()
  366. comments := make(map[int64]*issues_model.Comment, len(commentIDs))
  367. left := len(commentIDs)
  368. for left > 0 {
  369. limit := min(left, db.DefaultMaxInSize)
  370. rows, err := db.GetEngine(ctx).
  371. In("id", commentIDs[:limit]).
  372. Rows(new(issues_model.Comment))
  373. if err != nil {
  374. return nil, err
  375. }
  376. for rows.Next() {
  377. var comment issues_model.Comment
  378. err = rows.Scan(&comment)
  379. if err != nil {
  380. rows.Close()
  381. return nil, err
  382. }
  383. comments[comment.ID] = &comment
  384. }
  385. _ = rows.Close()
  386. left -= limit
  387. commentIDs = commentIDs[limit:]
  388. }
  389. failures := []int{}
  390. for i, notification := range nl {
  391. if notification.CommentID > 0 && notification.Comment == nil && comments[notification.CommentID] != nil {
  392. notification.Comment = comments[notification.CommentID]
  393. if notification.Comment == nil {
  394. log.Error("Notification[%d]: CommentID[%d] failed to load", notification.ID, notification.CommentID)
  395. failures = append(failures, i)
  396. continue
  397. }
  398. notification.Comment.Issue = notification.Issue
  399. }
  400. }
  401. return failures, nil
  402. }
  403. // LoadIssuePullRequests loads all issues' pull requests if possible
  404. func (nl NotificationList) LoadIssuePullRequests(ctx context.Context) error {
  405. issues := make(map[int64]*issues_model.Issue, len(nl))
  406. for _, notification := range nl {
  407. if notification.Issue != nil && notification.Issue.IsPull && notification.Issue.PullRequest == nil {
  408. issues[notification.Issue.ID] = notification.Issue
  409. }
  410. }
  411. if len(issues) == 0 {
  412. return nil
  413. }
  414. pulls, err := issues_model.GetPullRequestByIssueIDs(ctx, util.KeysOfMap(issues))
  415. if err != nil {
  416. return err
  417. }
  418. for _, pull := range pulls {
  419. if issue := issues[pull.IssueID]; issue != nil {
  420. issue.PullRequest = pull
  421. issue.PullRequest.Issue = issue
  422. }
  423. }
  424. return nil
  425. }