From 44de66bf50d1ab9a5acc298063cd942768092a19 Mon Sep 17 00:00:00 2001
From: 6543 <6543@obermui.de>
Date: Tue, 14 Jan 2020 16:37:19 +0100
Subject: [PATCH] [API] add endpoint to check notifications [Extend #9488]
 (#9595)

* introduce GET /notifications/new

* add TEST

* use Sprintf instead of path.Join

* Error more verbose

* return number of notifications if unreaded exist

* 200 http status for available notifications
---
 integrations/api_notification_test.go  |  8 ++++++
 models/issue.go                        |  3 +--
 models/issue_comment.go                |  3 +--
 models/notification.go                 | 17 ++++++++++++-
 modules/structs/notifications.go       |  5 ++++
 routers/api/v1/api.go                  |  1 +
 routers/api/v1/notify/notifications.go | 33 ++++++++++++++++++++++++
 routers/api/v1/swagger/notify.go       |  7 ++++++
 templates/swagger/v1_json.tmpl         | 35 ++++++++++++++++++++++++++
 9 files changed, 107 insertions(+), 5 deletions(-)
 create mode 100644 routers/api/v1/notify/notifications.go

diff --git a/integrations/api_notification_test.go b/integrations/api_notification_test.go
index 2c5477dfb0..baab00f6d2 100644
--- a/integrations/api_notification_test.go
+++ b/integrations/api_notification_test.go
@@ -81,6 +81,10 @@ func TestAPINotification(t *testing.T) {
 	assert.EqualValues(t, thread5.Issue.APIURL(), apiN.Subject.URL)
 	assert.EqualValues(t, thread5.Repository.HTMLURL(), apiN.Repository.HTMLURL)
 
+	// -- check notifications --
+	req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/notifications/new?token=%s", token))
+	resp = session.MakeRequest(t, req, http.StatusOK)
+
 	// -- mark notifications as read --
 	req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/notifications?token=%s", token))
 	resp = session.MakeRequest(t, req, http.StatusOK)
@@ -103,4 +107,8 @@ func TestAPINotification(t *testing.T) {
 	assert.Equal(t, models.NotificationStatusUnread, thread5.Status)
 	thread5 = models.AssertExistsAndLoadBean(t, &models.Notification{ID: 5}).(*models.Notification)
 	assert.Equal(t, models.NotificationStatusRead, thread5.Status)
+
+	// -- check notifications --
+	req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/notifications/new?token=%s", token))
+	resp = session.MakeRequest(t, req, http.StatusNoContent)
 }
diff --git a/models/issue.go b/models/issue.go
index 25765292ae..b6408365f7 100644
--- a/models/issue.go
+++ b/models/issue.go
@@ -7,7 +7,6 @@ package models
 
 import (
 	"fmt"
-	"path"
 	"regexp"
 	"sort"
 	"strconv"
@@ -324,7 +323,7 @@ func (issue *Issue) GetIsRead(userID int64) error {
 
 // APIURL returns the absolute APIURL to this issue.
 func (issue *Issue) APIURL() string {
-	return issue.Repo.APIURL() + "/" + path.Join("issues", fmt.Sprint(issue.Index))
+	return fmt.Sprintf("%s/issues/%d", issue.Repo.APIURL(), issue.Index)
 }
 
 // HTMLURL returns the absolute URL to this issue.
diff --git a/models/issue_comment.go b/models/issue_comment.go
index 8f54d9656a..699b8f0487 100644
--- a/models/issue_comment.go
+++ b/models/issue_comment.go
@@ -8,7 +8,6 @@ package models
 
 import (
 	"fmt"
-	"path"
 	"strings"
 
 	"code.gitea.io/gitea/modules/git"
@@ -249,7 +248,7 @@ func (c *Comment) APIURL() string {
 		return ""
 	}
 
-	return c.Issue.Repo.APIURL() + "/" + path.Join("issues/comments", fmt.Sprint(c.ID))
+	return fmt.Sprintf("%s/issues/comments/%d", c.Issue.Repo.APIURL(), c.ID)
 }
 
 // IssueURL formats a URL-string to the issue
diff --git a/models/notification.go b/models/notification.go
index 8e9bca0dc6..403c53243d 100644
--- a/models/notification.go
+++ b/models/notification.go
@@ -8,6 +8,7 @@ import (
 	"fmt"
 	"path"
 
+	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/timeutil"
@@ -294,6 +295,20 @@ func notificationsForUser(e Engine, user *User, statuses []NotificationStatus, p
 	return
 }
 
+// CountUnread count unread notifications for a user
+func CountUnread(user *User) int64 {
+	return countUnread(x, user.ID)
+}
+
+func countUnread(e Engine, userID int64) int64 {
+	exist, err := e.Where("user_id = ?", userID).And("status = ?", NotificationStatusUnread).Count(new(Notification))
+	if err != nil {
+		log.Error("countUnread", err)
+		return 0
+	}
+	return exist
+}
+
 // APIFormat converts a Notification to api.NotificationThread
 func (n *Notification) APIFormat() *api.NotificationThread {
 	result := &api.NotificationThread{
@@ -388,7 +403,7 @@ 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 fmt.Errorf("GetCommentByID [%d] for issue ID [%d]: %v", n.CommentID, n.IssueID, err)
 		}
 	}
 	return nil
diff --git a/modules/structs/notifications.go b/modules/structs/notifications.go
index b1e8b7781c..b6c9774a97 100644
--- a/modules/structs/notifications.go
+++ b/modules/structs/notifications.go
@@ -26,3 +26,8 @@ type NotificationSubject struct {
 	LatestCommentURL string `json:"latest_comment_url"`
 	Type             string `json:"type" binding:"In(Issue,Pull,Commit)"`
 }
+
+// NotificationCount number of unread notifications
+type NotificationCount struct {
+	New int64 `json:"new"`
+}
diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go
index 86c7450173..4c9f9dd03e 100644
--- a/routers/api/v1/api.go
+++ b/routers/api/v1/api.go
@@ -518,6 +518,7 @@ func RegisterRoutes(m *macaron.Macaron) {
 			m.Combo("").
 				Get(notify.ListNotifications).
 				Put(notify.ReadNotifications)
+			m.Get("/new", notify.NewAvailable)
 			m.Combo("/threads/:id").
 				Get(notify.GetThread).
 				Patch(notify.ReadThread)
diff --git a/routers/api/v1/notify/notifications.go b/routers/api/v1/notify/notifications.go
new file mode 100644
index 0000000000..847fe3313e
--- /dev/null
+++ b/routers/api/v1/notify/notifications.go
@@ -0,0 +1,33 @@
+// 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"
+
+	"code.gitea.io/gitea/models"
+	"code.gitea.io/gitea/modules/context"
+	api "code.gitea.io/gitea/modules/structs"
+)
+
+// NewAvailable check if unread notifications exist
+func NewAvailable(ctx *context.APIContext) {
+	// swagger:operation GET /notifications/new notification notifyNewAvailable
+	// ---
+	// summary: Check if unread notifications exist
+	// responses:
+	//   "200":
+	//    "$ref": "#/responses/NotificationCount"
+	//   "204":
+	//     description: No unread notification
+
+	count := models.CountUnread(ctx.User)
+
+	if count > 0 {
+		ctx.JSON(http.StatusOK, api.NotificationCount{New: count})
+	} else {
+		ctx.Status(http.StatusNoContent)
+	}
+}
diff --git a/routers/api/v1/swagger/notify.go b/routers/api/v1/swagger/notify.go
index 7d45da0e12..cd30d496e0 100644
--- a/routers/api/v1/swagger/notify.go
+++ b/routers/api/v1/swagger/notify.go
@@ -21,3 +21,10 @@ type swaggerNotificationThreadList struct {
 	// in:body
 	Body []api.NotificationThread `json:"body"`
 }
+
+// Number of unread notifications
+// swagger:response NotificationCount
+type swaggerNotificationCount struct {
+	// in:body
+	Body api.NotificationCount `json:"body"`
+}
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index a2baac1364..8ff4597b2e 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -494,6 +494,23 @@
         }
       }
     },
+    "/notifications/new": {
+      "get": {
+        "tags": [
+          "notification"
+        ],
+        "summary": "Check if unread notifications exist",
+        "operationId": "notifyNewAvailable",
+        "responses": {
+          "200": {
+            "$ref": "#/responses/NotificationCount"
+          },
+          "204": {
+            "description": "No unread notification"
+          }
+        }
+      }
+    },
     "/notifications/threads/{id}": {
       "get": {
         "consumes": [
@@ -10911,6 +10928,18 @@
       },
       "x-go-package": "code.gitea.io/gitea/modules/structs"
     },
+    "NotificationCount": {
+      "description": "NotificationCount number of unread notifications",
+      "type": "object",
+      "properties": {
+        "new": {
+          "type": "integer",
+          "format": "int64",
+          "x-go-name": "New"
+        }
+      },
+      "x-go-package": "code.gitea.io/gitea/modules/structs"
+    },
     "NotificationSubject": {
       "description": "NotificationSubject contains the notification subject (Issue/Pull/Commit)",
       "type": "object",
@@ -12397,6 +12426,12 @@
         }
       }
     },
+    "NotificationCount": {
+      "description": "Number of unread notifications",
+      "schema": {
+        "$ref": "#/definitions/NotificationCount"
+      }
+    },
     "NotificationThread": {
       "description": "NotificationThread",
       "schema": {