summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
author6543 <6543@obermui.de>2019-12-07 23:04:19 +0100
committertechknowlogick <techknowlogick@gitea.io>2019-12-07 17:04:19 -0500
commit37e10d4543c1e516e1a721d72c0054fefceb9499 (patch)
treef824b014a135d3bcf243d5bb3e87cab3de225c90
parentee7df7ba8c5e6a4b32b0c4048d2b535d8df3cbe9 (diff)
downloadgitea-37e10d4543c1e516e1a721d72c0054fefceb9499.tar.gz
gitea-37e10d4543c1e516e1a721d72c0054fefceb9499.zip
[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
-rw-r--r--integrations/api_issue_reaction_test.go145
-rw-r--r--models/error.go15
-rw-r--r--models/fixtures/reaction.yml40
-rw-r--r--models/issue_reaction.go39
-rw-r--r--models/issue_reaction_test.go24
-rw-r--r--modules/setting/setting.go6
-rw-r--r--modules/structs/issue_reaction.go22
-rw-r--r--routers/api/v1/api.go20
-rw-r--r--routers/api/v1/repo/issue_reaction.go394
-rw-r--r--routers/api/v1/swagger/issue.go21
-rw-r--r--routers/repo/issue.go20
-rw-r--r--templates/swagger/v1_json.tmpl335
12 files changed, 1049 insertions, 32 deletions
diff --git a/integrations/api_issue_reaction_test.go b/integrations/api_issue_reaction_test.go
new file mode 100644
index 0000000000..f3fcf3946f
--- /dev/null
+++ b/integrations/api_issue_reaction_test.go
@@ -0,0 +1,145 @@
+// 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 integrations
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+ "time"
+
+ "code.gitea.io/gitea/models"
+ api "code.gitea.io/gitea/modules/structs"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestAPIIssuesReactions(t *testing.T) {
+ defer prepareTestEnv(t)()
+
+ issue := models.AssertExistsAndLoadBean(t, &models.Issue{ID: 1}).(*models.Issue)
+ _ = issue.LoadRepo()
+ owner := models.AssertExistsAndLoadBean(t, &models.User{ID: issue.Repo.OwnerID}).(*models.User)
+
+ session := loginUser(t, owner.Name)
+ token := getTokenForLoggedInUser(t, session)
+
+ user1 := models.AssertExistsAndLoadBean(t, &models.User{ID: 1}).(*models.User)
+ user2 := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User)
+ urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/reactions?token=%s",
+ owner.Name, issue.Repo.Name, issue.Index, token)
+
+ //Try to add not allowed reaction
+ req := NewRequestWithJSON(t, "POST", urlStr, &api.EditReactionOption{
+ Reaction: "wrong",
+ })
+ resp := session.MakeRequest(t, req, http.StatusForbidden)
+
+ //Delete not allowed reaction
+ req = NewRequestWithJSON(t, "DELETE", urlStr, &api.EditReactionOption{
+ Reaction: "zzz",
+ })
+ resp = session.MakeRequest(t, req, http.StatusOK)
+
+ //Add allowed reaction
+ req = NewRequestWithJSON(t, "POST", urlStr, &api.EditReactionOption{
+ Reaction: "rocket",
+ })
+ resp = session.MakeRequest(t, req, http.StatusCreated)
+ var apiNewReaction api.ReactionResponse
+ DecodeJSON(t, resp, &apiNewReaction)
+
+ //Add existing reaction
+ resp = session.MakeRequest(t, req, http.StatusForbidden)
+
+ //Get end result of reaction list of issue #1
+ req = NewRequestf(t, "GET", urlStr)
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ var apiReactions []*api.ReactionResponse
+ DecodeJSON(t, resp, &apiReactions)
+ expectResponse := make(map[int]api.ReactionResponse)
+ expectResponse[0] = api.ReactionResponse{
+ User: user1.APIFormat(),
+ Reaction: "zzz",
+ Created: time.Unix(1573248002, 0),
+ }
+ expectResponse[1] = api.ReactionResponse{
+ User: user2.APIFormat(),
+ Reaction: "eyes",
+ Created: time.Unix(1573248003, 0),
+ }
+ expectResponse[2] = apiNewReaction
+ assert.Len(t, apiReactions, 3)
+ for i, r := range apiReactions {
+ assert.Equal(t, expectResponse[i].Reaction, r.Reaction)
+ assert.Equal(t, expectResponse[i].Created.Unix(), r.Created.Unix())
+ assert.Equal(t, expectResponse[i].User.ID, r.User.ID)
+ }
+}
+
+func TestAPICommentReactions(t *testing.T) {
+ defer prepareTestEnv(t)()
+
+ comment := models.AssertExistsAndLoadBean(t, &models.Comment{ID: 2}).(*models.Comment)
+ _ = comment.LoadIssue()
+ issue := comment.Issue
+ _ = issue.LoadRepo()
+ owner := models.AssertExistsAndLoadBean(t, &models.User{ID: issue.Repo.OwnerID}).(*models.User)
+
+ session := loginUser(t, owner.Name)
+ token := getTokenForLoggedInUser(t, session)
+
+ user1 := models.AssertExistsAndLoadBean(t, &models.User{ID: 1}).(*models.User)
+ user2 := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User)
+ urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments/%d/reactions?token=%s",
+ owner.Name, issue.Repo.Name, comment.ID, token)
+
+ //Try to add not allowed reaction
+ req := NewRequestWithJSON(t, "POST", urlStr, &api.EditReactionOption{
+ Reaction: "wrong",
+ })
+ resp := session.MakeRequest(t, req, http.StatusForbidden)
+
+ //Delete none existing reaction
+ req = NewRequestWithJSON(t, "DELETE", urlStr, &api.EditReactionOption{
+ Reaction: "eyes",
+ })
+ resp = session.MakeRequest(t, req, http.StatusOK)
+
+ //Add allowed reaction
+ req = NewRequestWithJSON(t, "POST", urlStr, &api.EditReactionOption{
+ Reaction: "+1",
+ })
+ resp = session.MakeRequest(t, req, http.StatusCreated)
+ var apiNewReaction api.ReactionResponse
+ DecodeJSON(t, resp, &apiNewReaction)
+
+ //Add existing reaction
+ resp = session.MakeRequest(t, req, http.StatusForbidden)
+
+ //Get end result of reaction list of issue #1
+ req = NewRequestf(t, "GET", urlStr)
+ resp = session.MakeRequest(t, req, http.StatusOK)
+ var apiReactions []*api.ReactionResponse
+ DecodeJSON(t, resp, &apiReactions)
+ expectResponse := make(map[int]api.ReactionResponse)
+ expectResponse[0] = api.ReactionResponse{
+ User: user2.APIFormat(),
+ Reaction: "laugh",
+ Created: time.Unix(1573248004, 0),
+ }
+ expectResponse[1] = api.ReactionResponse{
+ User: user1.APIFormat(),
+ Reaction: "laugh",
+ Created: time.Unix(1573248005, 0),
+ }
+ expectResponse[2] = apiNewReaction
+ assert.Len(t, apiReactions, 3)
+ for i, r := range apiReactions {
+ assert.Equal(t, expectResponse[i].Reaction, r.Reaction)
+ assert.Equal(t, expectResponse[i].Created.Unix(), r.Created.Unix())
+ assert.Equal(t, expectResponse[i].User.ID, r.User.ID)
+ }
+}
diff --git a/models/error.go b/models/error.go
index 313c36354d..16be512139 100644
--- a/models/error.go
+++ b/models/error.go
@@ -1121,6 +1121,21 @@ func (err ErrNewIssueInsert) Error() string {
return err.OriginalError.Error()
}
+// ErrForbiddenIssueReaction is used when a forbidden reaction was try to created
+type ErrForbiddenIssueReaction struct {
+ Reaction string
+}
+
+// IsErrForbiddenIssueReaction checks if an error is a ErrForbiddenIssueReaction.
+func IsErrForbiddenIssueReaction(err error) bool {
+ _, ok := err.(ErrForbiddenIssueReaction)
+ return ok
+}
+
+func (err ErrForbiddenIssueReaction) Error() string {
+ return fmt.Sprintf("'%s' is not an allowed reaction", err.Reaction)
+}
+
// __________ .__ .__ __________ __
// \______ \__ __| | | |\______ \ ____ ________ __ ____ _______/ |_
// | ___/ | \ | | | | _// __ \/ ____/ | \_/ __ \ / ___/\ __\
diff --git a/models/fixtures/reaction.yml b/models/fixtures/reaction.yml
index ca780a73aa..4925935fe6 100644
--- a/models/fixtures/reaction.yml
+++ b/models/fixtures/reaction.yml
@@ -1 +1,39 @@
-[] # empty
+-
+ id: 1 #issue reaction
+ type: zzz # not allowed reaction (added before allowed reaction list has changed)
+ issue_id: 1
+ comment_id: 0
+ user_id: 2
+ created_unix: 1573248001
+
+-
+ id: 2 #issue reaction
+ type: zzz # not allowed reaction (added before allowed reaction list has changed)
+ issue_id: 1
+ comment_id: 0
+ user_id: 1
+ created_unix: 1573248002
+
+-
+ id: 3 #issue reaction
+ type: eyes # allowed reaction
+ issue_id: 1
+ comment_id: 0
+ user_id: 2
+ created_unix: 1573248003
+
+-
+ id: 4 #comment reaction
+ type: laugh # allowed reaction
+ issue_id: 1
+ comment_id: 2
+ user_id: 2
+ created_unix: 1573248004
+
+-
+ id: 5 #comment reaction
+ type: laugh # allowed reaction
+ issue_id: 1
+ comment_id: 2
+ user_id: 1
+ created_unix: 1573248005
diff --git a/models/issue_reaction.go b/models/issue_reaction.go
index 4596d32d06..b4f332a089 100644
--- a/models/issue_reaction.go
+++ b/models/issue_reaction.go
@@ -33,16 +33,38 @@ type FindReactionsOptions struct {
}
func (opts *FindReactionsOptions) toConds() builder.Cond {
+ //If Issue ID is set add to Query
var cond = builder.NewCond()
if opts.IssueID > 0 {
cond = cond.And(builder.Eq{"reaction.issue_id": opts.IssueID})
}
+ //If CommentID is > 0 add to Query
+ //If it is 0 Query ignore CommentID to select
+ //If it is -1 it explicit search of Issue Reactions where CommentID = 0
if opts.CommentID > 0 {
cond = cond.And(builder.Eq{"reaction.comment_id": opts.CommentID})
+ } else if opts.CommentID == -1 {
+ cond = cond.And(builder.Eq{"reaction.comment_id": 0})
}
+
return cond
}
+// FindCommentReactions returns a ReactionList of all reactions from an comment
+func FindCommentReactions(comment *Comment) (ReactionList, error) {
+ return findReactions(x, FindReactionsOptions{
+ IssueID: comment.IssueID,
+ CommentID: comment.ID})
+}
+
+// FindIssueReactions returns a ReactionList of all reactions from an issue
+func FindIssueReactions(issue *Issue) (ReactionList, error) {
+ return findReactions(x, FindReactionsOptions{
+ IssueID: issue.ID,
+ CommentID: -1,
+ })
+}
+
func findReactions(e Engine, opts FindReactionsOptions) ([]*Reaction, error) {
reactions := make([]*Reaction, 0, 10)
sess := e.Where(opts.toConds())
@@ -77,6 +99,10 @@ type ReactionOptions struct {
// CreateReaction creates reaction for issue or comment.
func CreateReaction(opts *ReactionOptions) (reaction *Reaction, err error) {
+ if !setting.UI.ReactionsMap[opts.Type] {
+ return nil, ErrForbiddenIssueReaction{opts.Type}
+ }
+
sess := x.NewSession()
defer sess.Close()
if err = sess.Begin(); err != nil {
@@ -160,6 +186,19 @@ func DeleteCommentReaction(doer *User, issue *Issue, comment *Comment, content s
})
}
+// LoadUser load user of reaction
+func (r *Reaction) LoadUser() (*User, error) {
+ if r.User != nil {
+ return r.User, nil
+ }
+ user, err := getUserByID(x, r.UserID)
+ if err != nil {
+ return nil, err
+ }
+ r.User = user
+ return user, nil
+}
+
// ReactionList represents list of reactions
type ReactionList []*Reaction
diff --git a/models/issue_reaction_test.go b/models/issue_reaction_test.go
index bbd8cf29fe..1189b389e9 100644
--- a/models/issue_reaction_test.go
+++ b/models/issue_reaction_test.go
@@ -81,22 +81,22 @@ func TestIssueReactionCount(t *testing.T) {
user4 := AssertExistsAndLoadBean(t, &User{ID: 4}).(*User)
ghost := NewGhostUser()
- issue1 := AssertExistsAndLoadBean(t, &Issue{ID: 1}).(*Issue)
+ issue := AssertExistsAndLoadBean(t, &Issue{ID: 2}).(*Issue)
- addReaction(t, user1, issue1, nil, "heart")
- addReaction(t, user2, issue1, nil, "heart")
- addReaction(t, user3, issue1, nil, "heart")
- addReaction(t, user3, issue1, nil, "+1")
- addReaction(t, user4, issue1, nil, "+1")
- addReaction(t, user4, issue1, nil, "heart")
- addReaction(t, ghost, issue1, nil, "-1")
-
- err := issue1.loadReactions(x)
+ addReaction(t, user1, issue, nil, "heart")
+ addReaction(t, user2, issue, nil, "heart")
+ addReaction(t, user3, issue, nil, "heart")
+ addReaction(t, user3, issue, nil, "+1")
+ addReaction(t, user4, issue, nil, "+1")
+ addReaction(t, user4, issue, nil, "heart")
+ addReaction(t, ghost, issue, nil, "-1")
+
+ err := issue.loadReactions(x)
assert.NoError(t, err)
- assert.Len(t, issue1.Reactions, 7)
+ assert.Len(t, issue.Reactions, 7)
- reactions := issue1.Reactions.GroupByType()
+ reactions := issue.Reactions.GroupByType()
assert.Len(t, reactions["heart"], 4)
assert.Equal(t, 2, reactions["heart"].GetMoreUserCount())
assert.Equal(t, user1.DisplayName()+", "+user2.DisplayName(), reactions["heart"].GetFirstUsers())
diff --git a/modules/setting/setting.go b/modules/setting/setting.go
index c08621df58..f55833a0e0 100644
--- a/modules/setting/setting.go
+++ b/modules/setting/setting.go
@@ -171,6 +171,7 @@ var (
DefaultTheme string
Themes []string
Reactions []string
+ ReactionsMap map[string]bool
SearchRepoDescription bool
UseServiceWorker bool
@@ -985,6 +986,11 @@ func NewContext() {
U2F.AppID = sec.Key("APP_ID").MustString(strings.TrimRight(AppURL, "/"))
zip.Verbose = false
+
+ UI.ReactionsMap = make(map[string]bool)
+ for _, reaction := range UI.Reactions {
+ UI.ReactionsMap[reaction] = true
+ }
}
func loadInternalToken(sec *ini.Section) string {
diff --git a/modules/structs/issue_reaction.go b/modules/structs/issue_reaction.go
new file mode 100644
index 0000000000..9d71740052
--- /dev/null
+++ b/modules/structs/issue_reaction.go
@@ -0,0 +1,22 @@
+// 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 structs
+
+import (
+ "time"
+)
+
+// EditReactionOption contain the reaction type
+type EditReactionOption struct {
+ Reaction string `json:"content"`
+}
+
+// ReactionResponse contain one reaction
+type ReactionResponse struct {
+ User *User `json:"user"`
+ Reaction string `json:"content"`
+ // swagger:strfmt date-time
+ Created time.Time `json:"created_at"`
+}
diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go
index 47c9c95c7f..cd5fc1f3eb 100644
--- a/routers/api/v1/api.go
+++ b/routers/api/v1/api.go
@@ -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() {
diff --git a/routers/api/v1/repo/issue_reaction.go b/routers/api/v1/repo/issue_reaction.go
new file mode 100644
index 0000000000..56e12ccdcc
--- /dev/null
+++ b/routers/api/v1/repo/issue_reaction.go
@@ -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)
+ }
+}
diff --git a/routers/api/v1/swagger/issue.go b/routers/api/v1/swagger/issue.go
index c06186bf6c..a78c2982fd 100644
--- a/routers/api/v1/swagger/issue.go
+++ b/routers/api/v1/swagger/issue.go
@@ -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"`
+}
diff --git a/routers/repo/issue.go b/routers/repo/issue.go
index c79ea02e86..5d5aaca253 100644
--- a/routers/repo/issue.go
+++ b/routers/repo/issue.go
@@ -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
}
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index 1d8cd36858..9c8db28817 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -3016,6 +3016,148 @@
}
}
},
+ "/repos/{owner}/{repo}/issues/comments/{id}/reactions": {
+ "get": {
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "issue"
+ ],
+ "summary": "Get a list reactions of a issue 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 to edit",
+ "name": "id",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "$ref": "#/responses/ReactionResponseList"
+ }
+ }
+ },
+ "post": {
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "issue"
+ ],
+ "summary": "Add a reaction to a comment of a issue comment",
+ "operationId": "issuePostCommentReaction",
+ "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 to edit",
+ "name": "id",
+ "in": "path",
+ "required": true
+ },
+ {
+ "name": "content",
+ "in": "body",
+ "schema": {
+ "$ref": "#/definitions/EditReactionOption"
+ }
+ }
+ ],
+ "responses": {
+ "201": {
+ "$ref": "#/responses/ReactionResponse"
+ }
+ }
+ },
+ "delete": {
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "issue"
+ ],
+ "summary": "Remove a reaction from a comment of a issue comment",
+ "operationId": "issueDeleteCommentReaction",
+ "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 to edit",
+ "name": "id",
+ "in": "path",
+ "required": true
+ },
+ {
+ "name": "content",
+ "in": "body",
+ "schema": {
+ "$ref": "#/definitions/EditReactionOption"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "$ref": "#/responses/empty"
+ }
+ }
+ }
+ },
"/repos/{owner}/{repo}/issues/{id}/times": {
"get": {
"produces": [
@@ -3688,6 +3830,148 @@
}
}
},
+ "/repos/{owner}/{repo}/issues/{index}/reactions": {
+ "get": {
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "issue"
+ ],
+ "summary": "Get a list reactions of a issue",
+ "operationId": "issueGetIssueReactions",
+ "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/ReactionResponseList"
+ }
+ }
+ },
+ "post": {
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "issue"
+ ],
+ "summary": "Add a reaction to a comment of a issue",
+ "operationId": "issuePostIssueReaction",
+ "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
+ },
+ {
+ "name": "content",
+ "in": "body",
+ "schema": {
+ "$ref": "#/definitions/EditReactionOption"
+ }
+ }
+ ],
+ "responses": {
+ "201": {
+ "$ref": "#/responses/ReactionResponse"
+ }
+ }
+ },
+ "delete": {
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "issue"
+ ],
+ "summary": "Remove a reaction from a comment of a issue",
+ "operationId": "issueDeleteIssueReaction",
+ "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
+ },
+ {
+ "name": "content",
+ "in": "body",
+ "schema": {
+ "$ref": "#/definitions/EditReactionOption"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "$ref": "#/responses/empty"
+ }
+ }
+ }
+ },
"/repos/{owner}/{repo}/issues/{index}/stopwatch/start": {
"post": {
"consumes": [
@@ -8721,6 +9005,17 @@
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
+ "EditReactionOption": {
+ "description": "EditReactionOption contain the reaction type",
+ "type": "object",
+ "properties": {
+ "content": {
+ "type": "string",
+ "x-go-name": "Reaction"
+ }
+ },
+ "x-go-package": "code.gitea.io/gitea/modules/structs"
+ },
"EditReleaseOption": {
"description": "EditReleaseOption options when editing a release",
"type": "object",
@@ -10095,6 +10390,25 @@
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
+ "ReactionResponse": {
+ "description": "ReactionResponse contain one reaction",
+ "type": "object",
+ "properties": {
+ "content": {
+ "type": "string",
+ "x-go-name": "Reaction"
+ },
+ "created_at": {
+ "type": "string",
+ "format": "date-time",
+ "x-go-name": "Created"
+ },
+ "user": {
+ "$ref": "#/definitions/User"
+ }
+ },
+ "x-go-package": "code.gitea.io/gitea/modules/structs"
+ },
"Reference": {
"type": "object",
"title": "Reference represents a Git reference.",
@@ -10960,6 +11274,12 @@
}
}
},
+ "EditReactionOption": {
+ "description": "EditReactionOption",
+ "schema": {
+ "$ref": "#/definitions/EditReactionOption"
+ }
+ },
"EmailList": {
"description": "EmailList",
"schema": {
@@ -11146,6 +11466,21 @@
}
}
},
+ "ReactionResponse": {
+ "description": "ReactionResponse",
+ "schema": {
+ "$ref": "#/definitions/ReactionResponse"
+ }
+ },
+ "ReactionResponseList": {
+ "description": "ReactionResponseList",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/ReactionResponse"
+ }
+ }
+ },
"Reference": {
"description": "Reference",
"schema": {