[API] Add Reactions (#9220)
* reject reactions wich ar not allowed * dont duble check CreateReaction now throw ErrForbiddenIssueReaction * add /repos/{owner}/{repo}/issues/comments/{id}/reactions endpoint * add Find Functions * fix some swagger stuff + add issue reaction endpoints + GET ReactionList now use FindReactions... * explicite Issue Only Reaction for FindReactionsOptions with "-1" commentID * load issue; load user ... * return error again * swagger def canged after LINT * check if user has ben loaded * add Tests * better way of comparing results * add suggestion * use different issue for test (dont interfear with integration test) * test dont compare Location on timeCompare * TEST: add forbidden dubble add * add comments in code to explain * add settings.UI.ReactionsMap so if !setting.UI.ReactionsMap[opts.Type] works
This commit is contained in:
parent
ee7df7ba8c
commit
37e10d4543
12 changed files with 1048 additions and 31 deletions
|
@ -657,21 +657,25 @@ 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", reqToken()).
|
||||
Get(repo.GetIssueCommentReactions).
|
||||
Post(bind(api.EditReactionOption{}), repo.PostIssueCommentReaction).
|
||||
Delete(bind(api.EditReactionOption{}), repo.DeleteIssueCommentReaction)
|
||||
})
|
||||
})
|
||||
m.Group("/:index", func() {
|
||||
m.Combo("").Get(repo.GetIssue).
|
||||
Patch(reqToken(), bind(api.EditIssueOption{}), repo.EditIssue)
|
||||
|
||||
m.Group("/comments", func() {
|
||||
m.Combo("").Get(repo.ListIssueComments).
|
||||
Post(reqToken(), mustNotBeArchived, bind(api.CreateIssueCommentOption{}), repo.CreateIssueComment)
|
||||
m.Combo("/:id", reqToken()).Patch(bind(api.EditIssueCommentOption{}), repo.EditIssueCommentDeprecated).
|
||||
Delete(repo.DeleteIssueCommentDeprecated)
|
||||
})
|
||||
|
||||
m.Group("/labels", func() {
|
||||
m.Combo("").Get(repo.ListIssueLabels).
|
||||
Post(reqToken(), bind(api.IssueLabelsOption{}), repo.AddIssueLabels).
|
||||
|
@ -679,12 +683,10 @@ func RegisterRoutes(m *macaron.Macaron) {
|
|||
Delete(reqToken(), repo.ClearIssueLabels)
|
||||
m.Delete("/:id", reqToken(), repo.DeleteIssueLabel)
|
||||
})
|
||||
|
||||
m.Group("/times", func() {
|
||||
m.Combo("").Get(repo.ListTrackedTimes).
|
||||
Post(reqToken(), bind(api.AddTimeOption{}), repo.AddTime)
|
||||
})
|
||||
|
||||
m.Combo("/deadline").Post(reqToken(), bind(api.EditDeadlineOption{}), repo.UpdateIssueDeadline)
|
||||
m.Group("/stopwatch", func() {
|
||||
m.Post("/start", reqToken(), repo.StartIssueStopwatch)
|
||||
|
@ -695,6 +697,10 @@ func RegisterRoutes(m *macaron.Macaron) {
|
|||
m.Put("/:user", reqToken(), repo.AddIssueSubscription)
|
||||
m.Delete("/:user", reqToken(), repo.DelIssueSubscription)
|
||||
})
|
||||
m.Combo("/reactions", reqToken()).
|
||||
Get(repo.GetIssueReactions).
|
||||
Post(bind(api.EditReactionOption{}), repo.PostIssueReaction).
|
||||
Delete(bind(api.EditReactionOption{}), repo.DeleteIssueReaction)
|
||||
})
|
||||
}, mustEnableIssuesOrPulls)
|
||||
m.Group("/labels", func() {
|
||||
|
|
394
routers/api/v1/repo/issue_reaction.go
Normal file
394
routers/api/v1/repo/issue_reaction.go
Normal file
|
@ -0,0 +1,394 @@
|
|||
// Copyright 2019 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 repo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
)
|
||||
|
||||
// GetIssueCommentReactions list reactions of a issue comment
|
||||
func GetIssueCommentReactions(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/issues/comments/{id}/reactions issue issueGetCommentReactions
|
||||
// ---
|
||||
// summary: Get a list reactions of a issue 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 to edit
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/ReactionResponseList"
|
||||
comment, err := models.GetCommentByID(ctx.ParamsInt64(":id"))
|
||||
if err != nil {
|
||||
if models.IsErrCommentNotExist(err) {
|
||||
ctx.NotFound(err)
|
||||
} else {
|
||||
ctx.Error(500, "GetCommentByID", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.Repo.CanRead(models.UnitTypeIssues) && !ctx.User.IsAdmin {
|
||||
ctx.Error(403, "GetIssueCommentReactions", errors.New("no permission to get reactions"))
|
||||
return
|
||||
}
|
||||
|
||||
reactions, err := models.FindCommentReactions(comment)
|
||||
if err != nil {
|
||||
ctx.Error(500, "FindIssueReactions", err)
|
||||
return
|
||||
}
|
||||
_, err = reactions.LoadUsers()
|
||||
if err != nil {
|
||||
ctx.Error(500, "ReactionList.LoadUsers()", err)
|
||||
return
|
||||
}
|
||||
|
||||
var result []api.ReactionResponse
|
||||
for _, r := range reactions {
|
||||
result = append(result, api.ReactionResponse{
|
||||
User: r.User.APIFormat(),
|
||||
Reaction: r.Type,
|
||||
Created: r.CreatedUnix.AsTime(),
|
||||
})
|
||||
}
|
||||
|
||||
ctx.JSON(200, result)
|
||||
}
|
||||
|
||||
// PostIssueCommentReaction add a reaction to a comment of a issue
|
||||
func PostIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOption) {
|
||||
// swagger:operation POST /repos/{owner}/{repo}/issues/comments/{id}/reactions issue issuePostCommentReaction
|
||||
// ---
|
||||
// summary: Add a reaction to a comment of a issue 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 to edit
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: content
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/EditReactionOption"
|
||||
// responses:
|
||||
// "201":
|
||||
// "$ref": "#/responses/ReactionResponse"
|
||||
changeIssueCommentReaction(ctx, form, true)
|
||||
}
|
||||
|
||||
// DeleteIssueCommentReaction list reactions of a issue comment
|
||||
func DeleteIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOption) {
|
||||
// swagger:operation DELETE /repos/{owner}/{repo}/issues/comments/{id}/reactions issue issueDeleteCommentReaction
|
||||
// ---
|
||||
// summary: Remove a reaction from a comment of a issue 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 to edit
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: content
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/EditReactionOption"
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/empty"
|
||||
changeIssueCommentReaction(ctx, form, false)
|
||||
}
|
||||
|
||||
func changeIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOption, isCreateType 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
|
||||
}
|
||||
|
||||
err = comment.LoadIssue()
|
||||
if err != nil {
|
||||
ctx.Error(500, "comment.LoadIssue() failed", err)
|
||||
}
|
||||
|
||||
if comment.Issue.IsLocked && !ctx.Repo.CanWrite(models.UnitTypeIssues) && !ctx.User.IsAdmin {
|
||||
ctx.Error(403, "ChangeIssueCommentReaction", errors.New("no permission to change reaction"))
|
||||
return
|
||||
}
|
||||
|
||||
if isCreateType {
|
||||
// PostIssueCommentReaction part
|
||||
reaction, err := models.CreateCommentReaction(ctx.User, comment.Issue, comment, form.Reaction)
|
||||
if err != nil {
|
||||
if models.IsErrForbiddenIssueReaction(err) {
|
||||
ctx.Error(403, err.Error(), err)
|
||||
} else {
|
||||
ctx.Error(500, "CreateCommentReaction", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
_, err = reaction.LoadUser()
|
||||
if err != nil {
|
||||
ctx.Error(500, "Reaction.LoadUser()", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(201, api.ReactionResponse{
|
||||
User: reaction.User.APIFormat(),
|
||||
Reaction: reaction.Type,
|
||||
Created: reaction.CreatedUnix.AsTime(),
|
||||
})
|
||||
} else {
|
||||
// DeleteIssueCommentReaction part
|
||||
err = models.DeleteCommentReaction(ctx.User, comment.Issue, comment, form.Reaction)
|
||||
if err != nil {
|
||||
ctx.Error(500, "DeleteCommentReaction", err)
|
||||
return
|
||||
}
|
||||
ctx.Status(200)
|
||||
}
|
||||
}
|
||||
|
||||
// GetIssueReactions list reactions of a issue comment
|
||||
func GetIssueReactions(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/issues/{index}/reactions issue issueGetIssueReactions
|
||||
// ---
|
||||
// summary: Get a list reactions of a 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/ReactionResponseList"
|
||||
issue, err := models.GetIssueWithAttrsByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
|
||||
if err != nil {
|
||||
if models.IsErrIssueNotExist(err) {
|
||||
ctx.NotFound()
|
||||
} else {
|
||||
ctx.Error(500, "GetIssueByIndex", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.Repo.CanRead(models.UnitTypeIssues) && !ctx.User.IsAdmin {
|
||||
ctx.Error(403, "GetIssueReactions", errors.New("no permission to get reactions"))
|
||||
return
|
||||
}
|
||||
|
||||
reactions, err := models.FindIssueReactions(issue)
|
||||
if err != nil {
|
||||
ctx.Error(500, "FindIssueReactions", err)
|
||||
return
|
||||
}
|
||||
_, err = reactions.LoadUsers()
|
||||
if err != nil {
|
||||
ctx.Error(500, "ReactionList.LoadUsers()", err)
|
||||
return
|
||||
}
|
||||
|
||||
var result []api.ReactionResponse
|
||||
for _, r := range reactions {
|
||||
result = append(result, api.ReactionResponse{
|
||||
User: r.User.APIFormat(),
|
||||
Reaction: r.Type,
|
||||
Created: r.CreatedUnix.AsTime(),
|
||||
})
|
||||
}
|
||||
|
||||
ctx.JSON(200, result)
|
||||
}
|
||||
|
||||
// PostIssueReaction add a reaction to a comment of a issue
|
||||
func PostIssueReaction(ctx *context.APIContext, form api.EditReactionOption) {
|
||||
// swagger:operation POST /repos/{owner}/{repo}/issues/{index}/reactions issue issuePostIssueReaction
|
||||
// ---
|
||||
// summary: Add a reaction to a comment of a 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
|
||||
// - name: content
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/EditReactionOption"
|
||||
// responses:
|
||||
// "201":
|
||||
// "$ref": "#/responses/ReactionResponse"
|
||||
changeIssueReaction(ctx, form, true)
|
||||
}
|
||||
|
||||
// DeleteIssueReaction list reactions of a issue comment
|
||||
func DeleteIssueReaction(ctx *context.APIContext, form api.EditReactionOption) {
|
||||
// swagger:operation DELETE /repos/{owner}/{repo}/issues/{index}/reactions issue issueDeleteIssueReaction
|
||||
// ---
|
||||
// summary: Remove a reaction from a comment of a 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
|
||||
// - name: content
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/EditReactionOption"
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/empty"
|
||||
changeIssueReaction(ctx, form, false)
|
||||
}
|
||||
|
||||
func changeIssueReaction(ctx *context.APIContext, form api.EditReactionOption, isCreateType bool) {
|
||||
issue, err := models.GetIssueWithAttrsByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
|
||||
if err != nil {
|
||||
if models.IsErrIssueNotExist(err) {
|
||||
ctx.NotFound()
|
||||
} else {
|
||||
ctx.Error(500, "GetIssueByIndex", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if issue.IsLocked && !ctx.Repo.CanWrite(models.UnitTypeIssues) && !ctx.User.IsAdmin {
|
||||
ctx.Error(403, "ChangeIssueCommentReaction", errors.New("no permission to change reaction"))
|
||||
return
|
||||
}
|
||||
|
||||
if isCreateType {
|
||||
// PostIssueReaction part
|
||||
reaction, err := models.CreateIssueReaction(ctx.User, issue, form.Reaction)
|
||||
if err != nil {
|
||||
if models.IsErrForbiddenIssueReaction(err) {
|
||||
ctx.Error(403, err.Error(), err)
|
||||
} else {
|
||||
ctx.Error(500, "CreateCommentReaction", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
_, err = reaction.LoadUser()
|
||||
if err != nil {
|
||||
ctx.Error(500, "Reaction.LoadUser()", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(201, api.ReactionResponse{
|
||||
User: reaction.User.APIFormat(),
|
||||
Reaction: reaction.Type,
|
||||
Created: reaction.CreatedUnix.AsTime(),
|
||||
})
|
||||
} else {
|
||||
// DeleteIssueReaction part
|
||||
err = models.DeleteIssueReaction(ctx.User, issue, form.Reaction)
|
||||
if err != nil {
|
||||
ctx.Error(500, "DeleteIssueReaction", err)
|
||||
return
|
||||
}
|
||||
ctx.Status(200)
|
||||
}
|
||||
}
|
|
@ -84,3 +84,24 @@ type swaggerIssueDeadline struct {
|
|||
// in:body
|
||||
Body api.IssueDeadline `json:"body"`
|
||||
}
|
||||
|
||||
// EditReactionOption
|
||||
// swagger:response EditReactionOption
|
||||
type swaggerEditReactionOption struct {
|
||||
// in:body
|
||||
Body api.EditReactionOption `json:"body"`
|
||||
}
|
||||
|
||||
// ReactionResponse
|
||||
// swagger:response ReactionResponse
|
||||
type swaggerReactionResponse struct {
|
||||
// in:body
|
||||
Body api.ReactionResponse `json:"body"`
|
||||
}
|
||||
|
||||
// ReactionResponseList
|
||||
// swagger:response ReactionResponseList
|
||||
type swaggerReactionResponseList struct {
|
||||
// in:body
|
||||
Body []api.ReactionResponse `json:"body"`
|
||||
}
|
||||
|
|
|
@ -1463,14 +1463,12 @@ func ChangeIssueReaction(ctx *context.Context, form auth.ReactionForm) {
|
|||
|
||||
switch ctx.Params(":action") {
|
||||
case "react":
|
||||
if !util.IsStringInSlice(form.Content, setting.UI.Reactions) {
|
||||
err := fmt.Errorf("ChangeIssueReaction: '%s' is not an allowed reaction", form.Content)
|
||||
ctx.ServerError(err.Error(), err)
|
||||
return
|
||||
}
|
||||
|
||||
reaction, err := models.CreateIssueReaction(ctx.User, issue, form.Content)
|
||||
if err != nil {
|
||||
if models.IsErrForbiddenIssueReaction(err) {
|
||||
ctx.ServerError("ChangeIssueReaction", err)
|
||||
return
|
||||
}
|
||||
log.Info("CreateIssueReaction: %s", err)
|
||||
break
|
||||
}
|
||||
|
@ -1564,14 +1562,12 @@ func ChangeCommentReaction(ctx *context.Context, form auth.ReactionForm) {
|
|||
|
||||
switch ctx.Params(":action") {
|
||||
case "react":
|
||||
if !util.IsStringInSlice(form.Content, setting.UI.Reactions) {
|
||||
err := fmt.Errorf("ChangeIssueReaction: '%s' is not an allowed reaction", form.Content)
|
||||
ctx.ServerError(err.Error(), err)
|
||||
return
|
||||
}
|
||||
|
||||
reaction, err := models.CreateCommentReaction(ctx.User, comment.Issue, comment, form.Content)
|
||||
if err != nil {
|
||||
if models.IsErrForbiddenIssueReaction(err) {
|
||||
ctx.ServerError("ChangeIssueReaction", err)
|
||||
return
|
||||
}
|
||||
log.Info("CreateCommentReaction: %s", err)
|
||||
break
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue