mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 21:28:11 +09:00 
			
		
		
		
	[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:
		
							
								
								
									
										145
									
								
								integrations/api_issue_reaction_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										145
									
								
								integrations/api_issue_reaction_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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() | 	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 { | func (opts *FindReactionsOptions) toConds() builder.Cond { | ||||||
|  | 	//If Issue ID is set add to Query | ||||||
| 	var cond = builder.NewCond() | 	var cond = builder.NewCond() | ||||||
| 	if opts.IssueID > 0 { | 	if opts.IssueID > 0 { | ||||||
| 		cond = cond.And(builder.Eq{"reaction.issue_id": opts.IssueID}) | 		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 { | 	if opts.CommentID > 0 { | ||||||
| 		cond = cond.And(builder.Eq{"reaction.comment_id": opts.CommentID}) | 		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 | 	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) { | func findReactions(e Engine, opts FindReactionsOptions) ([]*Reaction, error) { | ||||||
| 	reactions := make([]*Reaction, 0, 10) | 	reactions := make([]*Reaction, 0, 10) | ||||||
| 	sess := e.Where(opts.toConds()) | 	sess := e.Where(opts.toConds()) | ||||||
| @@ -77,6 +99,10 @@ type ReactionOptions struct { | |||||||
|  |  | ||||||
| // CreateReaction creates reaction for issue or comment. | // CreateReaction creates reaction for issue or comment. | ||||||
| func CreateReaction(opts *ReactionOptions) (reaction *Reaction, err error) { | func CreateReaction(opts *ReactionOptions) (reaction *Reaction, err error) { | ||||||
|  | 	if !setting.UI.ReactionsMap[opts.Type] { | ||||||
|  | 		return nil, ErrForbiddenIssueReaction{opts.Type} | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	sess := x.NewSession() | 	sess := x.NewSession() | ||||||
| 	defer sess.Close() | 	defer sess.Close() | ||||||
| 	if err = sess.Begin(); err != nil { | 	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 | // ReactionList represents list of reactions | ||||||
| type ReactionList []*Reaction | type ReactionList []*Reaction | ||||||
|  |  | ||||||
|   | |||||||
| @@ -81,22 +81,22 @@ func TestIssueReactionCount(t *testing.T) { | |||||||
| 	user4 := AssertExistsAndLoadBean(t, &User{ID: 4}).(*User) | 	user4 := AssertExistsAndLoadBean(t, &User{ID: 4}).(*User) | ||||||
| 	ghost := NewGhostUser() | 	ghost := NewGhostUser() | ||||||
|  |  | ||||||
| 	issue1 := AssertExistsAndLoadBean(t, &Issue{ID: 1}).(*Issue) | 	issue := AssertExistsAndLoadBean(t, &Issue{ID: 2}).(*Issue) | ||||||
|  |  | ||||||
| 	addReaction(t, user1, issue1, nil, "heart") | 	addReaction(t, user1, issue, nil, "heart") | ||||||
| 	addReaction(t, user2, issue1, nil, "heart") | 	addReaction(t, user2, issue, nil, "heart") | ||||||
| 	addReaction(t, user3, issue1, nil, "heart") | 	addReaction(t, user3, issue, nil, "heart") | ||||||
| 	addReaction(t, user3, issue1, nil, "+1") | 	addReaction(t, user3, issue, nil, "+1") | ||||||
| 	addReaction(t, user4, issue1, nil, "+1") | 	addReaction(t, user4, issue, nil, "+1") | ||||||
| 	addReaction(t, user4, issue1, nil, "heart") | 	addReaction(t, user4, issue, nil, "heart") | ||||||
| 	addReaction(t, ghost, issue1, nil, "-1") | 	addReaction(t, ghost, issue, nil, "-1") | ||||||
|  |  | ||||||
| 	err := issue1.loadReactions(x) | 	err := issue.loadReactions(x) | ||||||
| 	assert.NoError(t, err) | 	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.Len(t, reactions["heart"], 4) | ||||||
| 	assert.Equal(t, 2, reactions["heart"].GetMoreUserCount()) | 	assert.Equal(t, 2, reactions["heart"].GetMoreUserCount()) | ||||||
| 	assert.Equal(t, user1.DisplayName()+", "+user2.DisplayName(), reactions["heart"].GetFirstUsers()) | 	assert.Equal(t, user1.DisplayName()+", "+user2.DisplayName(), reactions["heart"].GetFirstUsers()) | ||||||
|   | |||||||
| @@ -171,6 +171,7 @@ var ( | |||||||
| 		DefaultTheme          string | 		DefaultTheme          string | ||||||
| 		Themes                []string | 		Themes                []string | ||||||
| 		Reactions             []string | 		Reactions             []string | ||||||
|  | 		ReactionsMap          map[string]bool | ||||||
| 		SearchRepoDescription bool | 		SearchRepoDescription bool | ||||||
| 		UseServiceWorker      bool | 		UseServiceWorker      bool | ||||||
|  |  | ||||||
| @@ -985,6 +986,11 @@ func NewContext() { | |||||||
| 	U2F.AppID = sec.Key("APP_ID").MustString(strings.TrimRight(AppURL, "/")) | 	U2F.AppID = sec.Key("APP_ID").MustString(strings.TrimRight(AppURL, "/")) | ||||||
|  |  | ||||||
| 	zip.Verbose = false | 	zip.Verbose = false | ||||||
|  |  | ||||||
|  | 	UI.ReactionsMap = make(map[string]bool) | ||||||
|  | 	for _, reaction := range UI.Reactions { | ||||||
|  | 		UI.ReactionsMap[reaction] = true | ||||||
|  | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| func loadInternalToken(sec *ini.Section) string { | func loadInternalToken(sec *ini.Section) string { | ||||||
|   | |||||||
							
								
								
									
										22
									
								
								modules/structs/issue_reaction.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								modules/structs/issue_reaction.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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) | 						Post(reqToken(), mustNotBeArchived, bind(api.CreateIssueOption{}), repo.CreateIssue) | ||||||
| 					m.Group("/comments", func() { | 					m.Group("/comments", func() { | ||||||
| 						m.Get("", repo.ListRepoIssueComments) | 						m.Get("", repo.ListRepoIssueComments) | ||||||
| 						m.Combo("/:id", reqToken()). | 						m.Group("/:id", func() { | ||||||
| 							Patch(mustNotBeArchived, bind(api.EditIssueCommentOption{}), repo.EditIssueComment). | 							m.Combo("", reqToken()). | ||||||
| 							Delete(repo.DeleteIssueComment) | 								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.Group("/:index", func() { | ||||||
| 						m.Combo("").Get(repo.GetIssue). | 						m.Combo("").Get(repo.GetIssue). | ||||||
| 							Patch(reqToken(), bind(api.EditIssueOption{}), repo.EditIssue) | 							Patch(reqToken(), bind(api.EditIssueOption{}), repo.EditIssue) | ||||||
|  |  | ||||||
| 						m.Group("/comments", func() { | 						m.Group("/comments", func() { | ||||||
| 							m.Combo("").Get(repo.ListIssueComments). | 							m.Combo("").Get(repo.ListIssueComments). | ||||||
| 								Post(reqToken(), mustNotBeArchived, bind(api.CreateIssueCommentOption{}), repo.CreateIssueComment) | 								Post(reqToken(), mustNotBeArchived, bind(api.CreateIssueCommentOption{}), repo.CreateIssueComment) | ||||||
| 							m.Combo("/:id", reqToken()).Patch(bind(api.EditIssueCommentOption{}), repo.EditIssueCommentDeprecated). | 							m.Combo("/:id", reqToken()).Patch(bind(api.EditIssueCommentOption{}), repo.EditIssueCommentDeprecated). | ||||||
| 								Delete(repo.DeleteIssueCommentDeprecated) | 								Delete(repo.DeleteIssueCommentDeprecated) | ||||||
| 						}) | 						}) | ||||||
|  |  | ||||||
| 						m.Group("/labels", func() { | 						m.Group("/labels", func() { | ||||||
| 							m.Combo("").Get(repo.ListIssueLabels). | 							m.Combo("").Get(repo.ListIssueLabels). | ||||||
| 								Post(reqToken(), bind(api.IssueLabelsOption{}), repo.AddIssueLabels). | 								Post(reqToken(), bind(api.IssueLabelsOption{}), repo.AddIssueLabels). | ||||||
| @@ -679,12 +683,10 @@ func RegisterRoutes(m *macaron.Macaron) { | |||||||
| 								Delete(reqToken(), repo.ClearIssueLabels) | 								Delete(reqToken(), repo.ClearIssueLabels) | ||||||
| 							m.Delete("/:id", reqToken(), repo.DeleteIssueLabel) | 							m.Delete("/:id", reqToken(), repo.DeleteIssueLabel) | ||||||
| 						}) | 						}) | ||||||
|  |  | ||||||
| 						m.Group("/times", func() { | 						m.Group("/times", func() { | ||||||
| 							m.Combo("").Get(repo.ListTrackedTimes). | 							m.Combo("").Get(repo.ListTrackedTimes). | ||||||
| 								Post(reqToken(), bind(api.AddTimeOption{}), repo.AddTime) | 								Post(reqToken(), bind(api.AddTimeOption{}), repo.AddTime) | ||||||
| 						}) | 						}) | ||||||
|  |  | ||||||
| 						m.Combo("/deadline").Post(reqToken(), bind(api.EditDeadlineOption{}), repo.UpdateIssueDeadline) | 						m.Combo("/deadline").Post(reqToken(), bind(api.EditDeadlineOption{}), repo.UpdateIssueDeadline) | ||||||
| 						m.Group("/stopwatch", func() { | 						m.Group("/stopwatch", func() { | ||||||
| 							m.Post("/start", reqToken(), repo.StartIssueStopwatch) | 							m.Post("/start", reqToken(), repo.StartIssueStopwatch) | ||||||
| @@ -695,6 +697,10 @@ func RegisterRoutes(m *macaron.Macaron) { | |||||||
| 							m.Put("/:user", reqToken(), repo.AddIssueSubscription) | 							m.Put("/:user", reqToken(), repo.AddIssueSubscription) | ||||||
| 							m.Delete("/:user", reqToken(), repo.DelIssueSubscription) | 							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) | 				}, mustEnableIssuesOrPulls) | ||||||
| 				m.Group("/labels", func() { | 				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 | 	// in:body | ||||||
| 	Body api.IssueDeadline `json:"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") { | 	switch ctx.Params(":action") { | ||||||
| 	case "react": | 	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) | 		reaction, err := models.CreateIssueReaction(ctx.User, issue, form.Content) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
|  | 			if models.IsErrForbiddenIssueReaction(err) { | ||||||
|  | 				ctx.ServerError("ChangeIssueReaction", err) | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
| 			log.Info("CreateIssueReaction: %s", err) | 			log.Info("CreateIssueReaction: %s", err) | ||||||
| 			break | 			break | ||||||
| 		} | 		} | ||||||
| @@ -1564,14 +1562,12 @@ func ChangeCommentReaction(ctx *context.Context, form auth.ReactionForm) { | |||||||
|  |  | ||||||
| 	switch ctx.Params(":action") { | 	switch ctx.Params(":action") { | ||||||
| 	case "react": | 	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) | 		reaction, err := models.CreateCommentReaction(ctx.User, comment.Issue, comment, form.Content) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
|  | 			if models.IsErrForbiddenIssueReaction(err) { | ||||||
|  | 				ctx.ServerError("ChangeIssueReaction", err) | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
| 			log.Info("CreateCommentReaction: %s", err) | 			log.Info("CreateCommentReaction: %s", err) | ||||||
| 			break | 			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": { |     "/repos/{owner}/{repo}/issues/{id}/times": { | ||||||
|       "get": { |       "get": { | ||||||
|         "produces": [ |         "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": { |     "/repos/{owner}/{repo}/issues/{index}/stopwatch/start": { | ||||||
|       "post": { |       "post": { | ||||||
|         "consumes": [ |         "consumes": [ | ||||||
| @@ -8721,6 +9005,17 @@ | |||||||
|       }, |       }, | ||||||
|       "x-go-package": "code.gitea.io/gitea/modules/structs" |       "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": { |     "EditReleaseOption": { | ||||||
|       "description": "EditReleaseOption options when editing a release", |       "description": "EditReleaseOption options when editing a release", | ||||||
|       "type": "object", |       "type": "object", | ||||||
| @@ -10095,6 +10390,25 @@ | |||||||
|       }, |       }, | ||||||
|       "x-go-package": "code.gitea.io/gitea/modules/structs" |       "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": { |     "Reference": { | ||||||
|       "type": "object", |       "type": "object", | ||||||
|       "title": "Reference represents a Git reference.", |       "title": "Reference represents a Git reference.", | ||||||
| @@ -10960,6 +11274,12 @@ | |||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "EditReactionOption": { | ||||||
|  |       "description": "EditReactionOption", | ||||||
|  |       "schema": { | ||||||
|  |         "$ref": "#/definitions/EditReactionOption" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "EmailList": { |     "EmailList": { | ||||||
|       "description": "EmailList", |       "description": "EmailList", | ||||||
|       "schema": { |       "schema": { | ||||||
| @@ -11146,6 +11466,21 @@ | |||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "ReactionResponse": { | ||||||
|  |       "description": "ReactionResponse", | ||||||
|  |       "schema": { | ||||||
|  |         "$ref": "#/definitions/ReactionResponse" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     "ReactionResponseList": { | ||||||
|  |       "description": "ReactionResponseList", | ||||||
|  |       "schema": { | ||||||
|  |         "type": "array", | ||||||
|  |         "items": { | ||||||
|  |           "$ref": "#/definitions/ReactionResponse" | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "Reference": { |     "Reference": { | ||||||
|       "description": "Reference", |       "description": "Reference", | ||||||
|       "schema": { |       "schema": { | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user