Notification - Step 1 (#523)
* Notification - Step 1 * Add copyright headers * Cache issue and repository on notification model
This commit is contained in:
		
							parent
							
								
									37eec6c9b7
								
							
						
					
					
						commit
						42904cb98a
					
				
					 5 changed files with 349 additions and 11 deletions
				
			
		|  | @ -443,8 +443,16 @@ func (issue *Issue) GetAssignee() (err error) { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // ReadBy sets issue to be read by given user.
 | // ReadBy sets issue to be read by given user.
 | ||||||
| func (issue *Issue) ReadBy(uid int64) error { | func (issue *Issue) ReadBy(userID int64) error { | ||||||
| 	return UpdateIssueUserByRead(uid, issue.ID) | 	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 { | func updateIssueCols(e Engine, issue *Issue, cols ...string) error { | ||||||
|  |  | ||||||
|  | @ -71,15 +71,41 @@ var ( | ||||||
| 
 | 
 | ||||||
| func init() { | func init() { | ||||||
| 	tables = append(tables, | 	tables = append(tables, | ||||||
| 		new(User), new(PublicKey), new(AccessToken), | 		new(User), | ||||||
| 		new(Repository), new(DeployKey), new(Collaboration), new(Access), new(Upload), | 		new(PublicKey), | ||||||
| 		new(Watch), new(Star), new(Follow), new(Action), | 		new(AccessToken), | ||||||
| 		new(Issue), new(PullRequest), new(Comment), new(Attachment), new(IssueUser), | 		new(Repository), | ||||||
| 		new(Label), new(IssueLabel), new(Milestone), | 		new(DeployKey), | ||||||
| 		new(Mirror), new(Release), new(LoginSource), new(Webhook), | 		new(Collaboration), | ||||||
| 		new(UpdateTask), new(HookTask), | 		new(Access), | ||||||
| 		new(Team), new(OrgUser), new(TeamUser), new(TeamRepo), | 		new(Upload), | ||||||
| 		new(Notice), new(EmailAddress), new(LFSMetaObject)) | 		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), | ||||||
|  | 		new(LFSMetaObject), | ||||||
|  | 	) | ||||||
| 
 | 
 | ||||||
| 	gonicNames := []string{"SSL", "UID"} | 	gonicNames := []string{"SSL", "UID"} | ||||||
| 	for _, name := range gonicNames { | 	for _, name := range gonicNames { | ||||||
|  |  | ||||||
							
								
								
									
										249
									
								
								models/notification.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										249
									
								
								models/notification.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,249 @@ | ||||||
|  | // Copyright 2016 The Gitea Authors. All rights reserved.
 | ||||||
|  | // Use of this source code is governed by a MIT-style
 | ||||||
|  | // license that can be found in the LICENSE file.
 | ||||||
|  | 
 | ||||||
|  | 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:"-"` | ||||||
|  | 	Repository *Repository `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() | ||||||
|  | 	defer sess.Close() | ||||||
|  | 	if err := sess.Begin(); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	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 | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	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 | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if notificationExists(notifications, issue.ID, watch.UserID) { | ||||||
|  | 			err = updateIssueNotification(e, watch.UserID, issue.ID, notificationAuthorID) | ||||||
|  | 		} else { | ||||||
|  | 			err = createIssueNotification(e, watch.UserID, issue, notificationAuthorID) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 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 { | ||||||
|  | 	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() (*Repository, error) { | ||||||
|  | 	n.Repository = new(Repository) | ||||||
|  | 	_, err := x. | ||||||
|  | 		Where("id = ?", n.RepoID). | ||||||
|  | 		Get(n.Repository) | ||||||
|  | 	return n.Repository, err | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // GetIssue returns the issue of the notification
 | ||||||
|  | func (n *Notification) GetIssue() (*Issue, error) { | ||||||
|  | 	n.Issue = new(Issue) | ||||||
|  | 	_, err := x. | ||||||
|  | 		Where("id = ?", n.IssueID). | ||||||
|  | 		Get(n.Issue) | ||||||
|  | 	return n.Issue, err | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // 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 | ||||||
|  | } | ||||||
							
								
								
									
										50
									
								
								modules/notification/notification.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								modules/notification/notification.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,50 @@ | ||||||
|  | // Copyright 2016 The Gitea Authors. All rights reserved.
 | ||||||
|  | // Use of this source code is governed by a MIT-style
 | ||||||
|  | // license that can be found in the LICENSE file.
 | ||||||
|  | 
 | ||||||
|  | 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, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | @ -24,6 +24,7 @@ import ( | ||||||
| 	"code.gitea.io/gitea/modules/context" | 	"code.gitea.io/gitea/modules/context" | ||||||
| 	"code.gitea.io/gitea/modules/log" | 	"code.gitea.io/gitea/modules/log" | ||||||
| 	"code.gitea.io/gitea/modules/markdown" | 	"code.gitea.io/gitea/modules/markdown" | ||||||
|  | 	"code.gitea.io/gitea/modules/notification" | ||||||
| 	"code.gitea.io/gitea/modules/setting" | 	"code.gitea.io/gitea/modules/setting" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -467,6 +468,8 @@ func NewIssuePost(ctx *context.Context, form auth.CreateIssueForm) { | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	notification.Service.NotifyIssue(issue, ctx.User.ID) | ||||||
|  | 
 | ||||||
| 	log.Trace("Issue created: %d/%d", repo.ID, issue.ID) | 	log.Trace("Issue created: %d/%d", repo.ID, issue.ID) | ||||||
| 	ctx.Redirect(ctx.Repo.RepoLink + "/issues/" + com.ToStr(issue.Index)) | 	ctx.Redirect(ctx.Repo.RepoLink + "/issues/" + com.ToStr(issue.Index)) | ||||||
| } | } | ||||||
|  | @ -931,6 +934,8 @@ func NewComment(ctx *context.Context, form auth.CreateCommentForm) { | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	notification.Service.NotifyIssue(issue, ctx.User.ID) | ||||||
|  | 
 | ||||||
| 	log.Trace("Comment created: %d/%d/%d", ctx.Repo.Repository.ID, issue.ID, comment.ID) | 	log.Trace("Comment created: %d/%d/%d", ctx.Repo.Repository.ID, issue.ID, comment.ID) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
		Loading…
	
		Reference in a new issue