diff --git a/integrations/api_issue_subscription_test.go b/integrations/api_issue_subscription_test.go new file mode 100644 index 000000000..5d2956c4e --- /dev/null +++ b/integrations/api_issue_subscription_test.go @@ -0,0 +1,66 @@ +// 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 TestAPIIssueSubscriptions(t *testing.T) { + defer prepareTestEnv(t)() + + issue1 := models.AssertExistsAndLoadBean(t, &models.Issue{ID: 1}).(*models.Issue) + issue2 := models.AssertExistsAndLoadBean(t, &models.Issue{ID: 2}).(*models.Issue) + issue3 := models.AssertExistsAndLoadBean(t, &models.Issue{ID: 3}).(*models.Issue) + issue4 := models.AssertExistsAndLoadBean(t, &models.Issue{ID: 4}).(*models.Issue) + issue5 := models.AssertExistsAndLoadBean(t, &models.Issue{ID: 8}).(*models.Issue) + + owner := models.AssertExistsAndLoadBean(t, &models.User{ID: issue1.PosterID}).(*models.User) + + session := loginUser(t, owner.Name) + token := getTokenForLoggedInUser(t, session) + + testSubscription := func(issue *models.Issue, isWatching bool) { + + issueRepo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: issue.RepoID}).(*models.Repository) + + urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/subscriptions/check?token=%s", issueRepo.OwnerName, issueRepo.Name, issue.Index, token) + req := NewRequest(t, "GET", urlStr) + resp := session.MakeRequest(t, req, http.StatusOK) + wi := new(api.WatchInfo) + DecodeJSON(t, resp, wi) + + assert.EqualValues(t, isWatching, wi.Subscribed) + assert.EqualValues(t, !isWatching, wi.Ignored) + assert.EqualValues(t, issue.APIURL()+"/subscriptions", wi.URL) + assert.EqualValues(t, issue.CreatedUnix, wi.CreatedAt.Unix()) + assert.EqualValues(t, issueRepo.APIURL(), wi.RepositoryURL) + } + + testSubscription(issue1, true) + testSubscription(issue2, true) + testSubscription(issue3, true) + testSubscription(issue4, false) + testSubscription(issue5, false) + + issue1Repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: issue1.RepoID}).(*models.Repository) + urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/subscriptions/%s?token=%s", issue1Repo.OwnerName, issue1Repo.Name, issue1.Index, owner.Name, token) + req := NewRequest(t, "DELETE", urlStr) + session.MakeRequest(t, req, http.StatusCreated) + testSubscription(issue1, false) + + issue5Repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: issue5.RepoID}).(*models.Repository) + urlStr = fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/subscriptions/%s?token=%s", issue5Repo.OwnerName, issue5Repo.Name, issue5.Index, owner.Name, token) + req = NewRequest(t, "PUT", urlStr) + session.MakeRequest(t, req, http.StatusCreated) + testSubscription(issue5, true) +} diff --git a/models/issue.go b/models/issue.go index 17ec0a688..de7ac8c9f 100644 --- a/models/issue.go +++ b/models/issue.go @@ -332,6 +332,13 @@ func (issue *Issue) GetIsRead(userID int64) error { // APIURL returns the absolute APIURL to this issue. func (issue *Issue) APIURL() string { + if issue.Repo == nil { + err := issue.LoadRepo() + if err != nil { + log.Error("Issue[%d].APIURL(): %v", issue.ID, err) + return "" + } + } return fmt.Sprintf("%s/issues/%d", issue.Repo.APIURL(), issue.Index) } diff --git a/models/issue_watch.go b/models/issue_watch.go index dea6aa5a5..9a2985fb6 100644 --- a/models/issue_watch.go +++ b/models/issue_watch.go @@ -64,6 +64,23 @@ func getIssueWatch(e Engine, userID, issueID int64) (iw *IssueWatch, exists bool return } +// CheckIssueWatch check if an user is watching an issue +// it takes participants and repo watch into account +func CheckIssueWatch(user *User, issue *Issue) (bool, error) { + iw, exist, err := getIssueWatch(x, user.ID, issue.ID) + if err != nil { + return false, err + } + if exist { + return iw.IsWatching, nil + } + w, err := getWatch(x, user.ID, issue.RepoID) + if err != nil { + return false, err + } + return isWatchMode(w.Mode) || IsUserParticipantsOfIssue(user, issue), nil +} + // GetIssueWatchersIDs returns IDs of subscribers or explicit unsubscribers to a given issue id // but avoids joining with `user` for performance reasons // User permissions must be verified elsewhere if required diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 225f6a532..4b20c3e7c 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -735,6 +735,7 @@ func RegisterRoutes(m *macaron.Macaron) { }) m.Group("/subscriptions", func() { m.Get("", repo.GetIssueSubscribers) + m.Get("/check", reqToken(), repo.CheckIssueSubscription) m.Put("/:user", reqToken(), repo.AddIssueSubscription) m.Delete("/:user", reqToken(), repo.DelIssueSubscription) }) diff --git a/routers/api/v1/repo/issue_subscription.go b/routers/api/v1/repo/issue_subscription.go index 0406edd20..999dda173 100644 --- a/routers/api/v1/repo/issue_subscription.go +++ b/routers/api/v1/repo/issue_subscription.go @@ -9,6 +9,7 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/context" + api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/routers/api/v1/utils" ) @@ -133,6 +134,64 @@ func setIssueSubscription(ctx *context.APIContext, watch bool) { ctx.Status(http.StatusCreated) } +// CheckIssueSubscription check if user is subscribed to an issue +func CheckIssueSubscription(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/issues/{index}/subscriptions/check issue issueCheckSubscription + // --- + // summary: Check if user is subscribed to an issue + // 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: index + // in: path + // description: index of the issue + // type: integer + // format: int64 + // required: true + // responses: + // "200": + // "$ref": "#/responses/WatchInfo" + // "404": + // "$ref": "#/responses/notFound" + + issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) + if err != nil { + if models.IsErrIssueNotExist(err) { + ctx.NotFound() + } else { + ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err) + } + + return + } + + watching, err := models.CheckIssueWatch(ctx.User, issue) + if err != nil { + ctx.InternalServerError(err) + return + } + ctx.JSON(http.StatusOK, api.WatchInfo{ + Subscribed: watching, + Ignored: !watching, + Reason: nil, + CreatedAt: issue.CreatedUnix.AsTime(), + URL: issue.APIURL() + "/subscriptions", + RepositoryURL: ctx.Repo.Repository.APIURL(), + }) +} + // GetIssueSubscribers return subscribers of an issue func GetIssueSubscribers(ctx *context.APIContext) { // swagger:operation GET /repos/{owner}/{repo}/issues/{index}/subscriptions issue issueSubscriptions diff --git a/routers/api/v1/user/watch.go b/routers/api/v1/user/watch.go index 1fc736deb..1b5586303 100644 --- a/routers/api/v1/user/watch.go +++ b/routers/api/v1/user/watch.go @@ -9,7 +9,6 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/context" - "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/routers/api/v1/utils" ) @@ -124,7 +123,7 @@ func IsWatching(ctx *context.APIContext) { Reason: nil, CreatedAt: ctx.Repo.Repository.CreatedUnix.AsTime(), URL: subscriptionURL(ctx.Repo.Repository), - RepositoryURL: repositoryURL(ctx.Repo.Repository), + RepositoryURL: ctx.Repo.Repository.APIURL(), }) } else { ctx.NotFound() @@ -162,7 +161,7 @@ func Watch(ctx *context.APIContext) { Reason: nil, CreatedAt: ctx.Repo.Repository.CreatedUnix.AsTime(), URL: subscriptionURL(ctx.Repo.Repository), - RepositoryURL: repositoryURL(ctx.Repo.Repository), + RepositoryURL: ctx.Repo.Repository.APIURL(), }) } @@ -197,10 +196,5 @@ func Unwatch(ctx *context.APIContext) { // subscriptionURL returns the URL of the subscription API endpoint of a repo func subscriptionURL(repo *models.Repository) string { - return repositoryURL(repo) + "/subscription" -} - -// repositoryURL returns the URL of the API endpoint of a repo -func repositoryURL(repo *models.Repository) string { - return setting.AppURL + "api/v1/" + repo.FullName() + return repo.APIURL() + "/subscription" } diff --git a/routers/repo/issue.go b/routers/repo/issue.go index a7fda4e76..7bce95c9c 100644 --- a/routers/repo/issue.go +++ b/routers/repo/issue.go @@ -749,21 +749,15 @@ func ViewIssue(ctx *context.Context) { ctx.Data["Title"] = fmt.Sprintf("#%d - %s", issue.Index, issue.Title) - var iw *models.IssueWatch - var exists bool + iw := new(models.IssueWatch) if ctx.User != nil { - iw, exists, err = models.GetIssueWatch(ctx.User.ID, issue.ID) + iw.UserID = ctx.User.ID + iw.IssueID = issue.ID + iw.IsWatching, err = models.CheckIssueWatch(ctx.User, issue) if err != nil { - ctx.ServerError("GetIssueWatch", err) + ctx.InternalServerError(err) return } - if !exists { - iw = &models.IssueWatch{ - UserID: ctx.User.ID, - IssueID: issue.ID, - IsWatching: models.IsWatching(ctx.User.ID, ctx.Repo.Repository.ID) || models.IsUserParticipantsOfIssue(ctx.User, issue), - } - } } ctx.Data["IssueWatch"] = iw diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 24a6330a0..3095e5c7f 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -5217,6 +5217,53 @@ } } }, + "/repos/{owner}/{repo}/issues/{index}/subscriptions/check": { + "get": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "issue" + ], + "summary": "Check if user is subscribed to an issue", + "operationId": "issueCheckSubscription", + "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": "integer", + "format": "int64", + "description": "index of the issue", + "name": "index", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/WatchInfo" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, "/repos/{owner}/{repo}/issues/{index}/subscriptions/{user}": { "put": { "consumes": [