From db33c508854933347f7cc4cb0e84aa74cd4b5d7e Mon Sep 17 00:00:00 2001 From: Andrey Nering Date: Tue, 20 Dec 2016 20:27:54 -0200 Subject: [PATCH 1/3] Notifications - Step 1 --- models/models.go | 43 ++++- models/notification.go | 236 +++++++++++++++++++++++++++ modules/notification/notification.go | 46 ++++++ routers/repo/issue.go | 5 + 4 files changed, 321 insertions(+), 9 deletions(-) create mode 100644 models/notification.go create mode 100644 modules/notification/notification.go diff --git a/models/models.go b/models/models.go index 56306d61f5afe..dbfdbdd07eb19 100644 --- a/models/models.go +++ b/models/models.go @@ -68,15 +68,40 @@ var ( func init() { tables = append(tables, - new(User), new(PublicKey), new(AccessToken), - new(Repository), new(DeployKey), new(Collaboration), new(Access), new(Upload), - new(Watch), new(Star), new(Follow), new(Action), - new(Issue), new(PullRequest), new(Comment), new(Attachment), new(IssueUser), - new(Label), new(IssueLabel), new(Milestone), - new(Mirror), new(Release), new(LoginSource), new(Webhook), - new(UpdateTask), new(HookTask), - new(Team), new(OrgUser), new(TeamUser), new(TeamRepo), - new(Notice), new(EmailAddress)) + new(User), + new(PublicKey), + new(AccessToken), + new(Repository), + new(DeployKey), + new(Collaboration), + new(Access), + new(Upload), + new(Watch), + new(Star), + new(Follow), + new(Action), + new(Issue), + new(PullRequest), + new(Comment), + new(Attachment), + new(Label), + new(IssueLabel), + new(Milestone), + new(Mirror), + new(Release), + new(LoginSource), + new(Webhook), + new(UpdateTask), + new(HookTask), + new(Team), + new(OrgUser), + new(TeamUser), + new(TeamRepo), + new(Notice), + new(EmailAddress), + new(Notification), + new(IssueUser), + ) gonicNames := []string{"SSL", "UID"} for _, name := range gonicNames { diff --git a/models/notification.go b/models/notification.go new file mode 100644 index 0000000000000..fc8b0ce8d2ee5 --- /dev/null +++ b/models/notification.go @@ -0,0 +1,236 @@ +package models + +import ( + "time" +) + +type ( + // NotificationStatus is the status of the notification (read or unread) + NotificationStatus uint8 + // NotificationSource is the source of the notification (issue, PR, commit, etc) + NotificationSource uint8 +) + +const ( + // NotificationStatusUnread represents an unread notification + NotificationStatusUnread NotificationStatus = iota + 1 + // NotificationStatusRead represents a read notification + NotificationStatusRead +) + +const ( + // NotificationSourceIssue is a notification of an issue + NotificationSourceIssue NotificationSource = iota + 1 + // NotificationSourcePullRequest is a notification of a pull request + NotificationSourcePullRequest + // NotificationSourceCommit is a notification of a commit + NotificationSourceCommit +) + +// Notification represents a notification +type Notification struct { + ID int64 `xorm:"pk autoincr"` + UserID int64 `xorm:"INDEX NOT NULL"` + RepoID int64 `xorm:"INDEX NOT NULL"` + + Status NotificationStatus `xorm:"SMALLINT INDEX NOT NULL"` + Source NotificationSource `xorm:"SMALLINT INDEX NOT NULL"` + + IssueID int64 `xorm:"INDEX NOT NULL"` + CommitID string `xorm:"INDEX"` + + UpdatedBy int64 `xorm:"INDEX NOT NULL"` + + Issue *Issue `xorm:"-"` + PullRequest *PullRequest `xorm:"-"` + + Created time.Time `xorm:"-"` + CreatedUnix int64 `xorm:"INDEX NOT NULL"` + Updated time.Time `xorm:"-"` + UpdatedUnix int64 `xorm:"INDEX NOT NULL"` +} + +// BeforeInsert runs while inserting a record +func (n *Notification) BeforeInsert() { + var ( + now = time.Now() + nowUnix = now.Unix() + ) + n.Created = now + n.CreatedUnix = nowUnix + n.Updated = now + n.UpdatedUnix = nowUnix +} + +// BeforeUpdate runs while updateing a record +func (n *Notification) BeforeUpdate() { + var ( + now = time.Now() + nowUnix = now.Unix() + ) + n.Updated = now + n.UpdatedUnix = nowUnix +} + +// CreateOrUpdateIssueNotifications creates an issue notification +// for each watcher, or updates it if already exists +func CreateOrUpdateIssueNotifications(issue *Issue, notificationAuthorID int64) error { + sess := x.NewSession() + if err := sess.Begin(); err != nil { + return err + } + defer sess.Close() + + if err := createOrUpdateIssueNotifications(sess, issue, notificationAuthorID); err != nil { + return err + } + + return sess.Commit() +} + +func createOrUpdateIssueNotifications(e Engine, issue *Issue, notificationAuthorID int64) error { + watches, err := getWatchers(e, issue.RepoID) + if err != nil { + return err + } + + for _, watch := range watches { + // do not send notification for the own issuer/commenter + if watch.UserID == notificationAuthorID { + continue + } + + exists, err := issueNotificationExists(e, watch.UserID, issue.ID) + if err != nil { + return err + } + + if exists { + err = updateIssueNotification(e, watch.UserID, issue.ID, notificationAuthorID) + } else { + err = createIssueNotification(e, watch.UserID, issue, notificationAuthorID) + } + + if err != nil { + return err + } + } + + return nil +} + +func issueNotificationExists(e Engine, userID, issueID int64) (bool, error) { + count, err := e. + Where("user_id = ?", userID). + And("issue_id = ?", issueID). + Count(Notification{}) + return count > 0, err +} + +func createIssueNotification(e Engine, userID int64, issue *Issue, updatedByID int64) error { + notification := &Notification{ + UserID: userID, + RepoID: issue.RepoID, + Status: NotificationStatusUnread, + IssueID: issue.ID, + UpdatedBy: updatedByID, + } + + if issue.IsPull { + notification.Source = NotificationSourcePullRequest + } else { + notification.Source = NotificationSourceIssue + } + + _, err := e.Insert(notification) + return err +} + +func updateIssueNotification(e Engine, userID, issueID, updatedByID int64) error { + notification, err := getIssueNotification(e, userID, issueID) + if err != nil { + return err + } + + notification.Status = NotificationStatusUnread + notification.UpdatedBy = updatedByID + + _, err = e.Id(notification.ID).Update(notification) + return err +} + +func getIssueNotification(e Engine, userID, issueID int64) (*Notification, error) { + notification := new(Notification) + _, err := e. + Where("user_id = ?", userID). + And("issue_id = ?", issueID). + Get(notification) + return notification, err +} + +// NotificationsForUser returns notifications for a given user and status +func NotificationsForUser(user *User, status NotificationStatus) ([]*Notification, error) { + return notificationsForUser(x, user, status) +} +func notificationsForUser(e Engine, user *User, status NotificationStatus) (notifications []*Notification, err error) { + err = e. + Where("user_id = ?", user.ID). + And("status = ?", status). + OrderBy("updated_unix DESC"). + Find(¬ifications) + return +} + +// GetRepo returns the repo of the notification +func (n *Notification) GetRepo() (repo *Repository, err error) { + repo = new(Repository) + _, err = x. + Where("id = ?", n.RepoID). + Get(repo) + return +} + +// GetIssue returns the issue of the notification +func (n *Notification) GetIssue() (issue *Issue, err error) { + issue = new(Issue) + _, err = x. + Where("id = ?", n.IssueID). + Get(issue) + return +} + +// GetNotificationReadCount returns the notification read count for user +func GetNotificationReadCount(user *User) (int64, error) { + return GetNotificationCount(user, NotificationStatusRead) +} + +// GetNotificationUnreadCount returns the notification unread count for user +func GetNotificationUnreadCount(user *User) (int64, error) { + return GetNotificationCount(user, NotificationStatusUnread) +} + +// GetNotificationCount returns the notification count for user +func GetNotificationCount(user *User, status NotificationStatus) (int64, error) { + return getNotificationCount(x, user, status) +} + +func getNotificationCount(e Engine, user *User, status NotificationStatus) (count int64, err error) { + count, err = e. + Where("user_id = ?", user.ID). + And("status = ?", status). + Count(&Notification{}) + return +} + +func setNotificationStatusRead(e Engine, userID, issueID int64) error { + notification, err := getIssueNotification(e, userID, issueID) + // ignore if not exists + if err != nil { + return nil + } + + notification.Status = NotificationStatusRead + + _, err = e.Id(notification.ID).Update(notification) + return err +} diff --git a/modules/notification/notification.go b/modules/notification/notification.go new file mode 100644 index 0000000000000..894e637319b54 --- /dev/null +++ b/modules/notification/notification.go @@ -0,0 +1,46 @@ +package notification + +import ( + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/log" +) + +type ( + notificationService struct { + issueQueue chan issueNotificationOpts + } + + issueNotificationOpts struct { + issue *models.Issue + notificationAuthorID int64 + } +) + +var ( + // Service is the notification service + Service = ¬ificationService{ + issueQueue: make(chan issueNotificationOpts, 100), + } +) + +func init() { + go Service.Run() +} + +func (ns *notificationService) Run() { + for { + select { + case opts := <-ns.issueQueue: + if err := models.CreateOrUpdateIssueNotifications(opts.issue, opts.notificationAuthorID); err != nil { + log.Error(4, "Was unable to create issue notification: %v", err) + } + } + } +} + +func (ns *notificationService) NotifyIssue(issue *models.Issue, notificationAuthorID int64) { + ns.issueQueue <- issueNotificationOpts{ + issue, + notificationAuthorID, + } +} diff --git a/routers/repo/issue.go b/routers/repo/issue.go index e3c088d94024b..3a74ebc2c1c15 100644 --- a/routers/repo/issue.go +++ b/routers/repo/issue.go @@ -23,6 +23,7 @@ import ( "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markdown" + "code.gitea.io/gitea/modules/notification" "code.gitea.io/gitea/modules/setting" ) @@ -453,6 +454,8 @@ func NewIssuePost(ctx *context.Context, form auth.CreateIssueForm) { return } + notification.Service.NotifyIssue(issue, ctx.User.ID) + log.Trace("Issue created: %d/%d", repo.ID, issue.ID) ctx.Redirect(ctx.Repo.RepoLink + "/issues/" + com.ToStr(issue.Index)) } @@ -898,6 +901,8 @@ func NewComment(ctx *context.Context, form auth.CreateCommentForm) { return } + notification.Service.NotifyIssue(issue, ctx.User.ID) + log.Trace("Comment created: %d/%d/%d", ctx.Repo.Repository.ID, issue.ID, comment.ID) } From 175b5debfda5ce0ccb09f0fc992501287d61fe5f Mon Sep 17 00:00:00 2001 From: Andrey Nering Date: Thu, 22 Dec 2016 13:59:17 -0200 Subject: [PATCH 2/3] Set notification status to read --- models/issue.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/models/issue.go b/models/issue.go index 4937bf1b56d47..4c44e377de280 100644 --- a/models/issue.go +++ b/models/issue.go @@ -416,8 +416,16 @@ func (issue *Issue) GetAssignee() (err error) { } // ReadBy sets issue to be read by given user. -func (issue *Issue) ReadBy(uid int64) error { - return UpdateIssueUserByRead(uid, issue.ID) +func (issue *Issue) ReadBy(userID int64) error { + if err := UpdateIssueUserByRead(userID, issue.ID); err != nil { + return err + } + + if err := setNotificationStatusRead(x, userID, issue.ID); err != nil { + return err + } + + return nil } func updateIssueCols(e Engine, issue *Issue, cols ...string) error { From 4501dbc5704f9eb2158b47c1babaafb2973f6a17 Mon Sep 17 00:00:00 2001 From: Andrey Nering Date: Thu, 22 Dec 2016 14:16:23 -0200 Subject: [PATCH 3/3] Reduce number of queries for performance --- models/notification.go | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/models/notification.go b/models/notification.go index fc8b0ce8d2ee5..d792100f4ad69 100644 --- a/models/notification.go +++ b/models/notification.go @@ -94,18 +94,18 @@ func createOrUpdateIssueNotifications(e Engine, issue *Issue, notificationAuthor return err } + notifications, err := getNotificationsByIssueID(e, issue.ID) + if err != nil { + return err + } + for _, watch := range watches { // do not send notification for the own issuer/commenter if watch.UserID == notificationAuthorID { continue } - exists, err := issueNotificationExists(e, watch.UserID, issue.ID) - if err != nil { - return err - } - - if exists { + if notificationExists(notifications, issue.ID, watch.UserID) { err = updateIssueNotification(e, watch.UserID, issue.ID, notificationAuthorID) } else { err = createIssueNotification(e, watch.UserID, issue, notificationAuthorID) @@ -119,12 +119,21 @@ func createOrUpdateIssueNotifications(e Engine, issue *Issue, notificationAuthor return nil } -func issueNotificationExists(e Engine, userID, issueID int64) (bool, error) { - count, err := e. - Where("user_id = ?", userID). - And("issue_id = ?", issueID). - Count(Notification{}) - return count > 0, err +func getNotificationsByIssueID(e Engine, issueID int64) (notifications []*Notification, err error) { + err = e. + Where("issue_id = ?", issueID). + Find(¬ifications) + return +} + +func notificationExists(notifications []*Notification, issueID, userID int64) bool { + for _, notification := range notifications { + if notification.IssueID == issueID && notification.UserID == userID { + return true + } + } + + return false } func createIssueNotification(e Engine, userID int64, issue *Issue, updatedByID int64) error {