diff --git a/models/issue_reaction.go b/models/issue_reaction.go index 4596d32d06a7..7595cdfecba8 100644 --- a/models/issue_reaction.go +++ b/models/issue_reaction.go @@ -9,6 +9,7 @@ import ( "fmt" "code.gitea.io/gitea/modules/setting" + api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/timeutil" "xorm.io/builder" @@ -26,6 +27,9 @@ type Reaction struct { CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` } +// ReactionList represents list of reactions +type ReactionList []*Reaction + // FindReactionsOptions describes the conditions to Find reactions type FindReactionsOptions struct { IssueID int64 @@ -43,8 +47,15 @@ func (opts *FindReactionsOptions) toConds() builder.Cond { return cond } -func findReactions(e Engine, opts FindReactionsOptions) ([]*Reaction, error) { - reactions := make([]*Reaction, 0, 10) +//FindReactions returns Reactions based +func FindReactions(comment *Comment) (ReactionList, error) { + return findReactions(x, FindReactionsOptions{ + IssueID: comment.IssueID, + CommentID: comment.ID}) +} + +func findReactions(e Engine, opts FindReactionsOptions) (ReactionList, error) { + reactions := make(ReactionList, 0, 10) sess := e.Where(opts.toConds()) return reactions, sess. Asc("reaction.issue_id", "reaction.comment_id", "reaction.created_unix", "reaction.id"). @@ -160,9 +171,6 @@ func DeleteCommentReaction(doer *User, issue *Issue, comment *Comment, content s }) } -// ReactionList represents list of reactions -type ReactionList []*Reaction - // HasUser check if user has reacted func (list ReactionList) HasUser(userID int64) bool { if userID == 0 { @@ -247,3 +255,34 @@ func (list ReactionList) GetMoreUserCount() int { } return len(list) - setting.UI.ReactionMaxUserNum } + +// APIFormat returns Raction in api Format +func (list ReactionList) APIFormat() []*api.CommentReaction { + var result []*api.CommentReaction + users := make(map[string][]*string) + counts := make(map[string]int64) + + for _, r := range list { + u := r.User + t := r.Type + if t == "" { + _ = fmt.Errorf("Key is empty!") + continue + } + if u == nil { + _ = fmt.Errorf("Key: '" + t + "', User is Nil!") + continue + } + users[t] = append(users[t], &u.LoginName) + counts[t]++ + } + + for k, v := range users { + result = append(result, &api.CommentReaction{ + Reaction: k, + Users: v, + Count: counts[k], + }) + } + return result +} diff --git a/modules/structs/issue_comment.go b/modules/structs/issue_comment.go index 0c8ac20017fb..55a8a7a20853 100644 --- a/modules/structs/issue_comment.go +++ b/modules/structs/issue_comment.go @@ -35,3 +35,12 @@ type EditIssueCommentOption struct { // required: true Body string `json:"body" binding:"Required"` } + +// CommentReaction represent comment reactions +type CommentReaction struct { + // required: true + Reaction string `json:"reaction"` + // required: true + Users []*string `json:"users"` + Count int64 `json:"count"` +} diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index b68717f7c875..a2187a23c203 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -657,9 +657,15 @@ func RegisterRoutes(m *macaron.Macaron) { Post(reqToken(), mustNotBeArchived, bind(api.CreateIssueOption{}), repo.CreateIssue) m.Group("/comments", func() { m.Get("", repo.ListRepoIssueComments) - m.Combo("/:id", reqToken()). - Patch(mustNotBeArchived, bind(api.EditIssueCommentOption{}), repo.EditIssueComment). - Delete(repo.DeleteIssueComment) + m.Group("/:id", func() { + m.Combo("", reqToken()). + Patch(mustNotBeArchived, bind(api.EditIssueCommentOption{}), repo.EditIssueComment). + Delete(repo.DeleteIssueComment) + m.Combo("/reactions"). + Get(repo.GetCommentReactions). + Put(reqToken(), bind(api.CommentReaction{}), repo.AddCommentReaction). + Delete(reqToken(), bind(api.CommentReaction{}), repo.DelCommentReaction) + }) }) m.Group("/:index", func() { m.Combo("").Get(repo.GetIssue). diff --git a/routers/api/v1/repo/issue_comment.go b/routers/api/v1/repo/issue_comment.go index 3a5f6d24474f..99dc6e7407a1 100644 --- a/routers/api/v1/repo/issue_comment.go +++ b/routers/api/v1/repo/issue_comment.go @@ -393,3 +393,183 @@ func deleteIssueComment(ctx *context.APIContext) { ctx.Status(204) } + +//GetCommentReactions return all reactions of a specific comment +func GetCommentReactions(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/issues/comments/{id}/reactions issue issueGetCommentReactions + // --- + // summary: Return all reactions of a specific comment + // 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: id + // in: path + // description: id of the comment + // type: integer + // format: int64 + // required: true + // responses: + // "200": + // "$ref": "#/responses/CommentReactionList" + comment, err := models.GetCommentByID(ctx.ParamsInt64(":id")) + if err != nil { + if models.IsErrCommentNotExist(err) { + ctx.NotFound(err) + } else { + ctx.Error(500, "GetCommentByID", err) + } + return + } + + rl, err := models.FindReactions(comment) + if err != nil { + ctx.Error(500, "FindReactionsOptions", err) + return + } else if rl == nil { + ctx.NotFound("No Reactions Found") + return + } + + ctx.JSON(200, rl.APIFormat()) + +} + +// AddCommentReaction create a reaction to a comment +func AddCommentReaction(ctx *context.APIContext, form api.CommentReaction) { + // swagger:operation PUT /repos/{owner}/{repo}/issues/comments/{id}/reactions issue issueAddCommentReaction + // --- + // summary: Create reaction to a comment + // 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: id + // in: path + // description: id of the comment + // type: integer + // format: int64 + // required: true + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/CommentReaction" + // responses: + // "201": + // "$ref": "#/responses/empty" + // "304": + // description: User can only create reactions for itself if he is no admin + // "404": + // description: Comment not found + setCommentReaction(ctx, form, true) +} + +// DelCommentReaction delete a reaction to a comment +func DelCommentReaction(ctx *context.APIContext, form api.CommentReaction) { + // swagger:operation DELETE /repos/{owner}/{repo}/issues/comments/{id}/reactions issue issueDelCommentReaction + // --- + // summary: Delete reaction to a comment + // 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: id + // in: path + // description: id of the comment + // type: integer + // format: int64 + // required: true + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/CommentReaction" + // responses: + // "201": + // "$ref": "#/responses/empty" + // "304": + // description: User can only delete reactions for itself if he is no admin + // "404": + // description: Comment not found + setCommentReaction(ctx, form, false) +} + +func setCommentReaction(ctx *context.APIContext, form api.CommentReaction, create bool) { + comment, err := models.GetCommentByID(ctx.ParamsInt64(":id")) + if err != nil { + if models.IsErrCommentNotExist(err) { + ctx.NotFound(err) + } else { + ctx.Error(500, "GetCommentByID", err) + } + return + } + issue, err := models.GetIssueByID(comment.IssueID) + if err != nil { + if models.IsErrIssueNotExist(err) { + ctx.NotFound() + } else { + ctx.Error(500, "GetIssueByID", err) + } + + return + } + + for _, u := range form.Users { + user, err := models.GetUserByName(*u) + if err != nil { + ctx.Error(500, "GetUserByName", err) + } + + if ctx.User.ID != user.ID && !ctx.Repo.IsAdmin() { + ctx.Status(403) + return + } + + if create { + // Create Reaction + _, err = models.CreateCommentReaction(user, issue, comment, form.Reaction) + if err != nil { + ctx.Error(500, "CreateCommentReaction", err) + } + } else { + // Delete Reaction + err = models.DeleteCommentReaction(user, issue, comment, form.Reaction) + if err != nil { + ctx.Error(500, "DeleteCommentReaction", err) + } + } + } + + ctx.Status(201) +} diff --git a/routers/api/v1/swagger/issue.go b/routers/api/v1/swagger/issue.go index c06186bf6cd7..e512e07949fb 100644 --- a/routers/api/v1/swagger/issue.go +++ b/routers/api/v1/swagger/issue.go @@ -84,3 +84,17 @@ type swaggerIssueDeadline struct { // in:body Body api.IssueDeadline `json:"body"` } + +// CommentReaction +// swagger:response CommentReaction +type swaggerCommentReaction struct { + // in:body + Body api.CommentReaction `json:"body"` +} + +// CommentReactionList +// swagger:response CommentReactionList +type swaggerResponseCommentReactionList struct { + // in:body + Body []api.CommentReaction `json:"body"` +} diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index dc162bc37d34..b0d2ed4250be 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -3003,6 +3003,157 @@ } } }, + "/repos/{owner}/{repo}/issues/comments/{id}/reactions": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "issue" + ], + "summary": "Return all reactions of a specific comment", + "operationId": "issueGetCommentReactions", + "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": "id of the comment", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/CommentReactionList" + } + } + }, + "put": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "issue" + ], + "summary": "Create reaction to a comment", + "operationId": "issueAddCommentReaction", + "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": "id of the comment", + "name": "id", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "schema": { + "$ref": "#/definitions/CommentReaction" + } + } + ], + "responses": { + "201": { + "$ref": "#/responses/empty" + }, + "304": { + "description": "User can only create reactions for itself if he is no admin" + }, + "404": { + "description": "Comment not found" + } + } + }, + "delete": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "issue" + ], + "summary": "Delete reaction to a comment", + "operationId": "issueDelCommentReaction", + "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": "id of the comment", + "name": "id", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "schema": { + "$ref": "#/definitions/CommentReaction" + } + } + ], + "responses": { + "201": { + "$ref": "#/responses/empty" + }, + "304": { + "description": "User can only delete reactions for itself if he is no admin" + }, + "404": { + "description": "Comment not found" + } + } + } + }, "/repos/{owner}/{repo}/issues/{id}/times": { "get": { "produces": [ @@ -7627,6 +7778,47 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "CommentReaction": { + "description": "CommentReaction represent comment reactions", + "type": "object", + "required": [ + "reaction", + "users" + ], + "properties": { + "count": { + "type": "integer", + "format": "int64", + "x-go-name": "Count" + }, + "reaction": { + "type": "string", + "x-go-name": "Reaction" + }, + "users": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "Users" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, + "CommentReactionList": { + "description": "CommentReactionList is a list of comment reactions", + "type": "object", + "properties": { + "comment_reactions": { + "type": "array", + "items": { + "$ref": "#/definitions/CommentReaction" + }, + "x-go-name": "CommentReactions" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "Commit": { "type": "object", "title": "Commit contains information generated from a Git commit.", @@ -10817,6 +11009,21 @@ } } }, + "CommentReaction": { + "description": "CommentReaction", + "schema": { + "$ref": "#/definitions/CommentReactionList" + } + }, + "CommentReactionList": { + "description": "CommentReactionList", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/CommentReaction" + } + } + }, "Commit": { "description": "Commit", "schema": {