[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
				
			
		
							
								
								
									
										106
									
								
								integrations/api_notification_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								integrations/api_notification_test.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,106 @@ | |||
| // Copyright 2020 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 integrations | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"code.gitea.io/gitea/models" | ||||
| 	api "code.gitea.io/gitea/modules/structs" | ||||
| 
 | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
| 
 | ||||
| func TestAPINotification(t *testing.T) { | ||||
| 	defer prepareTestEnv(t)() | ||||
| 
 | ||||
| 	user2 := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User) | ||||
| 	repo1 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository) | ||||
| 	thread5 := models.AssertExistsAndLoadBean(t, &models.Notification{ID: 5}).(*models.Notification) | ||||
| 	assert.NoError(t, thread5.LoadAttributes()) | ||||
| 	session := loginUser(t, user2.Name) | ||||
| 	token := getTokenForLoggedInUser(t, session) | ||||
| 
 | ||||
| 	// -- GET /notifications --
 | ||||
| 	// test filter
 | ||||
| 	since := "2000-01-01T00%3A50%3A01%2B00%3A00" //946687801
 | ||||
| 	req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/notifications?since=%s&token=%s", since, token)) | ||||
| 	resp := session.MakeRequest(t, req, http.StatusOK) | ||||
| 	var apiNL []api.NotificationThread | ||||
| 	DecodeJSON(t, resp, &apiNL) | ||||
| 
 | ||||
| 	assert.Len(t, apiNL, 1) | ||||
| 	assert.EqualValues(t, 5, apiNL[0].ID) | ||||
| 
 | ||||
| 	// test filter
 | ||||
| 	before := "2000-01-01T01%3A06%3A59%2B00%3A00" //946688819
 | ||||
| 
 | ||||
| 	req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/notifications?all=%s&before=%s&token=%s", "true", before, token)) | ||||
| 	resp = session.MakeRequest(t, req, http.StatusOK) | ||||
| 	DecodeJSON(t, resp, &apiNL) | ||||
| 
 | ||||
| 	assert.Len(t, apiNL, 3) | ||||
| 	assert.EqualValues(t, 4, apiNL[0].ID) | ||||
| 	assert.EqualValues(t, true, apiNL[0].Unread) | ||||
| 	assert.EqualValues(t, false, apiNL[0].Pinned) | ||||
| 	assert.EqualValues(t, 3, apiNL[1].ID) | ||||
| 	assert.EqualValues(t, false, apiNL[1].Unread) | ||||
| 	assert.EqualValues(t, true, apiNL[1].Pinned) | ||||
| 	assert.EqualValues(t, 2, apiNL[2].ID) | ||||
| 	assert.EqualValues(t, false, apiNL[2].Unread) | ||||
| 	assert.EqualValues(t, false, apiNL[2].Pinned) | ||||
| 
 | ||||
| 	// -- GET /repos/{owner}/{repo}/notifications --
 | ||||
| 	req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/notifications?token=%s", user2.Name, repo1.Name, token)) | ||||
| 	resp = session.MakeRequest(t, req, http.StatusOK) | ||||
| 	DecodeJSON(t, resp, &apiNL) | ||||
| 
 | ||||
| 	assert.Len(t, apiNL, 1) | ||||
| 	assert.EqualValues(t, 4, apiNL[0].ID) | ||||
| 
 | ||||
| 	// -- GET /notifications/threads/{id} --
 | ||||
| 	// get forbidden
 | ||||
| 	req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/notifications/threads/%d?token=%s", 1, token)) | ||||
| 	resp = session.MakeRequest(t, req, http.StatusForbidden) | ||||
| 
 | ||||
| 	// get own
 | ||||
| 	req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/notifications/threads/%d?token=%s", thread5.ID, token)) | ||||
| 	resp = session.MakeRequest(t, req, http.StatusOK) | ||||
| 	var apiN api.NotificationThread | ||||
| 	DecodeJSON(t, resp, &apiN) | ||||
| 
 | ||||
| 	assert.EqualValues(t, 5, apiN.ID) | ||||
| 	assert.EqualValues(t, false, apiN.Pinned) | ||||
| 	assert.EqualValues(t, true, apiN.Unread) | ||||
| 	assert.EqualValues(t, "issue4", apiN.Subject.Title) | ||||
| 	assert.EqualValues(t, "Issue", apiN.Subject.Type) | ||||
| 	assert.EqualValues(t, thread5.Issue.APIURL(), apiN.Subject.URL) | ||||
| 	assert.EqualValues(t, thread5.Repository.HTMLURL(), apiN.Repository.HTMLURL) | ||||
| 
 | ||||
| 	// -- mark notifications as read --
 | ||||
| 	req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/notifications?token=%s", token)) | ||||
| 	resp = session.MakeRequest(t, req, http.StatusOK) | ||||
| 	DecodeJSON(t, resp, &apiNL) | ||||
| 	assert.Len(t, apiNL, 2) | ||||
| 
 | ||||
| 	lastReadAt := "2000-01-01T00%3A50%3A01%2B00%3A00" //946687801 <- only Notification 4 is in this filter ...
 | ||||
| 	req = NewRequest(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%s/notifications?last_read_at=%s&token=%s", user2.Name, repo1.Name, lastReadAt, token)) | ||||
| 	resp = session.MakeRequest(t, req, http.StatusResetContent) | ||||
| 
 | ||||
| 	req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/notifications?token=%s", token)) | ||||
| 	resp = session.MakeRequest(t, req, http.StatusOK) | ||||
| 	DecodeJSON(t, resp, &apiNL) | ||||
| 	assert.Len(t, apiNL, 1) | ||||
| 
 | ||||
| 	// -- PATCH /notifications/threads/{id} --
 | ||||
| 	req = NewRequest(t, "PATCH", fmt.Sprintf("/api/v1/notifications/threads/%d?token=%s", thread5.ID, token)) | ||||
| 	resp = session.MakeRequest(t, req, http.StatusResetContent) | ||||
| 
 | ||||
| 	assert.Equal(t, models.NotificationStatusUnread, thread5.Status) | ||||
| 	thread5 = models.AssertExistsAndLoadBean(t, &models.Notification{ID: 5}).(*models.Notification) | ||||
| 	assert.Equal(t, models.NotificationStatusRead, thread5.Status) | ||||
| } | ||||
|  | @ -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) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										28
									
								
								modules/structs/notifications.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								modules/structs/notifications.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,28 @@ | |||
| // Copyright 2019 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 structs | ||||
| 
 | ||||
| import ( | ||||
| 	"time" | ||||
| ) | ||||
| 
 | ||||
| // NotificationThread expose Notification on API
 | ||||
| type NotificationThread struct { | ||||
| 	ID         int64                `json:"id"` | ||||
| 	Repository *Repository          `json:"repository"` | ||||
| 	Subject    *NotificationSubject `json:"subject"` | ||||
| 	Unread     bool                 `json:"unread"` | ||||
| 	Pinned     bool                 `json:"pinned"` | ||||
| 	UpdatedAt  time.Time            `json:"updated_at"` | ||||
| 	URL        string               `json:"url"` | ||||
| } | ||||
| 
 | ||||
| // NotificationSubject contains the notification subject (Issue/Pull/Commit)
 | ||||
| type NotificationSubject struct { | ||||
| 	Title            string `json:"title"` | ||||
| 	URL              string `json:"url"` | ||||
| 	LatestCommentURL string `json:"latest_comment_url"` | ||||
| 	Type             string `json:"type" binding:"In(Issue,Pull,Commit)"` | ||||
| } | ||||
|  | @ -56,10 +56,10 @@ func CreateUser(ctx *context.APIContext, form api.CreateUserOption) { | |||
| 	// responses:
 | ||||
| 	//   "201":
 | ||||
| 	//     "$ref": "#/responses/User"
 | ||||
| 	//   "403":
 | ||||
| 	//     "$ref": "#/responses/forbidden"
 | ||||
| 	//   "400":
 | ||||
| 	//     "$ref": "#/responses/error"
 | ||||
| 	//   "403":
 | ||||
| 	//     "$ref": "#/responses/forbidden"
 | ||||
| 	//   "422":
 | ||||
| 	//     "$ref": "#/responses/validationError"
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -70,6 +70,7 @@ import ( | |||
| 	api "code.gitea.io/gitea/modules/structs" | ||||
| 	"code.gitea.io/gitea/routers/api/v1/admin" | ||||
| 	"code.gitea.io/gitea/routers/api/v1/misc" | ||||
| 	"code.gitea.io/gitea/routers/api/v1/notify" | ||||
| 	"code.gitea.io/gitea/routers/api/v1/org" | ||||
| 	"code.gitea.io/gitea/routers/api/v1/repo" | ||||
| 	_ "code.gitea.io/gitea/routers/api/v1/swagger" // for swagger generation
 | ||||
|  | @ -512,6 +513,16 @@ func RegisterRoutes(m *macaron.Macaron) { | |||
| 		m.Post("/markdown", bind(api.MarkdownOption{}), misc.Markdown) | ||||
| 		m.Post("/markdown/raw", misc.MarkdownRaw) | ||||
| 
 | ||||
| 		// Notifications
 | ||||
| 		m.Group("/notifications", func() { | ||||
| 			m.Combo(""). | ||||
| 				Get(notify.ListNotifications). | ||||
| 				Put(notify.ReadNotifications) | ||||
| 			m.Combo("/threads/:id"). | ||||
| 				Get(notify.GetThread). | ||||
| 				Patch(notify.ReadThread) | ||||
| 		}, reqToken()) | ||||
| 
 | ||||
| 		// Users
 | ||||
| 		m.Group("/users", func() { | ||||
| 			m.Get("/search", user.Search) | ||||
|  | @ -610,6 +621,9 @@ func RegisterRoutes(m *macaron.Macaron) { | |||
| 				m.Combo("").Get(reqAnyRepoReader(), repo.Get). | ||||
| 					Delete(reqToken(), reqOwner(), repo.Delete). | ||||
| 					Patch(reqToken(), reqAdmin(), bind(api.EditRepoOption{}), context.RepoRef(), repo.Edit) | ||||
| 				m.Combo("/notifications"). | ||||
| 					Get(reqToken(), notify.ListRepoNotifications). | ||||
| 					Put(reqToken(), notify.ReadRepoNotifications) | ||||
| 				m.Group("/hooks", func() { | ||||
| 					m.Combo("").Get(repo.ListHooks). | ||||
| 						Post(bind(api.CreateHookOption{}), repo.CreateHook) | ||||
|  |  | |||
							
								
								
									
										151
									
								
								routers/api/v1/notify/repo.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										151
									
								
								routers/api/v1/notify/repo.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,151 @@ | |||
| // Copyright 2020 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 notify | ||||
| 
 | ||||
| import ( | ||||
| 	"net/http" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"code.gitea.io/gitea/models" | ||||
| 	"code.gitea.io/gitea/modules/context" | ||||
| 	"code.gitea.io/gitea/routers/api/v1/utils" | ||||
| ) | ||||
| 
 | ||||
| // ListRepoNotifications list users's notification threads on a specific repo
 | ||||
| func ListRepoNotifications(ctx *context.APIContext) { | ||||
| 	// swagger:operation GET /repos/{owner}/{repo}/notifications notification notifyGetRepoList
 | ||||
| 	// ---
 | ||||
| 	// summary: List users's notification threads on a specific repo
 | ||||
| 	// consumes:
 | ||||
| 	// - application/json
 | ||||
| 	// produces:
 | ||||
| 	// - application/json
 | ||||
| 	// parameters:
 | ||||
| 	// - name: owner
 | ||||
| 	//   in: path
 | ||||
| 	//   description: owner of the repo
 | ||||
| 	//   type: string
 | ||||
| 	//   required: true
 | ||||
| 	// - name: repo
 | ||||
| 	//   in: path
 | ||||
| 	//   description: name of the repo
 | ||||
| 	//   type: string
 | ||||
| 	//   required: true
 | ||||
| 	// - name: all
 | ||||
| 	//   in: query
 | ||||
| 	//   description: If true, show notifications marked as read. Default value is false
 | ||||
| 	//   type: string
 | ||||
| 	//   required: false
 | ||||
| 	// - name: since
 | ||||
| 	//   in: query
 | ||||
| 	//   description: Only show notifications updated after the given time. This is a timestamp in RFC 3339 format
 | ||||
| 	//   type: string
 | ||||
| 	//   format: date-time
 | ||||
| 	//   required: false
 | ||||
| 	// - name: before
 | ||||
| 	//   in: query
 | ||||
| 	//   description: Only show notifications updated before the given time. This is a timestamp in RFC 3339 format
 | ||||
| 	//   type: string
 | ||||
| 	//   format: date-time
 | ||||
| 	//   required: false
 | ||||
| 	// responses:
 | ||||
| 	//   "200":
 | ||||
| 	//     "$ref": "#/responses/NotificationThreadList"
 | ||||
| 
 | ||||
| 	before, since, err := utils.GetQueryBeforeSince(ctx) | ||||
| 	if err != nil { | ||||
| 		ctx.InternalServerError(err) | ||||
| 		return | ||||
| 	} | ||||
| 	opts := models.FindNotificationOptions{ | ||||
| 		UserID:            ctx.User.ID, | ||||
| 		RepoID:            ctx.Repo.Repository.ID, | ||||
| 		UpdatedBeforeUnix: before, | ||||
| 		UpdatedAfterUnix:  since, | ||||
| 	} | ||||
| 	qAll := strings.Trim(ctx.Query("all"), " ") | ||||
| 	if qAll != "true" { | ||||
| 		opts.Status = models.NotificationStatusUnread | ||||
| 	} | ||||
| 	nl, err := models.GetNotifications(opts) | ||||
| 	if err != nil { | ||||
| 		ctx.InternalServerError(err) | ||||
| 		return | ||||
| 	} | ||||
| 	err = nl.LoadAttributes() | ||||
| 	if err != nil { | ||||
| 		ctx.InternalServerError(err) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	ctx.JSON(http.StatusOK, nl.APIFormat()) | ||||
| } | ||||
| 
 | ||||
| // ReadRepoNotifications mark notification threads as read on a specific repo
 | ||||
| func ReadRepoNotifications(ctx *context.APIContext) { | ||||
| 	// swagger:operation PUT /repos/{owner}/{repo}/notifications notification notifyReadRepoList
 | ||||
| 	// ---
 | ||||
| 	// summary: Mark notification threads as read on a specific repo
 | ||||
| 	// consumes:
 | ||||
| 	// - application/json
 | ||||
| 	// produces:
 | ||||
| 	// - application/json
 | ||||
| 	// parameters:
 | ||||
| 	// - name: owner
 | ||||
| 	//   in: path
 | ||||
| 	//   description: owner of the repo
 | ||||
| 	//   type: string
 | ||||
| 	//   required: true
 | ||||
| 	// - name: repo
 | ||||
| 	//   in: path
 | ||||
| 	//   description: name of the repo
 | ||||
| 	//   type: string
 | ||||
| 	//   required: true
 | ||||
| 	// - name: last_read_at
 | ||||
| 	//   in: query
 | ||||
| 	//   description: Describes the last point that notifications were checked. Anything updated since this time will not be updated.
 | ||||
| 	//   type: string
 | ||||
| 	//   format: date-time
 | ||||
| 	//   required: false
 | ||||
| 	// responses:
 | ||||
| 	//   "205":
 | ||||
| 	//     "$ref": "#/responses/empty"
 | ||||
| 
 | ||||
| 	lastRead := int64(0) | ||||
| 	qLastRead := strings.Trim(ctx.Query("last_read_at"), " ") | ||||
| 	if len(qLastRead) > 0 { | ||||
| 		tmpLastRead, err := time.Parse(time.RFC3339, qLastRead) | ||||
| 		if err != nil { | ||||
| 			ctx.InternalServerError(err) | ||||
| 			return | ||||
| 		} | ||||
| 		if !tmpLastRead.IsZero() { | ||||
| 			lastRead = tmpLastRead.Unix() | ||||
| 		} | ||||
| 	} | ||||
| 	opts := models.FindNotificationOptions{ | ||||
| 		UserID:            ctx.User.ID, | ||||
| 		RepoID:            ctx.Repo.Repository.ID, | ||||
| 		UpdatedBeforeUnix: lastRead, | ||||
| 		Status:            models.NotificationStatusUnread, | ||||
| 	} | ||||
| 	nl, err := models.GetNotifications(opts) | ||||
| 	if err != nil { | ||||
| 		ctx.InternalServerError(err) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	for _, n := range nl { | ||||
| 		err := models.SetNotificationStatus(n.ID, ctx.User, models.NotificationStatusRead) | ||||
| 		if err != nil { | ||||
| 			ctx.InternalServerError(err) | ||||
| 			return | ||||
| 		} | ||||
| 		ctx.Status(http.StatusResetContent) | ||||
| 	} | ||||
| 
 | ||||
| 	ctx.Status(http.StatusResetContent) | ||||
| } | ||||
							
								
								
									
										101
									
								
								routers/api/v1/notify/threads.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								routers/api/v1/notify/threads.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,101 @@ | |||
| // Copyright 2020 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 notify | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 
 | ||||
| 	"code.gitea.io/gitea/models" | ||||
| 	"code.gitea.io/gitea/modules/context" | ||||
| ) | ||||
| 
 | ||||
| // GetThread get notification by ID
 | ||||
| func GetThread(ctx *context.APIContext) { | ||||
| 	// swagger:operation GET /notifications/threads/{id} notification notifyGetThread
 | ||||
| 	// ---
 | ||||
| 	// summary: Get notification thread by ID
 | ||||
| 	// consumes:
 | ||||
| 	// - application/json
 | ||||
| 	// produces:
 | ||||
| 	// - application/json
 | ||||
| 	// parameters:
 | ||||
| 	// - name: id
 | ||||
| 	//   in: path
 | ||||
| 	//   description: id of notification thread
 | ||||
| 	//   type: string
 | ||||
| 	//   required: true
 | ||||
| 	// responses:
 | ||||
| 	//   "200":
 | ||||
| 	//     "$ref": "#/responses/NotificationThread"
 | ||||
| 	//   "403":
 | ||||
| 	//     "$ref": "#/responses/forbidden"
 | ||||
| 	//   "404":
 | ||||
| 	//     "$ref": "#/responses/notFound"
 | ||||
| 
 | ||||
| 	n := getThread(ctx) | ||||
| 	if n == nil { | ||||
| 		return | ||||
| 	} | ||||
| 	if err := n.LoadAttributes(); err != nil { | ||||
| 		ctx.InternalServerError(err) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	ctx.JSON(http.StatusOK, n.APIFormat()) | ||||
| } | ||||
| 
 | ||||
| // ReadThread mark notification as read by ID
 | ||||
| func ReadThread(ctx *context.APIContext) { | ||||
| 	// swagger:operation PATCH /notifications/threads/{id} notification notifyReadThread
 | ||||
| 	// ---
 | ||||
| 	// summary: Mark notification thread as read by ID
 | ||||
| 	// consumes:
 | ||||
| 	// - application/json
 | ||||
| 	// produces:
 | ||||
| 	// - application/json
 | ||||
| 	// parameters:
 | ||||
| 	// - name: id
 | ||||
| 	//   in: path
 | ||||
| 	//   description: id of notification thread
 | ||||
| 	//   type: string
 | ||||
| 	//   required: true
 | ||||
| 	// responses:
 | ||||
| 	//   "205":
 | ||||
| 	//     "$ref": "#/responses/empty"
 | ||||
| 	//   "403":
 | ||||
| 	//     "$ref": "#/responses/forbidden"
 | ||||
| 	//   "404":
 | ||||
| 	//     "$ref": "#/responses/notFound"
 | ||||
| 
 | ||||
| 	n := getThread(ctx) | ||||
| 	if n == nil { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	err := models.SetNotificationStatus(n.ID, ctx.User, models.NotificationStatusRead) | ||||
| 	if err != nil { | ||||
| 		ctx.InternalServerError(err) | ||||
| 		return | ||||
| 	} | ||||
| 	ctx.Status(http.StatusResetContent) | ||||
| } | ||||
| 
 | ||||
| func getThread(ctx *context.APIContext) *models.Notification { | ||||
| 	n, err := models.GetNotificationByID(ctx.ParamsInt64(":id")) | ||||
| 	if err != nil { | ||||
| 		if models.IsErrNotExist(err) { | ||||
| 			ctx.Error(http.StatusNotFound, "GetNotificationByID", err) | ||||
| 		} else { | ||||
| 			ctx.InternalServerError(err) | ||||
| 		} | ||||
| 		return nil | ||||
| 	} | ||||
| 	if n.UserID != ctx.User.ID && !ctx.User.IsAdmin { | ||||
| 		ctx.Error(http.StatusForbidden, "GetNotificationByID", fmt.Errorf("only user itself and admin are allowed to read/change this thread %d", n.ID)) | ||||
| 		return nil | ||||
| 	} | ||||
| 	return n | ||||
| } | ||||
							
								
								
									
										129
									
								
								routers/api/v1/notify/user.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										129
									
								
								routers/api/v1/notify/user.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,129 @@ | |||
| // Copyright 2020 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 notify | ||||
| 
 | ||||
| import ( | ||||
| 	"net/http" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"code.gitea.io/gitea/models" | ||||
| 	"code.gitea.io/gitea/modules/context" | ||||
| 	"code.gitea.io/gitea/routers/api/v1/utils" | ||||
| ) | ||||
| 
 | ||||
| // ListNotifications list users's notification threads
 | ||||
| func ListNotifications(ctx *context.APIContext) { | ||||
| 	// swagger:operation GET /notifications notification notifyGetList
 | ||||
| 	// ---
 | ||||
| 	// summary: List users's notification threads
 | ||||
| 	// consumes:
 | ||||
| 	// - application/json
 | ||||
| 	// produces:
 | ||||
| 	// - application/json
 | ||||
| 	// parameters:
 | ||||
| 	// - name: all
 | ||||
| 	//   in: query
 | ||||
| 	//   description: If true, show notifications marked as read. Default value is false
 | ||||
| 	//   type: string
 | ||||
| 	//   required: false
 | ||||
| 	// - name: since
 | ||||
| 	//   in: query
 | ||||
| 	//   description: Only show notifications updated after the given time. This is a timestamp in RFC 3339 format
 | ||||
| 	//   type: string
 | ||||
| 	//   format: date-time
 | ||||
| 	//   required: false
 | ||||
| 	// - name: before
 | ||||
| 	//   in: query
 | ||||
| 	//   description: Only show notifications updated before the given time. This is a timestamp in RFC 3339 format
 | ||||
| 	//   type: string
 | ||||
| 	//   format: date-time
 | ||||
| 	//   required: false
 | ||||
| 	// responses:
 | ||||
| 	//   "200":
 | ||||
| 	//     "$ref": "#/responses/NotificationThreadList"
 | ||||
| 
 | ||||
| 	before, since, err := utils.GetQueryBeforeSince(ctx) | ||||
| 	if err != nil { | ||||
| 		ctx.InternalServerError(err) | ||||
| 		return | ||||
| 	} | ||||
| 	opts := models.FindNotificationOptions{ | ||||
| 		UserID:            ctx.User.ID, | ||||
| 		UpdatedBeforeUnix: before, | ||||
| 		UpdatedAfterUnix:  since, | ||||
| 	} | ||||
| 	qAll := strings.Trim(ctx.Query("all"), " ") | ||||
| 	if qAll != "true" { | ||||
| 		opts.Status = models.NotificationStatusUnread | ||||
| 	} | ||||
| 	nl, err := models.GetNotifications(opts) | ||||
| 	if err != nil { | ||||
| 		ctx.InternalServerError(err) | ||||
| 		return | ||||
| 	} | ||||
| 	err = nl.LoadAttributes() | ||||
| 	if err != nil { | ||||
| 		ctx.InternalServerError(err) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	ctx.JSON(http.StatusOK, nl.APIFormat()) | ||||
| } | ||||
| 
 | ||||
| // ReadNotifications mark notification threads as read
 | ||||
| func ReadNotifications(ctx *context.APIContext) { | ||||
| 	// swagger:operation PUT /notifications notification notifyReadList
 | ||||
| 	// ---
 | ||||
| 	// summary: Mark notification threads as read
 | ||||
| 	// consumes:
 | ||||
| 	// - application/json
 | ||||
| 	// produces:
 | ||||
| 	// - application/json
 | ||||
| 	// parameters:
 | ||||
| 	// - name: last_read_at
 | ||||
| 	//   in: query
 | ||||
| 	//   description: Describes the last point that notifications were checked. Anything updated since this time will not be updated.
 | ||||
| 	//   type: string
 | ||||
| 	//   format: date-time
 | ||||
| 	//   required: false
 | ||||
| 	// responses:
 | ||||
| 	//   "205":
 | ||||
| 	//     "$ref": "#/responses/empty"
 | ||||
| 
 | ||||
| 	lastRead := int64(0) | ||||
| 	qLastRead := strings.Trim(ctx.Query("last_read_at"), " ") | ||||
| 	if len(qLastRead) > 0 { | ||||
| 		tmpLastRead, err := time.Parse(time.RFC3339, qLastRead) | ||||
| 		if err != nil { | ||||
| 			ctx.InternalServerError(err) | ||||
| 			return | ||||
| 		} | ||||
| 		if !tmpLastRead.IsZero() { | ||||
| 			lastRead = tmpLastRead.Unix() | ||||
| 		} | ||||
| 	} | ||||
| 	opts := models.FindNotificationOptions{ | ||||
| 		UserID:            ctx.User.ID, | ||||
| 		UpdatedBeforeUnix: lastRead, | ||||
| 		Status:            models.NotificationStatusUnread, | ||||
| 	} | ||||
| 	nl, err := models.GetNotifications(opts) | ||||
| 	if err != nil { | ||||
| 		ctx.InternalServerError(err) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	for _, n := range nl { | ||||
| 		err := models.SetNotificationStatus(n.ID, ctx.User, models.NotificationStatusRead) | ||||
| 		if err != nil { | ||||
| 			ctx.InternalServerError(err) | ||||
| 			return | ||||
| 		} | ||||
| 		ctx.Status(http.StatusResetContent) | ||||
| 	} | ||||
| 
 | ||||
| 	ctx.Status(http.StatusResetContent) | ||||
| } | ||||
|  | @ -136,6 +136,8 @@ func GetPullRequest(ctx *context.APIContext) { | |||
| 	// responses:
 | ||||
| 	//   "200":
 | ||||
| 	//     "$ref": "#/responses/PullRequest"
 | ||||
| 	//   "404":
 | ||||
| 	//     "$ref": "#/responses/notFound"
 | ||||
| 
 | ||||
| 	pr, err := models.GetPullRequestByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) | ||||
| 	if err != nil { | ||||
|  |  | |||
							
								
								
									
										23
									
								
								routers/api/v1/swagger/notify.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								routers/api/v1/swagger/notify.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,23 @@ | |||
| // Copyright 2019 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 swagger | ||||
| 
 | ||||
| import ( | ||||
| 	api "code.gitea.io/gitea/modules/structs" | ||||
| ) | ||||
| 
 | ||||
| // NotificationThread
 | ||||
| // swagger:response NotificationThread
 | ||||
| type swaggerNotificationThread struct { | ||||
| 	// in:body
 | ||||
| 	Body api.NotificationThread `json:"body"` | ||||
| } | ||||
| 
 | ||||
| // NotificationThreadList
 | ||||
| // swagger:response NotificationThreadList
 | ||||
| type swaggerNotificationThreadList struct { | ||||
| 	// in:body
 | ||||
| 	Body []api.NotificationThread `json:"body"` | ||||
| } | ||||
|  | @ -425,6 +425,143 @@ | |||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "/notifications": { | ||||
|       "get": { | ||||
|         "consumes": [ | ||||
|           "application/json" | ||||
|         ], | ||||
|         "produces": [ | ||||
|           "application/json" | ||||
|         ], | ||||
|         "tags": [ | ||||
|           "notification" | ||||
|         ], | ||||
|         "summary": "List users's notification threads", | ||||
|         "operationId": "notifyGetList", | ||||
|         "parameters": [ | ||||
|           { | ||||
|             "type": "string", | ||||
|             "description": "If true, show notifications marked as read. Default value is false", | ||||
|             "name": "all", | ||||
|             "in": "query" | ||||
|           }, | ||||
|           { | ||||
|             "type": "string", | ||||
|             "format": "date-time", | ||||
|             "description": "Only show notifications updated after the given time. This is a timestamp in RFC 3339 format", | ||||
|             "name": "since", | ||||
|             "in": "query" | ||||
|           }, | ||||
|           { | ||||
|             "type": "string", | ||||
|             "format": "date-time", | ||||
|             "description": "Only show notifications updated before the given time. This is a timestamp in RFC 3339 format", | ||||
|             "name": "before", | ||||
|             "in": "query" | ||||
|           } | ||||
|         ], | ||||
|         "responses": { | ||||
|           "200": { | ||||
|             "$ref": "#/responses/NotificationThreadList" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       "put": { | ||||
|         "consumes": [ | ||||
|           "application/json" | ||||
|         ], | ||||
|         "produces": [ | ||||
|           "application/json" | ||||
|         ], | ||||
|         "tags": [ | ||||
|           "notification" | ||||
|         ], | ||||
|         "summary": "Mark notification threads as read", | ||||
|         "operationId": "notifyReadList", | ||||
|         "parameters": [ | ||||
|           { | ||||
|             "type": "string", | ||||
|             "format": "date-time", | ||||
|             "description": "Describes the last point that notifications were checked. Anything updated since this time will not be updated.", | ||||
|             "name": "last_read_at", | ||||
|             "in": "query" | ||||
|           } | ||||
|         ], | ||||
|         "responses": { | ||||
|           "205": { | ||||
|             "$ref": "#/responses/empty" | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "/notifications/threads/{id}": { | ||||
|       "get": { | ||||
|         "consumes": [ | ||||
|           "application/json" | ||||
|         ], | ||||
|         "produces": [ | ||||
|           "application/json" | ||||
|         ], | ||||
|         "tags": [ | ||||
|           "notification" | ||||
|         ], | ||||
|         "summary": "Get notification thread by ID", | ||||
|         "operationId": "notifyGetThread", | ||||
|         "parameters": [ | ||||
|           { | ||||
|             "type": "string", | ||||
|             "description": "id of notification thread", | ||||
|             "name": "id", | ||||
|             "in": "path", | ||||
|             "required": true | ||||
|           } | ||||
|         ], | ||||
|         "responses": { | ||||
|           "200": { | ||||
|             "$ref": "#/responses/NotificationThread" | ||||
|           }, | ||||
|           "403": { | ||||
|             "$ref": "#/responses/forbidden" | ||||
|           }, | ||||
|           "404": { | ||||
|             "$ref": "#/responses/notFound" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       "patch": { | ||||
|         "consumes": [ | ||||
|           "application/json" | ||||
|         ], | ||||
|         "produces": [ | ||||
|           "application/json" | ||||
|         ], | ||||
|         "tags": [ | ||||
|           "notification" | ||||
|         ], | ||||
|         "summary": "Mark notification thread as read by ID", | ||||
|         "operationId": "notifyReadThread", | ||||
|         "parameters": [ | ||||
|           { | ||||
|             "type": "string", | ||||
|             "description": "id of notification thread", | ||||
|             "name": "id", | ||||
|             "in": "path", | ||||
|             "required": true | ||||
|           } | ||||
|         ], | ||||
|         "responses": { | ||||
|           "205": { | ||||
|             "$ref": "#/responses/empty" | ||||
|           }, | ||||
|           "403": { | ||||
|             "$ref": "#/responses/forbidden" | ||||
|           }, | ||||
|           "404": { | ||||
|             "$ref": "#/responses/notFound" | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "/org/{org}/repos": { | ||||
|       "post": { | ||||
|         "consumes": [ | ||||
|  | @ -5231,6 +5368,103 @@ | |||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "/repos/{owner}/{repo}/notifications": { | ||||
|       "get": { | ||||
|         "consumes": [ | ||||
|           "application/json" | ||||
|         ], | ||||
|         "produces": [ | ||||
|           "application/json" | ||||
|         ], | ||||
|         "tags": [ | ||||
|           "notification" | ||||
|         ], | ||||
|         "summary": "List users's notification threads on a specific repo", | ||||
|         "operationId": "notifyGetRepoList", | ||||
|         "parameters": [ | ||||
|           { | ||||
|             "type": "string", | ||||
|             "description": "owner of the repo", | ||||
|             "name": "owner", | ||||
|             "in": "path", | ||||
|             "required": true | ||||
|           }, | ||||
|           { | ||||
|             "type": "string", | ||||
|             "description": "name of the repo", | ||||
|             "name": "repo", | ||||
|             "in": "path", | ||||
|             "required": true | ||||
|           }, | ||||
|           { | ||||
|             "type": "string", | ||||
|             "description": "If true, show notifications marked as read. Default value is false", | ||||
|             "name": "all", | ||||
|             "in": "query" | ||||
|           }, | ||||
|           { | ||||
|             "type": "string", | ||||
|             "format": "date-time", | ||||
|             "description": "Only show notifications updated after the given time. This is a timestamp in RFC 3339 format", | ||||
|             "name": "since", | ||||
|             "in": "query" | ||||
|           }, | ||||
|           { | ||||
|             "type": "string", | ||||
|             "format": "date-time", | ||||
|             "description": "Only show notifications updated before the given time. This is a timestamp in RFC 3339 format", | ||||
|             "name": "before", | ||||
|             "in": "query" | ||||
|           } | ||||
|         ], | ||||
|         "responses": { | ||||
|           "200": { | ||||
|             "$ref": "#/responses/NotificationThreadList" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       "put": { | ||||
|         "consumes": [ | ||||
|           "application/json" | ||||
|         ], | ||||
|         "produces": [ | ||||
|           "application/json" | ||||
|         ], | ||||
|         "tags": [ | ||||
|           "notification" | ||||
|         ], | ||||
|         "summary": "Mark notification threads as read on a specific repo", | ||||
|         "operationId": "notifyReadRepoList", | ||||
|         "parameters": [ | ||||
|           { | ||||
|             "type": "string", | ||||
|             "description": "owner of the repo", | ||||
|             "name": "owner", | ||||
|             "in": "path", | ||||
|             "required": true | ||||
|           }, | ||||
|           { | ||||
|             "type": "string", | ||||
|             "description": "name of the repo", | ||||
|             "name": "repo", | ||||
|             "in": "path", | ||||
|             "required": true | ||||
|           }, | ||||
|           { | ||||
|             "type": "string", | ||||
|             "format": "date-time", | ||||
|             "description": "Describes the last point that notifications were checked. Anything updated since this time will not be updated.", | ||||
|             "name": "last_read_at", | ||||
|             "in": "query" | ||||
|           } | ||||
|         ], | ||||
|         "responses": { | ||||
|           "205": { | ||||
|             "$ref": "#/responses/empty" | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "/repos/{owner}/{repo}/pulls": { | ||||
|       "get": { | ||||
|         "produces": [ | ||||
|  | @ -5397,6 +5631,9 @@ | |||
|         "responses": { | ||||
|           "200": { | ||||
|             "$ref": "#/responses/PullRequest" | ||||
|           }, | ||||
|           "404": { | ||||
|             "$ref": "#/responses/notFound" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|  | @ -10584,6 +10821,64 @@ | |||
|       }, | ||||
|       "x-go-package": "code.gitea.io/gitea/modules/structs" | ||||
|     }, | ||||
|     "NotificationSubject": { | ||||
|       "description": "NotificationSubject contains the notification subject (Issue/Pull/Commit)", | ||||
|       "type": "object", | ||||
|       "properties": { | ||||
|         "latest_comment_url": { | ||||
|           "type": "string", | ||||
|           "x-go-name": "LatestCommentURL" | ||||
|         }, | ||||
|         "title": { | ||||
|           "type": "string", | ||||
|           "x-go-name": "Title" | ||||
|         }, | ||||
|         "type": { | ||||
|           "type": "string", | ||||
|           "x-go-name": "Type" | ||||
|         }, | ||||
|         "url": { | ||||
|           "type": "string", | ||||
|           "x-go-name": "URL" | ||||
|         } | ||||
|       }, | ||||
|       "x-go-package": "code.gitea.io/gitea/modules/structs" | ||||
|     }, | ||||
|     "NotificationThread": { | ||||
|       "description": "NotificationThread expose Notification on API", | ||||
|       "type": "object", | ||||
|       "properties": { | ||||
|         "id": { | ||||
|           "type": "integer", | ||||
|           "format": "int64", | ||||
|           "x-go-name": "ID" | ||||
|         }, | ||||
|         "pinned": { | ||||
|           "type": "boolean", | ||||
|           "x-go-name": "Pinned" | ||||
|         }, | ||||
|         "repository": { | ||||
|           "$ref": "#/definitions/Repository" | ||||
|         }, | ||||
|         "subject": { | ||||
|           "$ref": "#/definitions/NotificationSubject" | ||||
|         }, | ||||
|         "unread": { | ||||
|           "type": "boolean", | ||||
|           "x-go-name": "Unread" | ||||
|         }, | ||||
|         "updated_at": { | ||||
|           "type": "string", | ||||
|           "format": "date-time", | ||||
|           "x-go-name": "UpdatedAt" | ||||
|         }, | ||||
|         "url": { | ||||
|           "type": "string", | ||||
|           "x-go-name": "URL" | ||||
|         } | ||||
|       }, | ||||
|       "x-go-package": "code.gitea.io/gitea/modules/structs" | ||||
|     }, | ||||
|     "Organization": { | ||||
|       "description": "Organization represents an organization", | ||||
|       "type": "object", | ||||
|  | @ -12012,6 +12307,21 @@ | |||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "NotificationThread": { | ||||
|       "description": "NotificationThread", | ||||
|       "schema": { | ||||
|         "$ref": "#/definitions/NotificationThread" | ||||
|       } | ||||
|     }, | ||||
|     "NotificationThreadList": { | ||||
|       "description": "NotificationThreadList", | ||||
|       "schema": { | ||||
|         "type": "array", | ||||
|         "items": { | ||||
|           "$ref": "#/definitions/NotificationThread" | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "Organization": { | ||||
|       "description": "Organization", | ||||
|       "schema": { | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue