From 92736010646d4a5cfd3430c6f57bbf4cbd8951da Mon Sep 17 00:00:00 2001
From: 6543 <6543@obermui.de>
Date: Wed, 16 Jun 2021 19:04:37 +0200
Subject: [PATCH] Add subject-type filter to list notification API endpoints
 (#16177)

Close #15886
---
 models/notification.go                 |  8 +++--
 routers/api/v1/notify/notifications.go | 43 ++++++++++++++++++++++++++
 routers/api/v1/notify/repo.go          | 33 +++++++-------------
 routers/api/v1/notify/user.go          | 32 +++++++------------
 templates/swagger/v1_json.tmpl         | 32 +++++++++++++++++++
 5 files changed, 105 insertions(+), 43 deletions(-)

diff --git a/models/notification.go b/models/notification.go
index dcb032207..56abd0ed8 100644
--- a/models/notification.go
+++ b/models/notification.go
@@ -74,6 +74,7 @@ type FindNotificationOptions struct {
 	RepoID            int64
 	IssueID           int64
 	Status            []NotificationStatus
+	Source            []NotificationSource
 	UpdatedAfterUnix  int64
 	UpdatedBeforeUnix int64
 }
@@ -93,6 +94,9 @@ func (opts *FindNotificationOptions) ToCond() builder.Cond {
 	if len(opts.Status) > 0 {
 		cond = cond.And(builder.In("notification.status", opts.Status))
 	}
+	if len(opts.Source) > 0 {
+		cond = cond.And(builder.In("notification.source", opts.Source))
+	}
 	if opts.UpdatedAfterUnix != 0 {
 		cond = cond.And(builder.Gte{"notification.updated_unix": opts.UpdatedAfterUnix})
 	}
@@ -111,13 +115,13 @@ func (opts *FindNotificationOptions) ToSession(e Engine) *xorm.Session {
 	return sess
 }
 
-func getNotifications(e Engine, options FindNotificationOptions) (nl NotificationList, err error) {
+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) {
+func GetNotifications(opts *FindNotificationOptions) (NotificationList, error) {
 	return getNotifications(x, opts)
 }
 
diff --git a/routers/api/v1/notify/notifications.go b/routers/api/v1/notify/notifications.go
index 71dd7d949..a5e095a3b 100644
--- a/routers/api/v1/notify/notifications.go
+++ b/routers/api/v1/notify/notifications.go
@@ -6,10 +6,12 @@ package notify
 
 import (
 	"net/http"
+	"strings"
 
 	"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"
 )
 
 // NewAvailable check if unread notifications exist
@@ -22,3 +24,44 @@ func NewAvailable(ctx *context.APIContext) {
 	//     "$ref": "#/responses/NotificationCount"
 	ctx.JSON(http.StatusOK, api.NotificationCount{New: models.CountUnread(ctx.User)})
 }
+
+func getFindNotificationOptions(ctx *context.APIContext) *models.FindNotificationOptions {
+	before, since, err := utils.GetQueryBeforeSince(ctx)
+	if err != nil {
+		ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err)
+		return nil
+	}
+	opts := &models.FindNotificationOptions{
+		ListOptions:       utils.GetListOptions(ctx),
+		UserID:            ctx.User.ID,
+		UpdatedBeforeUnix: before,
+		UpdatedAfterUnix:  since,
+	}
+	if !ctx.QueryBool("all") {
+		statuses := ctx.QueryStrings("status-types")
+		opts.Status = statusStringsToNotificationStatuses(statuses, []string{"unread", "pinned"})
+	}
+
+	subjectTypes := ctx.QueryStrings("subject-type")
+	if len(subjectTypes) != 0 {
+		opts.Source = subjectToSource(subjectTypes)
+	}
+
+	return opts
+}
+
+func subjectToSource(value []string) (result []models.NotificationSource) {
+	for _, v := range value {
+		switch strings.ToLower(v) {
+		case "issue":
+			result = append(result, models.NotificationSourceIssue)
+		case "pull":
+			result = append(result, models.NotificationSourcePullRequest)
+		case "commit":
+			result = append(result, models.NotificationSourceCommit)
+		case "repository":
+			result = append(result, models.NotificationSourceRepository)
+		}
+	}
+	return
+}
diff --git a/routers/api/v1/notify/repo.go b/routers/api/v1/notify/repo.go
index 0a75fcd30..4deb16a22 100644
--- a/routers/api/v1/notify/repo.go
+++ b/routers/api/v1/notify/repo.go
@@ -13,7 +13,6 @@ import (
 	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/convert"
 	"code.gitea.io/gitea/modules/log"
-	"code.gitea.io/gitea/routers/api/v1/utils"
 )
 
 func statusStringToNotificationStatus(status string) models.NotificationStatus {
@@ -67,7 +66,6 @@ func ListRepoNotifications(ctx *context.APIContext) {
 	//   in: query
 	//   description: If true, show notifications marked as read. Default value is false
 	//   type: string
-	//   required: false
 	// - name: status-types
 	//   in: query
 	//   description: "Show notifications with the provided status types. Options are: unread, read and/or pinned. Defaults to unread & pinned"
@@ -75,19 +73,24 @@ func ListRepoNotifications(ctx *context.APIContext) {
 	//   collectionFormat: multi
 	//   items:
 	//     type: string
-	//   required: false
+	// - name: subject-type
+	//   in: query
+	//   description: "filter notifications by subject type"
+	//   type: array
+	//   collectionFormat: multi
+	//   items:
+	//     type: string
+	//     enum: [issue,pull,commit,repository]
 	// - 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
 	// - name: page
 	//   in: query
 	//   description: page number of results to return (1-based)
@@ -99,24 +102,12 @@ func ListRepoNotifications(ctx *context.APIContext) {
 	// responses:
 	//   "200":
 	//     "$ref": "#/responses/NotificationThreadList"
-
-	before, since, err := utils.GetQueryBeforeSince(ctx)
-	if err != nil {
-		ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err)
+	opts := getFindNotificationOptions(ctx)
+	if ctx.Written() {
 		return
 	}
-	opts := models.FindNotificationOptions{
-		ListOptions:       utils.GetListOptions(ctx),
-		UserID:            ctx.User.ID,
-		RepoID:            ctx.Repo.Repository.ID,
-		UpdatedBeforeUnix: before,
-		UpdatedAfterUnix:  since,
-	}
+	opts.RepoID = ctx.Repo.Repository.ID
 
-	if !ctx.QueryBool("all") {
-		statuses := ctx.QueryStrings("status-types")
-		opts.Status = statusStringsToNotificationStatuses(statuses, []string{"unread", "pinned"})
-	}
 	nl, err := models.GetNotifications(opts)
 	if err != nil {
 		ctx.InternalServerError(err)
@@ -192,7 +183,7 @@ func ReadRepoNotifications(ctx *context.APIContext) {
 		}
 	}
 
-	opts := models.FindNotificationOptions{
+	opts := &models.FindNotificationOptions{
 		UserID:            ctx.User.ID,
 		RepoID:            ctx.Repo.Repository.ID,
 		UpdatedBeforeUnix: lastRead,
diff --git a/routers/api/v1/notify/user.go b/routers/api/v1/notify/user.go
index e739c6a38..1ff62622b 100644
--- a/routers/api/v1/notify/user.go
+++ b/routers/api/v1/notify/user.go
@@ -12,7 +12,6 @@ import (
 	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/convert"
-	"code.gitea.io/gitea/routers/api/v1/utils"
 )
 
 // ListNotifications list users's notification threads
@@ -29,7 +28,6 @@ func ListNotifications(ctx *context.APIContext) {
 	//   in: query
 	//   description: If true, show notifications marked as read. Default value is false
 	//   type: string
-	//   required: false
 	// - name: status-types
 	//   in: query
 	//   description: "Show notifications with the provided status types. Options are: unread, read and/or pinned. Defaults to unread & pinned."
@@ -37,19 +35,24 @@ func ListNotifications(ctx *context.APIContext) {
 	//   collectionFormat: multi
 	//   items:
 	//     type: string
-	//   required: false
+	// - name: subject-type
+	//   in: query
+	//   description: "filter notifications by subject type"
+	//   type: array
+	//   collectionFormat: multi
+	//   items:
+	//     type: string
+	//     enum: [issue,pull,commit,repository]
 	// - 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
 	// - name: page
 	//   in: query
 	//   description: page number of results to return (1-based)
@@ -61,22 +64,11 @@ func ListNotifications(ctx *context.APIContext) {
 	// responses:
 	//   "200":
 	//     "$ref": "#/responses/NotificationThreadList"
-
-	before, since, err := utils.GetQueryBeforeSince(ctx)
-	if err != nil {
-		ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err)
+	opts := getFindNotificationOptions(ctx)
+	if ctx.Written() {
 		return
 	}
-	opts := models.FindNotificationOptions{
-		ListOptions:       utils.GetListOptions(ctx),
-		UserID:            ctx.User.ID,
-		UpdatedBeforeUnix: before,
-		UpdatedAfterUnix:  since,
-	}
-	if !ctx.QueryBool("all") {
-		statuses := ctx.QueryStrings("status-types")
-		opts.Status = statusStringsToNotificationStatuses(statuses, []string{"unread", "pinned"})
-	}
+
 	nl, err := models.GetNotifications(opts)
 	if err != nil {
 		ctx.InternalServerError(err)
@@ -141,7 +133,7 @@ func ReadNotifications(ctx *context.APIContext) {
 			lastRead = tmpLastRead.Unix()
 		}
 	}
-	opts := models.FindNotificationOptions{
+	opts := &models.FindNotificationOptions{
 		UserID:            ctx.User.ID,
 		UpdatedBeforeUnix: lastRead,
 	}
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index e3ac4a4c8..8ad9ae5a4 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -645,6 +645,22 @@
             "name": "status-types",
             "in": "query"
           },
+          {
+            "type": "array",
+            "items": {
+              "enum": [
+                "issue",
+                "pull",
+                "commit",
+                "repository"
+              ],
+              "type": "string"
+            },
+            "collectionFormat": "multi",
+            "description": "filter notifications by subject type",
+            "name": "subject-type",
+            "in": "query"
+          },
           {
             "type": "string",
             "format": "date-time",
@@ -6805,6 +6821,22 @@
             "name": "status-types",
             "in": "query"
           },
+          {
+            "type": "array",
+            "items": {
+              "enum": [
+                "issue",
+                "pull",
+                "commit",
+                "repository"
+              ],
+              "type": "string"
+            },
+            "collectionFormat": "multi",
+            "description": "filter notifications by subject type",
+            "name": "subject-type",
+            "in": "query"
+          },
           {
             "type": "string",
             "format": "date-time",