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 13KB

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