[API] Add notification endpoint (#9488)
* [API] Add notification endpoints * add func GetNotifications(opts FindNotificationOptions) * add func (n *Notification) APIFormat() * add func (nl NotificationList) APIFormat() * add func (n *Notification) APIURL() * add func (nl NotificationList) APIFormat() * add LoadAttributes functions (loadRepo, loadIssue, loadComment, loadUser) * add func (c *Comment) APIURL() * add func (issue *Issue) GetLastComment() * add endpoint GET /notifications * add endpoint PUT /notifications * add endpoint GET /repos/{owner}/{repo}/notifications * add endpoint PUT /repos/{owner}/{repo}/notifications * add endpoint GET /notifications/threads/{id} * add endpoint PATCH /notifications/threads/{id} * Add TEST * code format * code format
This commit is contained in:
parent
ee9ce0cfa9
commit
6baa5d7588
15 changed files with 1124 additions and 28 deletions
106
integrations/api_notification_test.go
Normal file
106
integrations/api_notification_test.go
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package integrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models"
|
||||||
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAPINotification(t *testing.T) {
|
||||||
|
defer prepareTestEnv(t)()
|
||||||
|
|
||||||
|
user2 := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User)
|
||||||
|
repo1 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository)
|
||||||
|
thread5 := models.AssertExistsAndLoadBean(t, &models.Notification{ID: 5}).(*models.Notification)
|
||||||
|
assert.NoError(t, thread5.LoadAttributes())
|
||||||
|
session := loginUser(t, user2.Name)
|
||||||
|
token := getTokenForLoggedInUser(t, session)
|
||||||
|
|
||||||
|
// -- GET /notifications --
|
||||||
|
// test filter
|
||||||
|
since := "2000-01-01T00%3A50%3A01%2B00%3A00" //946687801
|
||||||
|
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/notifications?since=%s&token=%s", since, token))
|
||||||
|
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
var apiNL []api.NotificationThread
|
||||||
|
DecodeJSON(t, resp, &apiNL)
|
||||||
|
|
||||||
|
assert.Len(t, apiNL, 1)
|
||||||
|
assert.EqualValues(t, 5, apiNL[0].ID)
|
||||||
|
|
||||||
|
// test filter
|
||||||
|
before := "2000-01-01T01%3A06%3A59%2B00%3A00" //946688819
|
||||||
|
|
||||||
|
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/notifications?all=%s&before=%s&token=%s", "true", before, token))
|
||||||
|
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
DecodeJSON(t, resp, &apiNL)
|
||||||
|
|
||||||
|
assert.Len(t, apiNL, 3)
|
||||||
|
assert.EqualValues(t, 4, apiNL[0].ID)
|
||||||
|
assert.EqualValues(t, true, apiNL[0].Unread)
|
||||||
|
assert.EqualValues(t, false, apiNL[0].Pinned)
|
||||||
|
assert.EqualValues(t, 3, apiNL[1].ID)
|
||||||
|
assert.EqualValues(t, false, apiNL[1].Unread)
|
||||||
|
assert.EqualValues(t, true, apiNL[1].Pinned)
|
||||||
|
assert.EqualValues(t, 2, apiNL[2].ID)
|
||||||
|
assert.EqualValues(t, false, apiNL[2].Unread)
|
||||||
|
assert.EqualValues(t, false, apiNL[2].Pinned)
|
||||||
|
|
||||||
|
// -- GET /repos/{owner}/{repo}/notifications --
|
||||||
|
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/notifications?token=%s", user2.Name, repo1.Name, token))
|
||||||
|
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
DecodeJSON(t, resp, &apiNL)
|
||||||
|
|
||||||
|
assert.Len(t, apiNL, 1)
|
||||||
|
assert.EqualValues(t, 4, apiNL[0].ID)
|
||||||
|
|
||||||
|
// -- GET /notifications/threads/{id} --
|
||||||
|
// get forbidden
|
||||||
|
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/notifications/threads/%d?token=%s", 1, token))
|
||||||
|
resp = session.MakeRequest(t, req, http.StatusForbidden)
|
||||||
|
|
||||||
|
// get own
|
||||||
|
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/notifications/threads/%d?token=%s", thread5.ID, token))
|
||||||
|
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
var apiN api.NotificationThread
|
||||||
|
DecodeJSON(t, resp, &apiN)
|
||||||
|
|
||||||
|
assert.EqualValues(t, 5, apiN.ID)
|
||||||
|
assert.EqualValues(t, false, apiN.Pinned)
|
||||||
|
assert.EqualValues(t, true, apiN.Unread)
|
||||||
|
assert.EqualValues(t, "issue4", apiN.Subject.Title)
|
||||||
|
assert.EqualValues(t, "Issue", apiN.Subject.Type)
|
||||||
|
assert.EqualValues(t, thread5.Issue.APIURL(), apiN.Subject.URL)
|
||||||
|
assert.EqualValues(t, thread5.Repository.HTMLURL(), apiN.Repository.HTMLURL)
|
||||||
|
|
||||||
|
// -- mark notifications as read --
|
||||||
|
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/notifications?token=%s", token))
|
||||||
|
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
DecodeJSON(t, resp, &apiNL)
|
||||||
|
assert.Len(t, apiNL, 2)
|
||||||
|
|
||||||
|
lastReadAt := "2000-01-01T00%3A50%3A01%2B00%3A00" //946687801 <- only Notification 4 is in this filter ...
|
||||||
|
req = NewRequest(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%s/notifications?last_read_at=%s&token=%s", user2.Name, repo1.Name, lastReadAt, token))
|
||||||
|
resp = session.MakeRequest(t, req, http.StatusResetContent)
|
||||||
|
|
||||||
|
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/notifications?token=%s", token))
|
||||||
|
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
DecodeJSON(t, resp, &apiNL)
|
||||||
|
assert.Len(t, apiNL, 1)
|
||||||
|
|
||||||
|
// -- PATCH /notifications/threads/{id} --
|
||||||
|
req = NewRequest(t, "PATCH", fmt.Sprintf("/api/v1/notifications/threads/%d?token=%s", thread5.ID, token))
|
||||||
|
resp = session.MakeRequest(t, req, http.StatusResetContent)
|
||||||
|
|
||||||
|
assert.Equal(t, models.NotificationStatusUnread, thread5.Status)
|
||||||
|
thread5 = models.AssertExistsAndLoadBean(t, &models.Notification{ID: 5}).(*models.Notification)
|
||||||
|
assert.Equal(t, models.NotificationStatusRead, thread5.Status)
|
||||||
|
}
|
|
@ -7,7 +7,7 @@
|
||||||
updated_by: 2
|
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": {
|
||||||
|
|
Reference in a new issue