gitea源码

notification.go 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419
  1. // Copyright 2016 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package activities
  4. import (
  5. "context"
  6. "fmt"
  7. "net/url"
  8. "strconv"
  9. "code.gitea.io/gitea/models/db"
  10. issues_model "code.gitea.io/gitea/models/issues"
  11. "code.gitea.io/gitea/models/organization"
  12. repo_model "code.gitea.io/gitea/models/repo"
  13. user_model "code.gitea.io/gitea/models/user"
  14. "code.gitea.io/gitea/modules/setting"
  15. "code.gitea.io/gitea/modules/timeutil"
  16. "xorm.io/builder"
  17. "xorm.io/xorm/schemas"
  18. )
  19. type (
  20. // NotificationStatus is the status of the notification (read or unread)
  21. NotificationStatus uint8
  22. // NotificationSource is the source of the notification (issue, PR, commit, etc)
  23. NotificationSource uint8
  24. )
  25. const (
  26. // NotificationStatusUnread represents an unread notification
  27. NotificationStatusUnread NotificationStatus = iota + 1
  28. // NotificationStatusRead represents a read notification
  29. NotificationStatusRead
  30. // NotificationStatusPinned represents a pinned notification
  31. NotificationStatusPinned
  32. )
  33. const (
  34. // NotificationSourceIssue is a notification of an issue
  35. NotificationSourceIssue NotificationSource = iota + 1
  36. // NotificationSourcePullRequest is a notification of a pull request
  37. NotificationSourcePullRequest
  38. // NotificationSourceCommit is a notification of a commit
  39. NotificationSourceCommit
  40. // NotificationSourceRepository is a notification for a repository
  41. NotificationSourceRepository
  42. )
  43. // Notification represents a notification
  44. type Notification struct {
  45. ID int64 `xorm:"pk autoincr"`
  46. UserID int64 `xorm:"NOT NULL"`
  47. RepoID int64 `xorm:"NOT NULL"`
  48. Status NotificationStatus `xorm:"SMALLINT NOT NULL"`
  49. Source NotificationSource `xorm:"SMALLINT NOT NULL"`
  50. IssueID int64 `xorm:"NOT NULL"`
  51. CommitID string
  52. CommentID int64
  53. UpdatedBy int64 `xorm:"NOT NULL"`
  54. Issue *issues_model.Issue `xorm:"-"`
  55. Repository *repo_model.Repository `xorm:"-"`
  56. Comment *issues_model.Comment `xorm:"-"`
  57. User *user_model.User `xorm:"-"`
  58. CreatedUnix timeutil.TimeStamp `xorm:"created NOT NULL"`
  59. UpdatedUnix timeutil.TimeStamp `xorm:"updated NOT NULL"`
  60. }
  61. // TableIndices implements xorm's TableIndices interface
  62. func (n *Notification) TableIndices() []*schemas.Index {
  63. indices := make([]*schemas.Index, 0, 8)
  64. usuuIndex := schemas.NewIndex("u_s_uu", schemas.IndexType)
  65. usuuIndex.AddColumn("user_id", "status", "updated_unix")
  66. indices = append(indices, usuuIndex)
  67. // Add the individual indices that were previously defined in struct tags
  68. userIDIndex := schemas.NewIndex("idx_notification_user_id", schemas.IndexType)
  69. userIDIndex.AddColumn("user_id")
  70. indices = append(indices, userIDIndex)
  71. repoIDIndex := schemas.NewIndex("idx_notification_repo_id", schemas.IndexType)
  72. repoIDIndex.AddColumn("repo_id")
  73. indices = append(indices, repoIDIndex)
  74. statusIndex := schemas.NewIndex("idx_notification_status", schemas.IndexType)
  75. statusIndex.AddColumn("status")
  76. indices = append(indices, statusIndex)
  77. sourceIndex := schemas.NewIndex("idx_notification_source", schemas.IndexType)
  78. sourceIndex.AddColumn("source")
  79. indices = append(indices, sourceIndex)
  80. issueIDIndex := schemas.NewIndex("idx_notification_issue_id", schemas.IndexType)
  81. issueIDIndex.AddColumn("issue_id")
  82. indices = append(indices, issueIDIndex)
  83. commitIDIndex := schemas.NewIndex("idx_notification_commit_id", schemas.IndexType)
  84. commitIDIndex.AddColumn("commit_id")
  85. indices = append(indices, commitIDIndex)
  86. updatedByIndex := schemas.NewIndex("idx_notification_updated_by", schemas.IndexType)
  87. updatedByIndex.AddColumn("updated_by")
  88. indices = append(indices, updatedByIndex)
  89. return indices
  90. }
  91. func init() {
  92. db.RegisterModel(new(Notification))
  93. }
  94. // CreateRepoTransferNotification creates notification for the user a repository was transferred to
  95. func CreateRepoTransferNotification(ctx context.Context, doer, newOwner *user_model.User, repo *repo_model.Repository) error {
  96. return db.WithTx(ctx, func(ctx context.Context) error {
  97. var notify []*Notification
  98. if newOwner.IsOrganization() {
  99. users, err := organization.GetUsersWhoCanCreateOrgRepo(ctx, newOwner.ID)
  100. if err != nil || len(users) == 0 {
  101. return err
  102. }
  103. for i := range users {
  104. notify = append(notify, &Notification{
  105. UserID: i,
  106. RepoID: repo.ID,
  107. Status: NotificationStatusUnread,
  108. UpdatedBy: doer.ID,
  109. Source: NotificationSourceRepository,
  110. })
  111. }
  112. } else {
  113. notify = []*Notification{{
  114. UserID: newOwner.ID,
  115. RepoID: repo.ID,
  116. Status: NotificationStatusUnread,
  117. UpdatedBy: doer.ID,
  118. Source: NotificationSourceRepository,
  119. }}
  120. }
  121. return db.Insert(ctx, notify)
  122. })
  123. }
  124. func createIssueNotification(ctx context.Context, userID int64, issue *issues_model.Issue, commentID, updatedByID int64) error {
  125. notification := &Notification{
  126. UserID: userID,
  127. RepoID: issue.RepoID,
  128. Status: NotificationStatusUnread,
  129. IssueID: issue.ID,
  130. CommentID: commentID,
  131. UpdatedBy: updatedByID,
  132. }
  133. if issue.IsPull {
  134. notification.Source = NotificationSourcePullRequest
  135. } else {
  136. notification.Source = NotificationSourceIssue
  137. }
  138. return db.Insert(ctx, notification)
  139. }
  140. func updateIssueNotification(ctx context.Context, userID, issueID, commentID, updatedByID int64) error {
  141. notification, err := GetIssueNotification(ctx, userID, issueID)
  142. if err != nil {
  143. return err
  144. }
  145. // NOTICE: Only update comment id when the before notification on this issue is read, otherwise you may miss some old comments.
  146. // But we need update update_by so that the notification will be reorder
  147. var cols []string
  148. if notification.Status == NotificationStatusRead {
  149. notification.Status = NotificationStatusUnread
  150. notification.CommentID = commentID
  151. cols = []string{"status", "update_by", "comment_id"}
  152. } else {
  153. notification.UpdatedBy = updatedByID
  154. cols = []string{"update_by"}
  155. }
  156. _, err = db.GetEngine(ctx).ID(notification.ID).Cols(cols...).Update(notification)
  157. return err
  158. }
  159. // GetIssueNotification return the notification about an issue
  160. func GetIssueNotification(ctx context.Context, userID, issueID int64) (*Notification, error) {
  161. notification := new(Notification)
  162. _, err := db.GetEngine(ctx).
  163. Where("user_id = ?", userID).
  164. And("issue_id = ?", issueID).
  165. Get(notification)
  166. return notification, err
  167. }
  168. // LoadAttributes load Repo Issue User and Comment if not loaded
  169. func (n *Notification) LoadAttributes(ctx context.Context) (err error) {
  170. if err = n.loadRepo(ctx); err != nil {
  171. return err
  172. }
  173. if err = n.loadIssue(ctx); err != nil {
  174. return err
  175. }
  176. if err = n.loadUser(ctx); err != nil {
  177. return err
  178. }
  179. if err = n.loadComment(ctx); err != nil {
  180. return err
  181. }
  182. return err
  183. }
  184. func (n *Notification) loadRepo(ctx context.Context) (err error) {
  185. if n.Repository == nil {
  186. n.Repository, err = repo_model.GetRepositoryByID(ctx, n.RepoID)
  187. if err != nil {
  188. return fmt.Errorf("getRepositoryByID [%d]: %w", n.RepoID, err)
  189. }
  190. }
  191. return nil
  192. }
  193. func (n *Notification) loadIssue(ctx context.Context) (err error) {
  194. if n.Issue == nil && n.IssueID != 0 {
  195. n.Issue, err = issues_model.GetIssueByID(ctx, n.IssueID)
  196. if err != nil {
  197. return fmt.Errorf("getIssueByID [%d]: %w", n.IssueID, err)
  198. }
  199. return n.Issue.LoadAttributes(ctx)
  200. }
  201. return nil
  202. }
  203. func (n *Notification) loadComment(ctx context.Context) (err error) {
  204. if n.Comment == nil && n.CommentID != 0 {
  205. n.Comment, err = issues_model.GetCommentByID(ctx, n.CommentID)
  206. if err != nil {
  207. if issues_model.IsErrCommentNotExist(err) {
  208. return issues_model.ErrCommentNotExist{
  209. ID: n.CommentID,
  210. IssueID: n.IssueID,
  211. }
  212. }
  213. return err
  214. }
  215. }
  216. return nil
  217. }
  218. func (n *Notification) loadUser(ctx context.Context) (err error) {
  219. if n.User == nil {
  220. n.User, err = user_model.GetUserByID(ctx, n.UserID)
  221. if err != nil {
  222. return fmt.Errorf("getUserByID [%d]: %w", n.UserID, err)
  223. }
  224. }
  225. return nil
  226. }
  227. // GetRepo returns the repo of the notification
  228. func (n *Notification) GetRepo(ctx context.Context) (*repo_model.Repository, error) {
  229. return n.Repository, n.loadRepo(ctx)
  230. }
  231. // GetIssue returns the issue of the notification
  232. func (n *Notification) GetIssue(ctx context.Context) (*issues_model.Issue, error) {
  233. return n.Issue, n.loadIssue(ctx)
  234. }
  235. // HTMLURL formats a URL-string to the notification
  236. func (n *Notification) HTMLURL(ctx context.Context) string {
  237. switch n.Source {
  238. case NotificationSourceIssue, NotificationSourcePullRequest:
  239. if n.Comment != nil {
  240. return n.Comment.HTMLURL(ctx)
  241. }
  242. return n.Issue.HTMLURL(ctx)
  243. case NotificationSourceCommit:
  244. return n.Repository.HTMLURL(ctx) + "/commit/" + url.PathEscape(n.CommitID)
  245. case NotificationSourceRepository:
  246. return n.Repository.HTMLURL(ctx)
  247. }
  248. return ""
  249. }
  250. // Link formats a relative URL-string to the notification
  251. func (n *Notification) Link(ctx context.Context) string {
  252. switch n.Source {
  253. case NotificationSourceIssue, NotificationSourcePullRequest:
  254. if n.Comment != nil {
  255. return n.Comment.Link(ctx)
  256. }
  257. return n.Issue.Link()
  258. case NotificationSourceCommit:
  259. return n.Repository.Link() + "/commit/" + url.PathEscape(n.CommitID)
  260. case NotificationSourceRepository:
  261. return n.Repository.Link()
  262. }
  263. return ""
  264. }
  265. // APIURL formats a URL-string to the notification
  266. func (n *Notification) APIURL() string {
  267. return setting.AppURL + "api/v1/notifications/threads/" + strconv.FormatInt(n.ID, 10)
  268. }
  269. func notificationExists(notifications []*Notification, issueID, userID int64) bool {
  270. for _, notification := range notifications {
  271. if notification.IssueID == issueID && notification.UserID == userID {
  272. return true
  273. }
  274. }
  275. return false
  276. }
  277. // UserIDCount is a simple coalition of UserID and Count
  278. type UserIDCount struct {
  279. UserID int64
  280. Count int64
  281. }
  282. // GetUIDsAndNotificationCounts returns the unread counts for every user between the two provided times.
  283. // It must return all user IDs which appear during the period, including count=0 for users who have read all.
  284. func GetUIDsAndNotificationCounts(ctx context.Context, since, until timeutil.TimeStamp) ([]UserIDCount, error) {
  285. sql := `SELECT user_id, sum(case when status= ? then 1 else 0 end) AS count FROM notification ` +
  286. `WHERE user_id IN (SELECT user_id FROM notification WHERE updated_unix >= ? AND ` +
  287. `updated_unix < ?) GROUP BY user_id`
  288. var res []UserIDCount
  289. return res, db.GetEngine(ctx).SQL(sql, NotificationStatusUnread, since, until).Find(&res)
  290. }
  291. // SetIssueReadBy sets issue to be read by given user.
  292. func SetIssueReadBy(ctx context.Context, issueID, userID int64) error {
  293. if err := issues_model.UpdateIssueUserByRead(ctx, userID, issueID); err != nil {
  294. return err
  295. }
  296. return setIssueNotificationStatusReadIfUnread(ctx, userID, issueID)
  297. }
  298. func setIssueNotificationStatusReadIfUnread(ctx context.Context, userID, issueID int64) error {
  299. notification, err := GetIssueNotification(ctx, userID, issueID)
  300. // ignore if not exists
  301. if err != nil {
  302. return nil
  303. }
  304. if notification.Status != NotificationStatusUnread {
  305. return nil
  306. }
  307. notification.Status = NotificationStatusRead
  308. _, err = db.GetEngine(ctx).ID(notification.ID).Cols("status").Update(notification)
  309. return err
  310. }
  311. // SetRepoReadBy sets repo to be visited by given user.
  312. func SetRepoReadBy(ctx context.Context, userID, repoID int64) error {
  313. _, err := db.GetEngine(ctx).Where(builder.Eq{
  314. "user_id": userID,
  315. "status": NotificationStatusUnread,
  316. "source": NotificationSourceRepository,
  317. "repo_id": repoID,
  318. }).Cols("status").Update(&Notification{Status: NotificationStatusRead})
  319. return err
  320. }
  321. // SetNotificationStatus change the notification status
  322. func SetNotificationStatus(ctx context.Context, notificationID int64, user *user_model.User, status NotificationStatus) (*Notification, error) {
  323. notification, err := GetNotificationByID(ctx, notificationID)
  324. if err != nil {
  325. return notification, err
  326. }
  327. if notification.UserID != user.ID {
  328. return nil, fmt.Errorf("Can't change notification of another user: %d, %d", notification.UserID, user.ID)
  329. }
  330. notification.Status = status
  331. _, err = db.GetEngine(ctx).ID(notificationID).Update(notification)
  332. return notification, err
  333. }
  334. // GetNotificationByID return notification by ID
  335. func GetNotificationByID(ctx context.Context, notificationID int64) (*Notification, error) {
  336. notification := new(Notification)
  337. ok, err := db.GetEngine(ctx).
  338. Where("id = ?", notificationID).
  339. Get(notification)
  340. if err != nil {
  341. return nil, err
  342. }
  343. if !ok {
  344. return nil, db.ErrNotExist{Resource: "notification", ID: notificationID}
  345. }
  346. return notification, nil
  347. }
  348. // UpdateNotificationStatuses updates the statuses of all of a user's notifications that are of the currentStatus type to the desiredStatus
  349. func UpdateNotificationStatuses(ctx context.Context, user *user_model.User, currentStatus, desiredStatus NotificationStatus) error {
  350. n := &Notification{Status: desiredStatus, UpdatedBy: user.ID}
  351. _, err := db.GetEngine(ctx).
  352. Where("user_id = ? AND status = ?", user.ID, currentStatus).
  353. Cols("status", "updated_by", "updated_unix").
  354. Update(n)
  355. return err
  356. }