From bb4261a5ed678235fadef279fe1ba1505993a406 Mon Sep 17 00:00:00 2001
From: 6543 <6543@obermui.de>
Date: Tue, 21 Apr 2020 15:48:53 +0200
Subject: [PATCH] Add issue subscription check to API (#10967)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

close #10962

Adds `GET /api/v1​/repos​/{owner}​/{repo}​/issues​/{index}​/subscriptions​/check`
 -> return a `WachInfo`
---
 integrations/api_issue_subscription_test.go | 66 +++++++++++++++++++++
 models/issue.go                             |  7 +++
 models/issue_watch.go                       | 17 ++++++
 routers/api/v1/api.go                       |  1 +
 routers/api/v1/repo/issue_subscription.go   | 59 ++++++++++++++++++
 routers/api/v1/user/watch.go                | 12 +---
 routers/repo/issue.go                       | 16 ++---
 templates/swagger/v1_json.tmpl              | 47 +++++++++++++++
 8 files changed, 205 insertions(+), 20 deletions(-)
 create mode 100644 integrations/api_issue_subscription_test.go

diff --git a/integrations/api_issue_subscription_test.go b/integrations/api_issue_subscription_test.go
new file mode 100644
index 0000000000..5d2956c4e0
--- /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 17ec0a6888..de7ac8c9f0 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 dea6aa5a52..9a2985fb69 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 225f6a5325..4b20c3e7c0 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 0406edd207..999dda1738 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 1fc736deba..1b55863034 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 a7fda4e769..7bce95c9c5 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 24a6330a06..3095e5c7f6 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": [