You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

717 lines
18 KiB

  1. // Copyright 2016 The Gitea Authors. All rights reserved.
  2. // Use of this source code is governed by a MIT-style
  3. // license that can be found in the LICENSE file.
  4. package models
  5. import (
  6. "fmt"
  7. "path"
  8. "code.gitea.io/gitea/modules/setting"
  9. api "code.gitea.io/gitea/modules/structs"
  10. "code.gitea.io/gitea/modules/timeutil"
  11. "xorm.io/builder"
  12. "xorm.io/xorm"
  13. )
  14. type (
  15. // NotificationStatus is the status of the notification (read or unread)
  16. NotificationStatus uint8
  17. // NotificationSource is the source of the notification (issue, PR, commit, etc)
  18. NotificationSource uint8
  19. )
  20. const (
  21. // NotificationStatusUnread represents an unread notification
  22. NotificationStatusUnread NotificationStatus = iota + 1
  23. // NotificationStatusRead represents a read notification
  24. NotificationStatusRead
  25. // NotificationStatusPinned represents a pinned notification
  26. NotificationStatusPinned
  27. )
  28. const (
  29. // NotificationSourceIssue is a notification of an issue
  30. NotificationSourceIssue NotificationSource = iota + 1
  31. // NotificationSourcePullRequest is a notification of a pull request
  32. NotificationSourcePullRequest
  33. // NotificationSourceCommit is a notification of a commit
  34. NotificationSourceCommit
  35. )
  36. // Notification represents a notification
  37. type Notification struct {
  38. ID int64 `xorm:"pk autoincr"`
  39. UserID int64 `xorm:"INDEX NOT NULL"`
  40. RepoID int64 `xorm:"INDEX NOT NULL"`
  41. Status NotificationStatus `xorm:"SMALLINT INDEX NOT NULL"`
  42. Source NotificationSource `xorm:"SMALLINT INDEX NOT NULL"`
  43. IssueID int64 `xorm:"INDEX NOT NULL"`
  44. CommitID string `xorm:"INDEX"`
  45. CommentID int64
  46. UpdatedBy int64 `xorm:"INDEX NOT NULL"`
  47. Issue *Issue `xorm:"-"`
  48. Repository *Repository `xorm:"-"`
  49. Comment *Comment `xorm:"-"`
  50. User *User `xorm:"-"`
  51. CreatedUnix timeutil.TimeStamp `xorm:"created INDEX NOT NULL"`
  52. UpdatedUnix timeutil.TimeStamp `xorm:"updated INDEX NOT NULL"`
  53. }
  54. // FindNotificationOptions represent the filters for notifications. If an ID is 0 it will be ignored.
  55. type FindNotificationOptions struct {
  56. UserID int64
  57. RepoID int64
  58. IssueID int64
  59. Status NotificationStatus
  60. UpdatedAfterUnix int64
  61. UpdatedBeforeUnix int64
  62. }
  63. // ToCond will convert each condition into a xorm-Cond
  64. func (opts *FindNotificationOptions) ToCond() builder.Cond {
  65. cond := builder.NewCond()
  66. if opts.UserID != 0 {
  67. cond = cond.And(builder.Eq{"notification.user_id": opts.UserID})
  68. }
  69. if opts.RepoID != 0 {
  70. cond = cond.And(builder.Eq{"notification.repo_id": opts.RepoID})
  71. }
  72. if opts.IssueID != 0 {
  73. cond = cond.And(builder.Eq{"notification.issue_id": opts.IssueID})
  74. }
  75. if opts.Status != 0 {
  76. cond = cond.And(builder.Eq{"notification.status": opts.Status})
  77. }
  78. if opts.UpdatedAfterUnix != 0 {
  79. cond = cond.And(builder.Gte{"notification.updated_unix": opts.UpdatedAfterUnix})
  80. }
  81. if opts.UpdatedBeforeUnix != 0 {
  82. cond = cond.And(builder.Lte{"notification.updated_unix": opts.UpdatedBeforeUnix})
  83. }
  84. return cond
  85. }
  86. // ToSession will convert the given options to a xorm Session by using the conditions from ToCond and joining with issue table if required
  87. func (opts *FindNotificationOptions) ToSession(e Engine) *xorm.Session {
  88. return e.Where(opts.ToCond())
  89. }
  90. func getNotifications(e Engine, options FindNotificationOptions) (nl NotificationList, err error) {
  91. err = options.ToSession(e).OrderBy("notification.updated_unix DESC").Find(&nl)
  92. return
  93. }
  94. // GetNotifications returns all notifications that fit to the given options.
  95. func GetNotifications(opts FindNotificationOptions) (NotificationList, error) {
  96. return getNotifications(x, opts)
  97. }
  98. // CreateOrUpdateIssueNotifications creates an issue notification
  99. // for each watcher, or updates it if already exists
  100. func CreateOrUpdateIssueNotifications(issueID, commentID int64, notificationAuthorID int64) error {
  101. sess := x.NewSession()
  102. defer sess.Close()
  103. if err := sess.Begin(); err != nil {
  104. return err
  105. }
  106. if err := createOrUpdateIssueNotifications(sess, issueID, commentID, notificationAuthorID); err != nil {
  107. return err
  108. }
  109. return sess.Commit()
  110. }
  111. func createOrUpdateIssueNotifications(e Engine, issueID, commentID int64, notificationAuthorID int64) error {
  112. issueWatches, err := getIssueWatchers(e, issueID)
  113. if err != nil {
  114. return err
  115. }
  116. issue, err := getIssueByID(e, issueID)
  117. if err != nil {
  118. return err
  119. }
  120. watches, err := getWatchers(e, issue.RepoID)
  121. if err != nil {
  122. return err
  123. }
  124. notifications, err := getNotificationsByIssueID(e, issueID)
  125. if err != nil {
  126. return err
  127. }
  128. alreadyNotified := make(map[int64]struct{}, len(issueWatches)+len(watches))
  129. notifyUser := func(userID int64) error {
  130. // do not send notification for the own issuer/commenter
  131. if userID == notificationAuthorID {
  132. return nil
  133. }
  134. if _, ok := alreadyNotified[userID]; ok {
  135. return nil
  136. }
  137. alreadyNotified[userID] = struct{}{}
  138. if notificationExists(notifications, issue.ID, userID) {
  139. return updateIssueNotification(e, userID, issue.ID, commentID, notificationAuthorID)
  140. }
  141. return createIssueNotification(e, userID, issue, commentID, notificationAuthorID)
  142. }
  143. for _, issueWatch := range issueWatches {
  144. // ignore if user unwatched the issue
  145. if !issueWatch.IsWatching {
  146. alreadyNotified[issueWatch.UserID] = struct{}{}
  147. continue
  148. }
  149. if err := notifyUser(issueWatch.UserID); err != nil {
  150. return err
  151. }
  152. }
  153. err = issue.loadRepo(e)
  154. if err != nil {
  155. return err
  156. }
  157. for _, watch := range watches {
  158. issue.Repo.Units = nil
  159. if issue.IsPull && !issue.Repo.checkUnitUser(e, watch.UserID, false, UnitTypePullRequests) {
  160. continue
  161. }
  162. if !issue.IsPull && !issue.Repo.checkUnitUser(e, watch.UserID, false, UnitTypeIssues) {
  163. continue
  164. }
  165. if err := notifyUser(watch.UserID); err != nil {
  166. return err
  167. }
  168. }
  169. return nil
  170. }
  171. func getNotificationsByIssueID(e Engine, issueID int64) (notifications []*Notification, err error) {
  172. err = e.
  173. Where("issue_id = ?", issueID).
  174. Find(&notifications)
  175. return
  176. }
  177. func notificationExists(notifications []*Notification, issueID, userID int64) bool {
  178. for _, notification := range notifications {
  179. if notification.IssueID == issueID && notification.UserID == userID {
  180. return true
  181. }
  182. }
  183. return false
  184. }
  185. func createIssueNotification(e Engine, userID int64, issue *Issue, commentID, updatedByID int64) error {
  186. notification := &Notification{
  187. UserID: userID,
  188. RepoID: issue.RepoID,
  189. Status: NotificationStatusUnread,
  190. IssueID: issue.ID,
  191. CommentID: commentID,
  192. UpdatedBy: updatedByID,
  193. }
  194. if issue.IsPull {
  195. notification.Source = NotificationSourcePullRequest
  196. } else {
  197. notification.Source = NotificationSourceIssue
  198. }
  199. _, err := e.Insert(notification)
  200. return err
  201. }
  202. func updateIssueNotification(e Engine, userID, issueID, commentID, updatedByID int64) error {
  203. notification, err := getIssueNotification(e, userID, issueID)
  204. if err != nil {
  205. return err
  206. }
  207. // NOTICE: Only update comment id when the before notification on this issue is read, otherwise you may miss some old comments.
  208. // But we need update update_by so that the notification will be reorder
  209. var cols []string
  210. if notification.Status == NotificationStatusRead {
  211. notification.Status = NotificationStatusUnread
  212. notification.CommentID = commentID
  213. cols = []string{"status", "update_by", "comment_id"}
  214. } else {
  215. notification.UpdatedBy = updatedByID
  216. cols = []string{"update_by"}
  217. }
  218. _, err = e.ID(notification.ID).Cols(cols...).Update(notification)
  219. return err
  220. }
  221. func getIssueNotification(e Engine, userID, issueID int64) (*Notification, error) {
  222. notification := new(Notification)
  223. _, err := e.
  224. Where("user_id = ?", userID).
  225. And("issue_id = ?", issueID).
  226. Get(notification)
  227. return notification, err
  228. }
  229. // NotificationsForUser returns notifications for a given user and status
  230. func NotificationsForUser(user *User, statuses []NotificationStatus, page, perPage int) (NotificationList, error) {
  231. return notificationsForUser(x, user, statuses, page, perPage)
  232. }
  233. func notificationsForUser(e Engine, user *User, statuses []NotificationStatus, page, perPage int) (notifications []*Notification, err error) {
  234. if len(statuses) == 0 {
  235. return
  236. }
  237. sess := e.
  238. Where("user_id = ?", user.ID).
  239. In("status", statuses).
  240. OrderBy("updated_unix DESC")
  241. if page > 0 && perPage > 0 {
  242. sess.Limit(perPage, (page-1)*perPage)
  243. }
  244. err = sess.Find(&notifications)
  245. return
  246. }
  247. // APIFormat converts a Notification to api.NotificationThread
  248. func (n *Notification) APIFormat() *api.NotificationThread {
  249. result := &api.NotificationThread{
  250. ID: n.ID,
  251. Unread: !(n.Status == NotificationStatusRead || n.Status == NotificationStatusPinned),
  252. Pinned: n.Status == NotificationStatusPinned,
  253. UpdatedAt: n.UpdatedUnix.AsTime(),
  254. URL: n.APIURL(),
  255. }
  256. //since user only get notifications when he has access to use minimal access mode
  257. if n.Repository != nil {
  258. result.Repository = n.Repository.APIFormat(AccessModeRead)
  259. }
  260. //handle Subject
  261. switch n.Source {
  262. case NotificationSourceIssue:
  263. result.Subject = &api.NotificationSubject{Type: "Issue"}
  264. if n.Issue != nil {
  265. result.Subject.Title = n.Issue.Title
  266. result.Subject.URL = n.Issue.APIURL()
  267. comment, err := n.Issue.GetLastComment()
  268. if err == nil && comment != nil {
  269. result.Subject.LatestCommentURL = comment.APIURL()
  270. }
  271. }
  272. case NotificationSourcePullRequest:
  273. result.Subject = &api.NotificationSubject{Type: "Pull"}
  274. if n.Issue != nil {
  275. result.Subject.Title = n.Issue.Title
  276. result.Subject.URL = n.Issue.APIURL()
  277. comment, err := n.Issue.GetLastComment()
  278. if err == nil && comment != nil {
  279. result.Subject.LatestCommentURL = comment.APIURL()
  280. }
  281. }
  282. case NotificationSourceCommit:
  283. result.Subject = &api.NotificationSubject{
  284. Type: "Commit",
  285. Title: n.CommitID,
  286. }
  287. //unused until now
  288. }
  289. return result
  290. }
  291. // LoadAttributes load Repo Issue User and Comment if not loaded
  292. func (n *Notification) LoadAttributes() (err error) {
  293. return n.loadAttributes(x)
  294. }
  295. func (n *Notification) loadAttributes(e Engine) (err error) {
  296. if err = n.loadRepo(e); err != nil {
  297. return
  298. }
  299. if err = n.loadIssue(e); err != nil {
  300. return
  301. }
  302. if err = n.loadUser(e); err != nil {
  303. return
  304. }
  305. if err = n.loadComment(e); err != nil {
  306. return
  307. }
  308. return
  309. }
  310. func (n *Notification) loadRepo(e Engine) (err error) {
  311. if n.Repository == nil {
  312. n.Repository, err = getRepositoryByID(e, n.RepoID)
  313. if err != nil {
  314. return fmt.Errorf("getRepositoryByID [%d]: %v", n.RepoID, err)
  315. }
  316. }
  317. return nil
  318. }
  319. func (n *Notification) loadIssue(e Engine) (err error) {
  320. if n.Issue == nil {
  321. n.Issue, err = getIssueByID(e, n.IssueID)
  322. if err != nil {
  323. return fmt.Errorf("getIssueByID [%d]: %v", n.IssueID, err)
  324. }
  325. return n.Issue.loadAttributes(e)
  326. }
  327. return nil
  328. }
  329. func (n *Notification) loadComment(e Engine) (err error) {
  330. if n.Comment == nil && n.CommentID > 0 {
  331. n.Comment, err = GetCommentByID(n.CommentID)
  332. if err != nil {
  333. return fmt.Errorf("GetCommentByID [%d]: %v", n.CommentID, err)
  334. }
  335. }
  336. return nil
  337. }
  338. func (n *Notification) loadUser(e Engine) (err error) {
  339. if n.User == nil {
  340. n.User, err = getUserByID(e, n.UserID)
  341. if err != nil {
  342. return fmt.Errorf("getUserByID [%d]: %v", n.UserID, err)
  343. }
  344. }
  345. return nil
  346. }
  347. // GetRepo returns the repo of the notification
  348. func (n *Notification) GetRepo() (*Repository, error) {
  349. return n.Repository, n.loadRepo(x)
  350. }
  351. // GetIssue returns the issue of the notification
  352. func (n *Notification) GetIssue() (*Issue, error) {
  353. return n.Issue, n.loadIssue(x)
  354. }
  355. // HTMLURL formats a URL-string to the notification
  356. func (n *Notification) HTMLURL() string {
  357. if n.Comment != nil {
  358. return n.Comment.HTMLURL()
  359. }
  360. return n.Issue.HTMLURL()
  361. }
  362. // APIURL formats a URL-string to the notification
  363. func (n *Notification) APIURL() string {
  364. return setting.AppURL + path.Join("api/v1/notifications/threads", fmt.Sprintf("%d", n.ID))
  365. }
  366. // NotificationList contains a list of notifications
  367. type NotificationList []*Notification
  368. // APIFormat converts a NotificationList to api.NotificationThread list
  369. func (nl NotificationList) APIFormat() []*api.NotificationThread {
  370. var result = make([]*api.NotificationThread, 0, len(nl))
  371. for _, n := range nl {
  372. result = append(result, n.APIFormat())
  373. }
  374. return result
  375. }
  376. // LoadAttributes load Repo Issue User and Comment if not loaded
  377. func (nl NotificationList) LoadAttributes() (err error) {
  378. for i := 0; i < len(nl); i++ {
  379. err = nl[i].LoadAttributes()
  380. if err != nil {
  381. return
  382. }
  383. }
  384. return
  385. }
  386. func (nl NotificationList) getPendingRepoIDs() []int64 {
  387. var ids = make(map[int64]struct{}, len(nl))
  388. for _, notification := range nl {
  389. if notification.Repository != nil {
  390. continue
  391. }
  392. if _, ok := ids[notification.RepoID]; !ok {
  393. ids[notification.RepoID] = struct{}{}
  394. }
  395. }
  396. return keysInt64(ids)
  397. }
  398. // LoadRepos loads repositories from database
  399. func (nl NotificationList) LoadRepos() (RepositoryList, error) {
  400. if len(nl) == 0 {
  401. return RepositoryList{}, nil
  402. }
  403. var repoIDs = nl.getPendingRepoIDs()
  404. var repos = make(map[int64]*Repository, len(repoIDs))
  405. var left = len(repoIDs)
  406. for left > 0 {
  407. var limit = defaultMaxInSize
  408. if left < limit {
  409. limit = left
  410. }
  411. rows, err := x.
  412. In("id", repoIDs[:limit]).
  413. Rows(new(Repository))
  414. if err != nil {
  415. return nil, err
  416. }
  417. for rows.Next() {
  418. var repo Repository
  419. err = rows.Scan(&repo)
  420. if err != nil {
  421. rows.Close()
  422. return nil, err
  423. }
  424. repos[repo.ID] = &repo
  425. }
  426. _ = rows.Close()
  427. left -= limit
  428. repoIDs = repoIDs[limit:]
  429. }
  430. var reposList = make(RepositoryList, 0, len(repoIDs))
  431. for _, notification := range nl {
  432. if notification.Repository == nil {
  433. notification.Repository = repos[notification.RepoID]
  434. }
  435. var found bool
  436. for _, r := range reposList {
  437. if r.ID == notification.Repository.ID {
  438. found = true
  439. break
  440. }
  441. }
  442. if !found {
  443. reposList = append(reposList, notification.Repository)
  444. }
  445. }
  446. return reposList, nil
  447. }
  448. func (nl NotificationList) getPendingIssueIDs() []int64 {
  449. var ids = make(map[int64]struct{}, len(nl))
  450. for _, notification := range nl {
  451. if notification.Issue != nil {
  452. continue
  453. }
  454. if _, ok := ids[notification.IssueID]; !ok {
  455. ids[notification.IssueID] = struct{}{}
  456. }
  457. }
  458. return keysInt64(ids)
  459. }
  460. // LoadIssues loads issues from database
  461. func (nl NotificationList) LoadIssues() error {
  462. if len(nl) == 0 {
  463. return nil
  464. }
  465. var issueIDs = nl.getPendingIssueIDs()
  466. var issues = make(map[int64]*Issue, len(issueIDs))
  467. var left = len(issueIDs)
  468. for left > 0 {
  469. var limit = defaultMaxInSize
  470. if left < limit {
  471. limit = left
  472. }
  473. rows, err := x.
  474. In("id", issueIDs[:limit]).
  475. Rows(new(Issue))
  476. if err != nil {
  477. return err
  478. }
  479. for rows.Next() {
  480. var issue Issue
  481. err = rows.Scan(&issue)
  482. if err != nil {
  483. rows.Close()
  484. return err
  485. }
  486. issues[issue.ID] = &issue
  487. }
  488. _ = rows.Close()
  489. left -= limit
  490. issueIDs = issueIDs[limit:]
  491. }
  492. for _, notification := range nl {
  493. if notification.Issue == nil {
  494. notification.Issue = issues[notification.IssueID]
  495. notification.Issue.Repo = notification.Repository
  496. }
  497. }
  498. return nil
  499. }
  500. func (nl NotificationList) getPendingCommentIDs() []int64 {
  501. var ids = make(map[int64]struct{}, len(nl))
  502. for _, notification := range nl {
  503. if notification.CommentID == 0 || notification.Comment != nil {
  504. continue
  505. }
  506. if _, ok := ids[notification.CommentID]; !ok {
  507. ids[notification.CommentID] = struct{}{}
  508. }
  509. }
  510. return keysInt64(ids)
  511. }
  512. // LoadComments loads comments from database
  513. func (nl NotificationList) LoadComments() error {
  514. if len(nl) == 0 {
  515. return nil
  516. }
  517. var commentIDs = nl.getPendingCommentIDs()
  518. var comments = make(map[int64]*Comment, len(commentIDs))
  519. var left = len(commentIDs)
  520. for left > 0 {
  521. var limit = defaultMaxInSize
  522. if left < limit {
  523. limit = left
  524. }
  525. rows, err := x.
  526. In("id", commentIDs[:limit]).
  527. Rows(new(Comment))
  528. if err != nil {
  529. return err
  530. }
  531. for rows.Next() {
  532. var comment Comment
  533. err = rows.Scan(&comment)
  534. if err != nil {
  535. rows.Close()
  536. return err
  537. }
  538. comments[comment.ID] = &comment
  539. }
  540. _ = rows.Close()
  541. left -= limit
  542. commentIDs = commentIDs[limit:]
  543. }
  544. for _, notification := range nl {
  545. if notification.CommentID > 0 && notification.Comment == nil && comments[notification.CommentID] != nil {
  546. notification.Comment = comments[notification.CommentID]
  547. notification.Comment.Issue = notification.Issue
  548. }
  549. }
  550. return nil
  551. }
  552. // GetNotificationCount returns the notification count for user
  553. func GetNotificationCount(user *User, status NotificationStatus) (int64, error) {
  554. return getNotificationCount(x, user, status)
  555. }
  556. func getNotificationCount(e Engine, user *User, status NotificationStatus) (count int64, err error) {
  557. count, err = e.
  558. Where("user_id = ?", user.ID).
  559. And("status = ?", status).
  560. Count(&Notification{})
  561. return
  562. }
  563. func setNotificationStatusReadIfUnread(e Engine, userID, issueID int64) error {
  564. notification, err := getIssueNotification(e, userID, issueID)
  565. // ignore if not exists
  566. if err != nil {
  567. return nil
  568. }
  569. if notification.Status != NotificationStatusUnread {
  570. return nil
  571. }
  572. notification.Status = NotificationStatusRead
  573. _, err = e.ID(notification.ID).Update(notification)
  574. return err
  575. }
  576. // SetNotificationStatus change the notification status
  577. func SetNotificationStatus(notificationID int64, user *User, status NotificationStatus) error {
  578. notification, err := getNotificationByID(x, notificationID)
  579. if err != nil {
  580. return err
  581. }
  582. if notification.UserID != user.ID {
  583. return fmt.Errorf("Can't change notification of another user: %d, %d", notification.UserID, user.ID)
  584. }
  585. notification.Status = status
  586. _, err = x.ID(notificationID).Update(notification)
  587. return err
  588. }
  589. // GetNotificationByID return notification by ID
  590. func GetNotificationByID(notificationID int64) (*Notification, error) {
  591. return getNotificationByID(x, notificationID)
  592. }
  593. func getNotificationByID(e Engine, notificationID int64) (*Notification, error) {
  594. notification := new(Notification)
  595. ok, err := e.
  596. Where("id = ?", notificationID).
  597. Get(notification)
  598. if err != nil {
  599. return nil, err
  600. }
  601. if !ok {
  602. return nil, ErrNotExist{ID: notificationID}
  603. }
  604. return notification, nil
  605. }
  606. // UpdateNotificationStatuses updates the statuses of all of a user's notifications that are of the currentStatus type to the desiredStatus
  607. func UpdateNotificationStatuses(user *User, currentStatus NotificationStatus, desiredStatus NotificationStatus) error {
  608. n := &Notification{Status: desiredStatus, UpdatedBy: user.ID}
  609. _, err := x.
  610. Where("user_id = ? AND status = ?", user.ID, currentStatus).
  611. Cols("status", "updated_by", "updated_unix").
  612. Update(n)
  613. return err
  614. }