[API] Add notification endpoint (#9488)
* [API] Add notification endpoints * add func GetNotifications(opts FindNotificationOptions) * add func (n *Notification) APIFormat() * add func (nl NotificationList) APIFormat() * add func (n *Notification) APIURL() * add func (nl NotificationList) APIFormat() * add LoadAttributes functions (loadRepo, loadIssue, loadComment, loadUser) * add func (c *Comment) APIURL() * add func (issue *Issue) GetLastComment() * add endpoint GET /notifications * add endpoint PUT /notifications * add endpoint GET /repos/{owner}/{repo}/notifications * add endpoint PUT /repos/{owner}/{repo}/notifications * add endpoint GET /notifications/threads/{id} * add endpoint PATCH /notifications/threads/{id} * Add TEST * code format * code format
This commit is contained in:
parent
ee9ce0cfa9
commit
6baa5d7588
15 changed files with 1124 additions and 28 deletions
|
@ -7,7 +7,7 @@
|
|||
updated_by: 2
|
||||
issue_id: 1
|
||||
created_unix: 946684800
|
||||
updated_unix: 946684800
|
||||
updated_unix: 946684820
|
||||
|
||||
-
|
||||
id: 2
|
||||
|
@ -17,8 +17,8 @@
|
|||
source: 1 # issue
|
||||
updated_by: 1
|
||||
issue_id: 2
|
||||
created_unix: 946684800
|
||||
updated_unix: 946684800
|
||||
created_unix: 946685800
|
||||
updated_unix: 946685820
|
||||
|
||||
-
|
||||
id: 3
|
||||
|
@ -27,9 +27,9 @@
|
|||
status: 3 # pinned
|
||||
source: 1 # issue
|
||||
updated_by: 1
|
||||
issue_id: 2
|
||||
created_unix: 946684800
|
||||
updated_unix: 946684800
|
||||
issue_id: 3
|
||||
created_unix: 946686800
|
||||
updated_unix: 946686800
|
||||
|
||||
-
|
||||
id: 4
|
||||
|
@ -38,6 +38,17 @@
|
|||
status: 1 # unread
|
||||
source: 1 # issue
|
||||
updated_by: 1
|
||||
issue_id: 2
|
||||
created_unix: 946684800
|
||||
updated_unix: 946684800
|
||||
issue_id: 5
|
||||
created_unix: 946687800
|
||||
updated_unix: 946687800
|
||||
|
||||
-
|
||||
id: 5
|
||||
user_id: 2
|
||||
repo_id: 2
|
||||
status: 1 # unread
|
||||
source: 1 # issue
|
||||
updated_by: 5
|
||||
issue_id: 4
|
||||
created_unix: 946688800
|
||||
updated_unix: 946688820
|
||||
|
|
|
@ -843,6 +843,20 @@ func (issue *Issue) GetLastEventLabel() string {
|
|||
return "repo.issues.opened_by"
|
||||
}
|
||||
|
||||
// GetLastComment return last comment for the current issue.
|
||||
func (issue *Issue) GetLastComment() (*Comment, error) {
|
||||
var c Comment
|
||||
exist, err := x.Where("type = ?", CommentTypeComment).
|
||||
And("issue_id = ?", issue.ID).Desc("id").Get(&c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !exist {
|
||||
return nil, nil
|
||||
}
|
||||
return &c, nil
|
||||
}
|
||||
|
||||
// GetLastEventLabelFake returns the localization label for the current issue without providing a link in the username.
|
||||
func (issue *Issue) GetLastEventLabelFake() string {
|
||||
if issue.IsClosed {
|
||||
|
|
|
@ -8,6 +8,7 @@ package models
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
|
@ -235,6 +236,22 @@ func (c *Comment) HTMLURL() string {
|
|||
return fmt.Sprintf("%s#%s", c.Issue.HTMLURL(), c.HashTag())
|
||||
}
|
||||
|
||||
// APIURL formats a API-string to the issue-comment
|
||||
func (c *Comment) APIURL() string {
|
||||
err := c.LoadIssue()
|
||||
if err != nil { // Silently dropping errors :unamused:
|
||||
log.Error("LoadIssue(%d): %v", c.IssueID, err)
|
||||
return ""
|
||||
}
|
||||
err = c.Issue.loadRepo(x)
|
||||
if err != nil { // Silently dropping errors :unamused:
|
||||
log.Error("loadRepo(%d): %v", c.Issue.RepoID, err)
|
||||
return ""
|
||||
}
|
||||
|
||||
return c.Issue.Repo.APIURL() + "/" + path.Join("issues/comments", fmt.Sprint(c.ID))
|
||||
}
|
||||
|
||||
// IssueURL formats a URL-string to the issue
|
||||
func (c *Comment) IssueURL() string {
|
||||
err := c.LoadIssue()
|
||||
|
|
|
@ -6,8 +6,14 @@ package models
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
|
||||
"xorm.io/builder"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
type (
|
||||
|
@ -47,17 +53,67 @@ type Notification struct {
|
|||
IssueID int64 `xorm:"INDEX NOT NULL"`
|
||||
CommitID string `xorm:"INDEX"`
|
||||
CommentID int64
|
||||
Comment *Comment `xorm:"-"`
|
||||
|
||||
UpdatedBy int64 `xorm:"INDEX NOT NULL"`
|
||||
|
||||
Issue *Issue `xorm:"-"`
|
||||
Repository *Repository `xorm:"-"`
|
||||
Comment *Comment `xorm:"-"`
|
||||
User *User `xorm:"-"`
|
||||
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"created INDEX NOT NULL"`
|
||||
UpdatedUnix timeutil.TimeStamp `xorm:"updated INDEX NOT NULL"`
|
||||
}
|
||||
|
||||
// FindNotificationOptions represent the filters for notifications. If an ID is 0 it will be ignored.
|
||||
type FindNotificationOptions struct {
|
||||
UserID int64
|
||||
RepoID int64
|
||||
IssueID int64
|
||||
Status NotificationStatus
|
||||
UpdatedAfterUnix int64
|
||||
UpdatedBeforeUnix int64
|
||||
}
|
||||
|
||||
// ToCond will convert each condition into a xorm-Cond
|
||||
func (opts *FindNotificationOptions) ToCond() builder.Cond {
|
||||
cond := builder.NewCond()
|
||||
if opts.UserID != 0 {
|
||||
cond = cond.And(builder.Eq{"notification.user_id": opts.UserID})
|
||||
}
|
||||
if opts.RepoID != 0 {
|
||||
cond = cond.And(builder.Eq{"notification.repo_id": opts.RepoID})
|
||||
}
|
||||
if opts.IssueID != 0 {
|
||||
cond = cond.And(builder.Eq{"notification.issue_id": opts.IssueID})
|
||||
}
|
||||
if opts.Status != 0 {
|
||||
cond = cond.And(builder.Eq{"notification.status": opts.Status})
|
||||
}
|
||||
if opts.UpdatedAfterUnix != 0 {
|
||||
cond = cond.And(builder.Gte{"notification.updated_unix": opts.UpdatedAfterUnix})
|
||||
}
|
||||
if opts.UpdatedBeforeUnix != 0 {
|
||||
cond = cond.And(builder.Lte{"notification.updated_unix": opts.UpdatedBeforeUnix})
|
||||
}
|
||||
return cond
|
||||
}
|
||||
|
||||
// ToSession will convert the given options to a xorm Session by using the conditions from ToCond and joining with issue table if required
|
||||
func (opts *FindNotificationOptions) ToSession(e Engine) *xorm.Session {
|
||||
return e.Where(opts.ToCond())
|
||||
}
|
||||
|
||||
func getNotifications(e Engine, options FindNotificationOptions) (nl NotificationList, err error) {
|
||||
err = options.ToSession(e).OrderBy("notification.updated_unix DESC").Find(&nl)
|
||||
return
|
||||
}
|
||||
|
||||
// GetNotifications returns all notifications that fit to the given options.
|
||||
func GetNotifications(opts FindNotificationOptions) (NotificationList, error) {
|
||||
return getNotifications(x, opts)
|
||||
}
|
||||
|
||||
// CreateOrUpdateIssueNotifications creates an issue notification
|
||||
// for each watcher, or updates it if already exists
|
||||
func CreateOrUpdateIssueNotifications(issueID, commentID int64, notificationAuthorID int64) error {
|
||||
|
@ -238,22 +294,124 @@ func notificationsForUser(e Engine, user *User, statuses []NotificationStatus, p
|
|||
return
|
||||
}
|
||||
|
||||
// APIFormat converts a Notification to api.NotificationThread
|
||||
func (n *Notification) APIFormat() *api.NotificationThread {
|
||||
result := &api.NotificationThread{
|
||||
ID: n.ID,
|
||||
Unread: !(n.Status == NotificationStatusRead || n.Status == NotificationStatusPinned),
|
||||
Pinned: n.Status == NotificationStatusPinned,
|
||||
UpdatedAt: n.UpdatedUnix.AsTime(),
|
||||
URL: n.APIURL(),
|
||||
}
|
||||
|
||||
//since user only get notifications when he has access to use minimal access mode
|
||||
if n.Repository != nil {
|
||||
result.Repository = n.Repository.APIFormat(AccessModeRead)
|
||||
}
|
||||
|
||||
//handle Subject
|
||||
switch n.Source {
|
||||
case NotificationSourceIssue:
|
||||
result.Subject = &api.NotificationSubject{Type: "Issue"}
|
||||
if n.Issue != nil {
|
||||
result.Subject.Title = n.Issue.Title
|
||||
result.Subject.URL = n.Issue.APIURL()
|
||||
comment, err := n.Issue.GetLastComment()
|
||||
if err == nil && comment != nil {
|
||||
result.Subject.LatestCommentURL = comment.APIURL()
|
||||
}
|
||||
}
|
||||
case NotificationSourcePullRequest:
|
||||
result.Subject = &api.NotificationSubject{Type: "Pull"}
|
||||
if n.Issue != nil {
|
||||
result.Subject.Title = n.Issue.Title
|
||||
result.Subject.URL = n.Issue.APIURL()
|
||||
comment, err := n.Issue.GetLastComment()
|
||||
if err == nil && comment != nil {
|
||||
result.Subject.LatestCommentURL = comment.APIURL()
|
||||
}
|
||||
}
|
||||
case NotificationSourceCommit:
|
||||
result.Subject = &api.NotificationSubject{
|
||||
Type: "Commit",
|
||||
Title: n.CommitID,
|
||||
}
|
||||
//unused until now
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// LoadAttributes load Repo Issue User and Comment if not loaded
|
||||
func (n *Notification) LoadAttributes() (err error) {
|
||||
return n.loadAttributes(x)
|
||||
}
|
||||
|
||||
func (n *Notification) loadAttributes(e Engine) (err error) {
|
||||
if err = n.loadRepo(e); err != nil {
|
||||
return
|
||||
}
|
||||
if err = n.loadIssue(e); err != nil {
|
||||
return
|
||||
}
|
||||
if err = n.loadUser(e); err != nil {
|
||||
return
|
||||
}
|
||||
if err = n.loadComment(e); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (n *Notification) loadRepo(e Engine) (err error) {
|
||||
if n.Repository == nil {
|
||||
n.Repository, err = getRepositoryByID(e, n.RepoID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getRepositoryByID [%d]: %v", n.RepoID, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *Notification) loadIssue(e Engine) (err error) {
|
||||
if n.Issue == nil {
|
||||
n.Issue, err = getIssueByID(e, n.IssueID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getIssueByID [%d]: %v", n.IssueID, err)
|
||||
}
|
||||
return n.Issue.loadAttributes(e)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *Notification) loadComment(e Engine) (err error) {
|
||||
if n.Comment == nil && n.CommentID > 0 {
|
||||
n.Comment, err = GetCommentByID(n.CommentID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("GetCommentByID [%d]: %v", n.CommentID, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *Notification) loadUser(e Engine) (err error) {
|
||||
if n.User == nil {
|
||||
n.User, err = getUserByID(e, n.UserID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getUserByID [%d]: %v", n.UserID, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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
|
||||
return n.Repository, n.loadRepo(x)
|
||||
}
|
||||
|
||||
// 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
|
||||
return n.Issue, n.loadIssue(x)
|
||||
}
|
||||
|
||||
// HTMLURL formats a URL-string to the notification
|
||||
|
@ -264,9 +422,34 @@ func (n *Notification) HTMLURL() string {
|
|||
return n.Issue.HTMLURL()
|
||||
}
|
||||
|
||||
// APIURL formats a URL-string to the notification
|
||||
func (n *Notification) APIURL() string {
|
||||
return setting.AppURL + path.Join("api/v1/notifications/threads", fmt.Sprintf("%d", n.ID))
|
||||
}
|
||||
|
||||
// NotificationList contains a list of notifications
|
||||
type NotificationList []*Notification
|
||||
|
||||
// APIFormat converts a NotificationList to api.NotificationThread list
|
||||
func (nl NotificationList) APIFormat() []*api.NotificationThread {
|
||||
var result = make([]*api.NotificationThread, 0, len(nl))
|
||||
for _, n := range nl {
|
||||
result = append(result, n.APIFormat())
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// LoadAttributes load Repo Issue User and Comment if not loaded
|
||||
func (nl NotificationList) LoadAttributes() (err error) {
|
||||
for i := 0; i < len(nl); i++ {
|
||||
err = nl[i].LoadAttributes()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (nl NotificationList) getPendingRepoIDs() []int64 {
|
||||
var ids = make(map[int64]struct{}, len(nl))
|
||||
for _, notification := range nl {
|
||||
|
@ -486,7 +669,7 @@ func setNotificationStatusReadIfUnread(e Engine, userID, issueID int64) error {
|
|||
|
||||
// SetNotificationStatus change the notification status
|
||||
func SetNotificationStatus(notificationID int64, user *User, status NotificationStatus) error {
|
||||
notification, err := getNotificationByID(notificationID)
|
||||
notification, err := getNotificationByID(x, notificationID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -501,9 +684,14 @@ func SetNotificationStatus(notificationID int64, user *User, status Notification
|
|||
return err
|
||||
}
|
||||
|
||||
func getNotificationByID(notificationID int64) (*Notification, error) {
|
||||
// GetNotificationByID return notification by ID
|
||||
func GetNotificationByID(notificationID int64) (*Notification, error) {
|
||||
return getNotificationByID(x, notificationID)
|
||||
}
|
||||
|
||||
func getNotificationByID(e Engine, notificationID int64) (*Notification, error) {
|
||||
notification := new(Notification)
|
||||
ok, err := x.
|
||||
ok, err := e.
|
||||
Where("id = ?", notificationID).
|
||||
Get(notification)
|
||||
|
||||
|
@ -512,7 +700,7 @@ func getNotificationByID(notificationID int64) (*Notification, error) {
|
|||
}
|
||||
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Notification %d does not exists", notificationID)
|
||||
return nil, ErrNotExist{ID: notificationID}
|
||||
}
|
||||
|
||||
return notification, nil
|
||||
|
|
|
@ -31,11 +31,13 @@ func TestNotificationsForUser(t *testing.T) {
|
|||
statuses := []NotificationStatus{NotificationStatusRead, NotificationStatusUnread}
|
||||
notfs, err := NotificationsForUser(user, statuses, 1, 10)
|
||||
assert.NoError(t, err)
|
||||
if assert.Len(t, notfs, 2) {
|
||||
assert.EqualValues(t, 2, notfs[0].ID)
|
||||
if assert.Len(t, notfs, 3) {
|
||||
assert.EqualValues(t, 5, notfs[0].ID)
|
||||
assert.EqualValues(t, user.ID, notfs[0].UserID)
|
||||
assert.EqualValues(t, 4, notfs[1].ID)
|
||||
assert.EqualValues(t, user.ID, notfs[1].UserID)
|
||||
assert.EqualValues(t, 2, notfs[2].ID)
|
||||
assert.EqualValues(t, user.ID, notfs[2].UserID)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue