* [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
tags/v1.21.12.1
| @@ -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) | |||
| } | |||
| } | |||
| @@ -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) | |||
| @@ -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) | |||
| } | |||
| @@ -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 | |||
| } | |||
| @@ -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 { | |||
| @@ -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": { | |||