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.

notification.go 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568
  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. "code.gitea.io/gitea/modules/log"
  8. "code.gitea.io/gitea/modules/timeutil"
  9. )
  10. type (
  11. // NotificationStatus is the status of the notification (read or unread)
  12. NotificationStatus uint8
  13. // NotificationSource is the source of the notification (issue, PR, commit, etc)
  14. NotificationSource uint8
  15. )
  16. const (
  17. // NotificationStatusUnread represents an unread notification
  18. NotificationStatusUnread NotificationStatus = iota + 1
  19. // NotificationStatusRead represents a read notification
  20. NotificationStatusRead
  21. // NotificationStatusPinned represents a pinned notification
  22. NotificationStatusPinned
  23. )
  24. const (
  25. // NotificationSourceIssue is a notification of an issue
  26. NotificationSourceIssue NotificationSource = iota + 1
  27. // NotificationSourcePullRequest is a notification of a pull request
  28. NotificationSourcePullRequest
  29. // NotificationSourceCommit is a notification of a commit
  30. NotificationSourceCommit
  31. )
  32. // Notification represents a notification
  33. type Notification struct {
  34. ID int64 `xorm:"pk autoincr"`
  35. UserID int64 `xorm:"INDEX NOT NULL"`
  36. RepoID int64 `xorm:"INDEX NOT NULL"`
  37. Status NotificationStatus `xorm:"SMALLINT INDEX NOT NULL"`
  38. Source NotificationSource `xorm:"SMALLINT INDEX NOT NULL"`
  39. IssueID int64 `xorm:"INDEX NOT NULL"`
  40. CommitID string `xorm:"INDEX"`
  41. CommentID int64
  42. Comment *Comment `xorm:"-"`
  43. UpdatedBy int64 `xorm:"INDEX NOT NULL"`
  44. Issue *Issue `xorm:"-"`
  45. Repository *Repository `xorm:"-"`
  46. CreatedUnix timeutil.TimeStamp `xorm:"created INDEX NOT NULL"`
  47. UpdatedUnix timeutil.TimeStamp `xorm:"updated INDEX NOT NULL"`
  48. }
  49. // CreateOrUpdateIssueNotifications creates an issue notification
  50. // for each watcher, or updates it if already exists
  51. func CreateOrUpdateIssueNotifications(issueID, commentID int64, notificationAuthorID int64) error {
  52. sess := x.NewSession()
  53. defer sess.Close()
  54. if err := sess.Begin(); err != nil {
  55. return err
  56. }
  57. if err := createOrUpdateIssueNotifications(sess, issueID, commentID, notificationAuthorID); err != nil {
  58. return err
  59. }
  60. return sess.Commit()
  61. }
  62. func createOrUpdateIssueNotifications(e Engine, issueID, commentID int64, notificationAuthorID int64) error {
  63. issueWatches, err := getIssueWatchers(e, issueID)
  64. if err != nil {
  65. return err
  66. }
  67. issue, err := getIssueByID(e, issueID)
  68. if err != nil {
  69. return err
  70. }
  71. watches, err := getWatchers(e, issue.RepoID)
  72. if err != nil {
  73. return err
  74. }
  75. notifications, err := getNotificationsByIssueID(e, issueID)
  76. if err != nil {
  77. return err
  78. }
  79. alreadyNotified := make(map[int64]struct{}, len(issueWatches)+len(watches))
  80. notifyUser := func(userID int64) error {
  81. // do not send notification for the own issuer/commenter
  82. if userID == notificationAuthorID {
  83. return nil
  84. }
  85. if _, ok := alreadyNotified[userID]; ok {
  86. return nil
  87. }
  88. alreadyNotified[userID] = struct{}{}
  89. if notificationExists(notifications, issue.ID, userID) {
  90. return updateIssueNotification(e, userID, issue.ID, commentID, notificationAuthorID)
  91. }
  92. return createIssueNotification(e, userID, issue, commentID, notificationAuthorID)
  93. }
  94. for _, issueWatch := range issueWatches {
  95. // ignore if user unwatched the issue
  96. if !issueWatch.IsWatching {
  97. alreadyNotified[issueWatch.UserID] = struct{}{}
  98. continue
  99. }
  100. if err := notifyUser(issueWatch.UserID); err != nil {
  101. return err
  102. }
  103. }
  104. err = issue.loadRepo(e)
  105. if err != nil {
  106. return err
  107. }
  108. for _, watch := range watches {
  109. issue.Repo.Units = nil
  110. if issue.IsPull && !issue.Repo.checkUnitUser(e, watch.UserID, false, UnitTypePullRequests) {
  111. continue
  112. }
  113. if !issue.IsPull && !issue.Repo.checkUnitUser(e, watch.UserID, false, UnitTypeIssues) {
  114. continue
  115. }
  116. if err := notifyUser(watch.UserID); err != nil {
  117. return err
  118. }
  119. }
  120. return nil
  121. }
  122. func getNotificationsByIssueID(e Engine, issueID int64) (notifications []*Notification, err error) {
  123. err = e.
  124. Where("issue_id = ?", issueID).
  125. Find(&notifications)
  126. return
  127. }
  128. func notificationExists(notifications []*Notification, issueID, userID int64) bool {
  129. for _, notification := range notifications {
  130. if notification.IssueID == issueID && notification.UserID == userID {
  131. return true
  132. }
  133. }
  134. return false
  135. }
  136. func createIssueNotification(e Engine, userID int64, issue *Issue, commentID, updatedByID int64) error {
  137. notification := &Notification{
  138. UserID: userID,
  139. RepoID: issue.RepoID,
  140. Status: NotificationStatusUnread,
  141. IssueID: issue.ID,
  142. CommentID: commentID,
  143. UpdatedBy: updatedByID,
  144. }
  145. if issue.IsPull {
  146. notification.Source = NotificationSourcePullRequest
  147. } else {
  148. notification.Source = NotificationSourceIssue
  149. }
  150. _, err := e.Insert(notification)
  151. return err
  152. }
  153. func updateIssueNotification(e Engine, userID, issueID, commentID, updatedByID int64) error {
  154. notification, err := getIssueNotification(e, userID, issueID)
  155. if err != nil {
  156. return err
  157. }
  158. // NOTICE: Only update comment id when the before notification on this issue is read, otherwise you may miss some old comments.
  159. // But we need update update_by so that the notification will be reorder
  160. var cols []string
  161. if notification.Status == NotificationStatusRead {
  162. notification.Status = NotificationStatusUnread
  163. notification.CommentID = commentID
  164. cols = []string{"status", "update_by", "comment_id"}
  165. } else {
  166. notification.UpdatedBy = updatedByID
  167. cols = []string{"update_by"}
  168. }
  169. _, err = e.ID(notification.ID).Cols(cols...).Update(notification)
  170. return err
  171. }
  172. func getIssueNotification(e Engine, userID, issueID int64) (*Notification, error) {
  173. notification := new(Notification)
  174. _, err := e.
  175. Where("user_id = ?", userID).
  176. And("issue_id = ?", issueID).
  177. Get(notification)
  178. return notification, err
  179. }
  180. // NotificationsForUser returns notifications for a given user and status
  181. func NotificationsForUser(user *User, statuses []NotificationStatus, page, perPage int) (NotificationList, error) {
  182. return notificationsForUser(x, user, statuses, page, perPage)
  183. }
  184. func notificationsForUser(e Engine, user *User, statuses []NotificationStatus, page, perPage int) (notifications []*Notification, err error) {
  185. if len(statuses) == 0 {
  186. return
  187. }
  188. sess := e.
  189. Where("user_id = ?", user.ID).
  190. In("status", statuses).
  191. OrderBy("updated_unix DESC")
  192. if page > 0 && perPage > 0 {
  193. sess.Limit(perPage, (page-1)*perPage)
  194. }
  195. err = sess.Find(&notifications)
  196. return
  197. }
  198. // GetRepo returns the repo of the notification
  199. func (n *Notification) GetRepo() (*Repository, error) {
  200. n.Repository = new(Repository)
  201. _, err := x.
  202. Where("id = ?", n.RepoID).
  203. Get(n.Repository)
  204. return n.Repository, err
  205. }
  206. // GetIssue returns the issue of the notification
  207. func (n *Notification) GetIssue() (*Issue, error) {
  208. n.Issue = new(Issue)
  209. _, err := x.
  210. Where("id = ?", n.IssueID).
  211. Get(n.Issue)
  212. return n.Issue, err
  213. }
  214. // HTMLURL formats a URL-string to the notification
  215. func (n *Notification) HTMLURL() string {
  216. if n.Comment != nil {
  217. return n.Comment.HTMLURL()
  218. }
  219. return n.Issue.HTMLURL()
  220. }
  221. // NotificationList contains a list of notifications
  222. type NotificationList []*Notification
  223. func (nl NotificationList) getPendingRepoIDs() []int64 {
  224. var ids = make(map[int64]struct{}, len(nl))
  225. for _, notification := range nl {
  226. if notification.Repository != nil {
  227. continue
  228. }
  229. if _, ok := ids[notification.RepoID]; !ok {
  230. ids[notification.RepoID] = struct{}{}
  231. }
  232. }
  233. return keysInt64(ids)
  234. }
  235. // LoadRepos loads repositories from database
  236. func (nl NotificationList) LoadRepos() (RepositoryList, []int, error) {
  237. if len(nl) == 0 {
  238. return RepositoryList{}, []int{}, nil
  239. }
  240. var repoIDs = nl.getPendingRepoIDs()
  241. var repos = make(map[int64]*Repository, len(repoIDs))
  242. var left = len(repoIDs)
  243. for left > 0 {
  244. var limit = defaultMaxInSize
  245. if left < limit {
  246. limit = left
  247. }
  248. rows, err := x.
  249. In("id", repoIDs[:limit]).
  250. Rows(new(Repository))
  251. if err != nil {
  252. return nil, nil, err
  253. }
  254. for rows.Next() {
  255. var repo Repository
  256. err = rows.Scan(&repo)
  257. if err != nil {
  258. rows.Close()
  259. return nil, nil, err
  260. }
  261. repos[repo.ID] = &repo
  262. }
  263. _ = rows.Close()
  264. left -= limit
  265. repoIDs = repoIDs[limit:]
  266. }
  267. failed := []int{}
  268. var reposList = make(RepositoryList, 0, len(repoIDs))
  269. for i, notification := range nl {
  270. if notification.Repository == nil {
  271. notification.Repository = repos[notification.RepoID]
  272. }
  273. if notification.Repository == nil {
  274. log.Error("Notification[%d]: RepoID: %d not found", notification.ID, notification.RepoID)
  275. failed = append(failed, i)
  276. continue
  277. }
  278. var found bool
  279. for _, r := range reposList {
  280. if r.ID == notification.RepoID {
  281. found = true
  282. break
  283. }
  284. }
  285. if !found {
  286. reposList = append(reposList, notification.Repository)
  287. }
  288. }
  289. return reposList, failed, nil
  290. }
  291. func (nl NotificationList) getPendingIssueIDs() []int64 {
  292. var ids = make(map[int64]struct{}, len(nl))
  293. for _, notification := range nl {
  294. if notification.Issue != nil {
  295. continue
  296. }
  297. if _, ok := ids[notification.IssueID]; !ok {
  298. ids[notification.IssueID] = struct{}{}
  299. }
  300. }
  301. return keysInt64(ids)
  302. }
  303. // LoadIssues loads issues from database
  304. func (nl NotificationList) LoadIssues() ([]int, error) {
  305. if len(nl) == 0 {
  306. return []int{}, nil
  307. }
  308. var issueIDs = nl.getPendingIssueIDs()
  309. var issues = make(map[int64]*Issue, len(issueIDs))
  310. var left = len(issueIDs)
  311. for left > 0 {
  312. var limit = defaultMaxInSize
  313. if left < limit {
  314. limit = left
  315. }
  316. rows, err := x.
  317. In("id", issueIDs[:limit]).
  318. Rows(new(Issue))
  319. if err != nil {
  320. return nil, err
  321. }
  322. for rows.Next() {
  323. var issue Issue
  324. err = rows.Scan(&issue)
  325. if err != nil {
  326. rows.Close()
  327. return nil, err
  328. }
  329. issues[issue.ID] = &issue
  330. }
  331. _ = rows.Close()
  332. left -= limit
  333. issueIDs = issueIDs[limit:]
  334. }
  335. failures := []int{}
  336. for i, notification := range nl {
  337. if notification.Issue == nil {
  338. notification.Issue = issues[notification.IssueID]
  339. if notification.Issue == nil {
  340. log.Error("Notification[%d]: IssueID: %d Not Found", notification.ID, notification.IssueID)
  341. failures = append(failures, i)
  342. continue
  343. }
  344. notification.Issue.Repo = notification.Repository
  345. }
  346. }
  347. return failures, nil
  348. }
  349. // Without returns the notification list without the failures
  350. func (nl NotificationList) Without(failures []int) NotificationList {
  351. if len(failures) == 0 {
  352. return nl
  353. }
  354. remaining := make([]*Notification, 0, len(nl))
  355. last := -1
  356. var i int
  357. for _, i = range failures {
  358. remaining = append(remaining, nl[last+1:i]...)
  359. last = i
  360. }
  361. if len(nl) > i {
  362. remaining = append(remaining, nl[i+1:]...)
  363. }
  364. return remaining
  365. }
  366. func (nl NotificationList) getPendingCommentIDs() []int64 {
  367. var ids = make(map[int64]struct{}, len(nl))
  368. for _, notification := range nl {
  369. if notification.CommentID == 0 || notification.Comment != nil {
  370. continue
  371. }
  372. if _, ok := ids[notification.CommentID]; !ok {
  373. ids[notification.CommentID] = struct{}{}
  374. }
  375. }
  376. return keysInt64(ids)
  377. }
  378. // LoadComments loads comments from database
  379. func (nl NotificationList) LoadComments() ([]int, error) {
  380. if len(nl) == 0 {
  381. return []int{}, nil
  382. }
  383. var commentIDs = nl.getPendingCommentIDs()
  384. var comments = make(map[int64]*Comment, len(commentIDs))
  385. var left = len(commentIDs)
  386. for left > 0 {
  387. var limit = defaultMaxInSize
  388. if left < limit {
  389. limit = left
  390. }
  391. rows, err := x.
  392. In("id", commentIDs[:limit]).
  393. Rows(new(Comment))
  394. if err != nil {
  395. return nil, err
  396. }
  397. for rows.Next() {
  398. var comment Comment
  399. err = rows.Scan(&comment)
  400. if err != nil {
  401. rows.Close()
  402. return nil, err
  403. }
  404. comments[comment.ID] = &comment
  405. }
  406. _ = rows.Close()
  407. left -= limit
  408. commentIDs = commentIDs[limit:]
  409. }
  410. failures := []int{}
  411. for i, notification := range nl {
  412. if notification.CommentID > 0 && notification.Comment == nil && comments[notification.CommentID] != nil {
  413. notification.Comment = comments[notification.CommentID]
  414. if notification.Comment == nil {
  415. log.Error("Notification[%d]: CommentID[%d] failed to load", notification.ID, notification.CommentID)
  416. failures = append(failures, i)
  417. continue
  418. }
  419. notification.Comment.Issue = notification.Issue
  420. }
  421. }
  422. return failures, nil
  423. }
  424. // GetNotificationCount returns the notification count for user
  425. func GetNotificationCount(user *User, status NotificationStatus) (int64, error) {
  426. return getNotificationCount(x, user, status)
  427. }
  428. func getNotificationCount(e Engine, user *User, status NotificationStatus) (count int64, err error) {
  429. count, err = e.
  430. Where("user_id = ?", user.ID).
  431. And("status = ?", status).
  432. Count(&Notification{})
  433. return
  434. }
  435. func setNotificationStatusReadIfUnread(e Engine, userID, issueID int64) error {
  436. notification, err := getIssueNotification(e, userID, issueID)
  437. // ignore if not exists
  438. if err != nil {
  439. return nil
  440. }
  441. if notification.Status != NotificationStatusUnread {
  442. return nil
  443. }
  444. notification.Status = NotificationStatusRead
  445. _, err = e.ID(notification.ID).Update(notification)
  446. return err
  447. }
  448. // SetNotificationStatus change the notification status
  449. func SetNotificationStatus(notificationID int64, user *User, status NotificationStatus) error {
  450. notification, err := getNotificationByID(notificationID)
  451. if err != nil {
  452. return err
  453. }
  454. if notification.UserID != user.ID {
  455. return fmt.Errorf("Can't change notification of another user: %d, %d", notification.UserID, user.ID)
  456. }
  457. notification.Status = status
  458. _, err = x.ID(notificationID).Update(notification)
  459. return err
  460. }
  461. func getNotificationByID(notificationID int64) (*Notification, error) {
  462. notification := new(Notification)
  463. ok, err := x.
  464. Where("id = ?", notificationID).
  465. Get(notification)
  466. if err != nil {
  467. return nil, err
  468. }
  469. if !ok {
  470. return nil, fmt.Errorf("Notification %d does not exists", notificationID)
  471. }
  472. return notification, nil
  473. }
  474. // UpdateNotificationStatuses updates the statuses of all of a user's notifications that are of the currentStatus type to the desiredStatus
  475. func UpdateNotificationStatuses(user *User, currentStatus NotificationStatus, desiredStatus NotificationStatus) error {
  476. n := &Notification{Status: desiredStatus, UpdatedBy: user.ID}
  477. _, err := x.
  478. Where("user_id = ? AND status = ?", user.ID, currentStatus).
  479. Cols("status", "updated_by", "updated_unix").
  480. Update(n)
  481. return err
  482. }