* 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
tags/v1.21.12.1
| @@ -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) | |||
| } | |||
| } | |||
| @@ -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) | |||
| } | |||
| // __________ .__ .__ __________ __ | |||
| // \______ \__ __| | | |\______ \ ____ ________ __ ____ _______/ |_ | |||
| // | ___/ | \ | | | | _// __ \/ ____/ | \_/ __ \ / ___/\ __\ | |||
| @@ -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 | |||
| @@ -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 | |||
| @@ -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()) | |||
| @@ -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 { | |||
| @@ -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"` | |||
| } | |||
| @@ -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() { | |||
| @@ -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 | |||
| } | |||
| @@ -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": { | |||