mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 11:41:32 +01:00 
			
		
		
		
	[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
					
				
							
								
								
									
										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 |   updated_by: 2 | ||||||
|   issue_id: 1 |   issue_id: 1 | ||||||
|   created_unix: 946684800 |   created_unix: 946684800 | ||||||
|   updated_unix: 946684800 |   updated_unix: 946684820 | ||||||
| 
 | 
 | ||||||
| - | - | ||||||
|   id: 2 |   id: 2 | ||||||
| @ -17,8 +17,8 @@ | |||||||
|   source: 1 # issue |   source: 1 # issue | ||||||
|   updated_by: 1 |   updated_by: 1 | ||||||
|   issue_id: 2 |   issue_id: 2 | ||||||
|   created_unix: 946684800 |   created_unix: 946685800 | ||||||
|   updated_unix: 946684800 |   updated_unix: 946685820 | ||||||
| 
 | 
 | ||||||
| - | - | ||||||
|   id: 3 |   id: 3 | ||||||
| @ -27,9 +27,9 @@ | |||||||
|   status: 3 # pinned |   status: 3 # pinned | ||||||
|   source: 1 # issue |   source: 1 # issue | ||||||
|   updated_by: 1 |   updated_by: 1 | ||||||
|   issue_id: 2 |   issue_id: 3 | ||||||
|   created_unix: 946684800 |   created_unix: 946686800 | ||||||
|   updated_unix: 946684800 |   updated_unix: 946686800 | ||||||
| 
 | 
 | ||||||
| - | - | ||||||
|   id: 4 |   id: 4 | ||||||
| @ -38,6 +38,17 @@ | |||||||
|   status: 1 # unread |   status: 1 # unread | ||||||
|   source: 1 # issue |   source: 1 # issue | ||||||
|   updated_by: 1 |   updated_by: 1 | ||||||
|   issue_id: 2 |   issue_id: 5 | ||||||
|   created_unix: 946684800 |   created_unix: 946687800 | ||||||
|   updated_unix: 946684800 |   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" | 	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. | // GetLastEventLabelFake returns the localization label for the current issue without providing a link in the username. | ||||||
| func (issue *Issue) GetLastEventLabelFake() string { | func (issue *Issue) GetLastEventLabelFake() string { | ||||||
| 	if issue.IsClosed { | 	if issue.IsClosed { | ||||||
|  | |||||||
| @ -8,6 +8,7 @@ package models | |||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"path" | ||||||
| 	"strings" | 	"strings" | ||||||
| 
 | 
 | ||||||
| 	"code.gitea.io/gitea/modules/git" | 	"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()) | 	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 | // IssueURL formats a URL-string to the issue | ||||||
| func (c *Comment) IssueURL() string { | func (c *Comment) IssueURL() string { | ||||||
| 	err := c.LoadIssue() | 	err := c.LoadIssue() | ||||||
|  | |||||||
| @ -6,8 +6,14 @@ package models | |||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"path" | ||||||
| 
 | 
 | ||||||
|  | 	"code.gitea.io/gitea/modules/setting" | ||||||
|  | 	api "code.gitea.io/gitea/modules/structs" | ||||||
| 	"code.gitea.io/gitea/modules/timeutil" | 	"code.gitea.io/gitea/modules/timeutil" | ||||||
|  | 
 | ||||||
|  | 	"xorm.io/builder" | ||||||
|  | 	"xorm.io/xorm" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| type ( | type ( | ||||||
| @ -47,17 +53,67 @@ type Notification struct { | |||||||
| 	IssueID   int64  `xorm:"INDEX NOT NULL"` | 	IssueID   int64  `xorm:"INDEX NOT NULL"` | ||||||
| 	CommitID  string `xorm:"INDEX"` | 	CommitID  string `xorm:"INDEX"` | ||||||
| 	CommentID int64 | 	CommentID int64 | ||||||
| 	Comment   *Comment `xorm:"-"` |  | ||||||
| 
 | 
 | ||||||
| 	UpdatedBy int64 `xorm:"INDEX NOT NULL"` | 	UpdatedBy int64 `xorm:"INDEX NOT NULL"` | ||||||
| 
 | 
 | ||||||
| 	Issue      *Issue      `xorm:"-"` | 	Issue      *Issue      `xorm:"-"` | ||||||
| 	Repository *Repository `xorm:"-"` | 	Repository *Repository `xorm:"-"` | ||||||
|  | 	Comment    *Comment    `xorm:"-"` | ||||||
|  | 	User       *User       `xorm:"-"` | ||||||
| 
 | 
 | ||||||
| 	CreatedUnix timeutil.TimeStamp `xorm:"created INDEX NOT NULL"` | 	CreatedUnix timeutil.TimeStamp `xorm:"created INDEX NOT NULL"` | ||||||
| 	UpdatedUnix timeutil.TimeStamp `xorm:"updated 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 | // CreateOrUpdateIssueNotifications creates an issue notification | ||||||
| // for each watcher, or updates it if already exists | // for each watcher, or updates it if already exists | ||||||
| func CreateOrUpdateIssueNotifications(issueID, commentID int64, notificationAuthorID int64) error { | func CreateOrUpdateIssueNotifications(issueID, commentID int64, notificationAuthorID int64) error { | ||||||
| @ -238,22 +294,124 @@ func notificationsForUser(e Engine, user *User, statuses []NotificationStatus, p | |||||||
| 	return | 	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 | // GetRepo returns the repo of the notification | ||||||
| func (n *Notification) GetRepo() (*Repository, error) { | func (n *Notification) GetRepo() (*Repository, error) { | ||||||
| 	n.Repository = new(Repository) | 	return n.Repository, n.loadRepo(x) | ||||||
| 	_, err := x. |  | ||||||
| 		Where("id = ?", n.RepoID). |  | ||||||
| 		Get(n.Repository) |  | ||||||
| 	return n.Repository, err |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // GetIssue returns the issue of the notification | // GetIssue returns the issue of the notification | ||||||
| func (n *Notification) GetIssue() (*Issue, error) { | func (n *Notification) GetIssue() (*Issue, error) { | ||||||
| 	n.Issue = new(Issue) | 	return n.Issue, n.loadIssue(x) | ||||||
| 	_, err := x. |  | ||||||
| 		Where("id = ?", n.IssueID). |  | ||||||
| 		Get(n.Issue) |  | ||||||
| 	return n.Issue, err |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // HTMLURL formats a URL-string to the notification | // HTMLURL formats a URL-string to the notification | ||||||
| @ -264,9 +422,34 @@ func (n *Notification) HTMLURL() string { | |||||||
| 	return n.Issue.HTMLURL() | 	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 | // NotificationList contains a list of notifications | ||||||
| type NotificationList []*Notification | 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 { | func (nl NotificationList) getPendingRepoIDs() []int64 { | ||||||
| 	var ids = make(map[int64]struct{}, len(nl)) | 	var ids = make(map[int64]struct{}, len(nl)) | ||||||
| 	for _, notification := range nl { | 	for _, notification := range nl { | ||||||
| @ -486,7 +669,7 @@ func setNotificationStatusReadIfUnread(e Engine, userID, issueID int64) error { | |||||||
| 
 | 
 | ||||||
| // SetNotificationStatus change the notification status | // SetNotificationStatus change the notification status | ||||||
| func SetNotificationStatus(notificationID int64, user *User, status NotificationStatus) error { | func SetNotificationStatus(notificationID int64, user *User, status NotificationStatus) error { | ||||||
| 	notification, err := getNotificationByID(notificationID) | 	notification, err := getNotificationByID(x, notificationID) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| @ -501,9 +684,14 @@ func SetNotificationStatus(notificationID int64, user *User, status Notification | |||||||
| 	return err | 	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) | 	notification := new(Notification) | ||||||
| 	ok, err := x. | 	ok, err := e. | ||||||
| 		Where("id = ?", notificationID). | 		Where("id = ?", notificationID). | ||||||
| 		Get(notification) | 		Get(notification) | ||||||
| 
 | 
 | ||||||
| @ -512,7 +700,7 @@ func getNotificationByID(notificationID int64) (*Notification, error) { | |||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if !ok { | 	if !ok { | ||||||
| 		return nil, fmt.Errorf("Notification %d does not exists", notificationID) | 		return nil, ErrNotExist{ID: notificationID} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return notification, nil | 	return notification, nil | ||||||
|  | |||||||
| @ -31,11 +31,13 @@ func TestNotificationsForUser(t *testing.T) { | |||||||
| 	statuses := []NotificationStatus{NotificationStatusRead, NotificationStatusUnread} | 	statuses := []NotificationStatus{NotificationStatusRead, NotificationStatusUnread} | ||||||
| 	notfs, err := NotificationsForUser(user, statuses, 1, 10) | 	notfs, err := NotificationsForUser(user, statuses, 1, 10) | ||||||
| 	assert.NoError(t, err) | 	assert.NoError(t, err) | ||||||
| 	if assert.Len(t, notfs, 2) { | 	if assert.Len(t, notfs, 3) { | ||||||
| 		assert.EqualValues(t, 2, notfs[0].ID) | 		assert.EqualValues(t, 5, notfs[0].ID) | ||||||
| 		assert.EqualValues(t, user.ID, notfs[0].UserID) | 		assert.EqualValues(t, user.ID, notfs[0].UserID) | ||||||
| 		assert.EqualValues(t, 4, notfs[1].ID) | 		assert.EqualValues(t, 4, notfs[1].ID) | ||||||
| 		assert.EqualValues(t, user.ID, notfs[1].UserID) | 		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: | 	// responses: | ||||||
| 	//   "201": | 	//   "201": | ||||||
| 	//     "$ref": "#/responses/User" | 	//     "$ref": "#/responses/User" | ||||||
| 	//   "403": |  | ||||||
| 	//     "$ref": "#/responses/forbidden" |  | ||||||
| 	//   "400": | 	//   "400": | ||||||
| 	//     "$ref": "#/responses/error" | 	//     "$ref": "#/responses/error" | ||||||
|  | 	//   "403": | ||||||
|  | 	//     "$ref": "#/responses/forbidden" | ||||||
| 	//   "422": | 	//   "422": | ||||||
| 	//     "$ref": "#/responses/validationError" | 	//     "$ref": "#/responses/validationError" | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -70,6 +70,7 @@ import ( | |||||||
| 	api "code.gitea.io/gitea/modules/structs" | 	api "code.gitea.io/gitea/modules/structs" | ||||||
| 	"code.gitea.io/gitea/routers/api/v1/admin" | 	"code.gitea.io/gitea/routers/api/v1/admin" | ||||||
| 	"code.gitea.io/gitea/routers/api/v1/misc" | 	"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/org" | ||||||
| 	"code.gitea.io/gitea/routers/api/v1/repo" | 	"code.gitea.io/gitea/routers/api/v1/repo" | ||||||
| 	_ "code.gitea.io/gitea/routers/api/v1/swagger" // for swagger generation | 	_ "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", bind(api.MarkdownOption{}), misc.Markdown) | ||||||
| 		m.Post("/markdown/raw", misc.MarkdownRaw) | 		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 | 		// Users | ||||||
| 		m.Group("/users", func() { | 		m.Group("/users", func() { | ||||||
| 			m.Get("/search", user.Search) | 			m.Get("/search", user.Search) | ||||||
| @ -610,6 +621,9 @@ func RegisterRoutes(m *macaron.Macaron) { | |||||||
| 				m.Combo("").Get(reqAnyRepoReader(), repo.Get). | 				m.Combo("").Get(reqAnyRepoReader(), repo.Get). | ||||||
| 					Delete(reqToken(), reqOwner(), repo.Delete). | 					Delete(reqToken(), reqOwner(), repo.Delete). | ||||||
| 					Patch(reqToken(), reqAdmin(), bind(api.EditRepoOption{}), context.RepoRef(), repo.Edit) | 					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.Group("/hooks", func() { | ||||||
| 					m.Combo("").Get(repo.ListHooks). | 					m.Combo("").Get(repo.ListHooks). | ||||||
| 						Post(bind(api.CreateHookOption{}), repo.CreateHook) | 						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: | 	// responses: | ||||||
| 	//   "200": | 	//   "200": | ||||||
| 	//     "$ref": "#/responses/PullRequest" | 	//     "$ref": "#/responses/PullRequest" | ||||||
|  | 	//   "404": | ||||||
|  | 	//     "$ref": "#/responses/notFound" | ||||||
| 
 | 
 | ||||||
| 	pr, err := models.GetPullRequestByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) | 	pr, err := models.GetPullRequestByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) | ||||||
| 	if err != nil { | 	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": { |     "/org/{org}/repos": { | ||||||
|       "post": { |       "post": { | ||||||
|         "consumes": [ |         "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": { |     "/repos/{owner}/{repo}/pulls": { | ||||||
|       "get": { |       "get": { | ||||||
|         "produces": [ |         "produces": [ | ||||||
| @ -5397,6 +5631,9 @@ | |||||||
|         "responses": { |         "responses": { | ||||||
|           "200": { |           "200": { | ||||||
|             "$ref": "#/responses/PullRequest" |             "$ref": "#/responses/PullRequest" | ||||||
|  |           }, | ||||||
|  |           "404": { | ||||||
|  |             "$ref": "#/responses/notFound" | ||||||
|           } |           } | ||||||
|         } |         } | ||||||
|       }, |       }, | ||||||
| @ -10584,6 +10821,64 @@ | |||||||
|       }, |       }, | ||||||
|       "x-go-package": "code.gitea.io/gitea/modules/structs" |       "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": { |     "Organization": { | ||||||
|       "description": "Organization represents an organization", |       "description": "Organization represents an organization", | ||||||
|       "type": "object", |       "type": "object", | ||||||
| @ -12012,6 +12307,21 @@ | |||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "NotificationThread": { | ||||||
|  |       "description": "NotificationThread", | ||||||
|  |       "schema": { | ||||||
|  |         "$ref": "#/definitions/NotificationThread" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     "NotificationThreadList": { | ||||||
|  |       "description": "NotificationThreadList", | ||||||
|  |       "schema": { | ||||||
|  |         "type": "array", | ||||||
|  |         "items": { | ||||||
|  |           "$ref": "#/definitions/NotificationThread" | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "Organization": { |     "Organization": { | ||||||
|       "description": "Organization", |       "description": "Organization", | ||||||
|       "schema": { |       "schema": { | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user