code.gitea.io/gitea@v1.22.3/routers/api/v1/repo/issue_reaction.go (about)

     1  // Copyright 2019 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package repo
     5  
     6  import (
     7  	"errors"
     8  	"net/http"
     9  
    10  	issues_model "code.gitea.io/gitea/models/issues"
    11  	user_model "code.gitea.io/gitea/models/user"
    12  	api "code.gitea.io/gitea/modules/structs"
    13  	"code.gitea.io/gitea/modules/web"
    14  	"code.gitea.io/gitea/routers/api/v1/utils"
    15  	"code.gitea.io/gitea/services/context"
    16  	"code.gitea.io/gitea/services/convert"
    17  	issue_service "code.gitea.io/gitea/services/issue"
    18  )
    19  
    20  // GetIssueCommentReactions list reactions of a comment from an issue
    21  func GetIssueCommentReactions(ctx *context.APIContext) {
    22  	// swagger:operation GET /repos/{owner}/{repo}/issues/comments/{id}/reactions issue issueGetCommentReactions
    23  	// ---
    24  	// summary: Get a list of reactions from a comment of an issue
    25  	// consumes:
    26  	// - application/json
    27  	// produces:
    28  	// - application/json
    29  	// parameters:
    30  	// - name: owner
    31  	//   in: path
    32  	//   description: owner of the repo
    33  	//   type: string
    34  	//   required: true
    35  	// - name: repo
    36  	//   in: path
    37  	//   description: name of the repo
    38  	//   type: string
    39  	//   required: true
    40  	// - name: id
    41  	//   in: path
    42  	//   description: id of the comment to edit
    43  	//   type: integer
    44  	//   format: int64
    45  	//   required: true
    46  	// responses:
    47  	//   "200":
    48  	//     "$ref": "#/responses/ReactionList"
    49  	//   "403":
    50  	//     "$ref": "#/responses/forbidden"
    51  	//   "404":
    52  	//     "$ref": "#/responses/notFound"
    53  
    54  	comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(":id"))
    55  	if err != nil {
    56  		if issues_model.IsErrCommentNotExist(err) {
    57  			ctx.NotFound(err)
    58  		} else {
    59  			ctx.Error(http.StatusInternalServerError, "GetCommentByID", err)
    60  		}
    61  		return
    62  	}
    63  
    64  	if err := comment.LoadIssue(ctx); err != nil {
    65  		ctx.Error(http.StatusInternalServerError, "comment.LoadIssue", err)
    66  		return
    67  	}
    68  
    69  	if comment.Issue.RepoID != ctx.Repo.Repository.ID {
    70  		ctx.NotFound()
    71  		return
    72  	}
    73  
    74  	if !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull) {
    75  		ctx.Error(http.StatusForbidden, "GetIssueCommentReactions", errors.New("no permission to get reactions"))
    76  		return
    77  	}
    78  
    79  	reactions, _, err := issues_model.FindCommentReactions(ctx, comment.IssueID, comment.ID)
    80  	if err != nil {
    81  		ctx.Error(http.StatusInternalServerError, "FindCommentReactions", err)
    82  		return
    83  	}
    84  	_, err = reactions.LoadUsers(ctx, ctx.Repo.Repository)
    85  	if err != nil {
    86  		ctx.Error(http.StatusInternalServerError, "ReactionList.LoadUsers()", err)
    87  		return
    88  	}
    89  
    90  	var result []api.Reaction
    91  	for _, r := range reactions {
    92  		result = append(result, api.Reaction{
    93  			User:     convert.ToUser(ctx, r.User, ctx.Doer),
    94  			Reaction: r.Type,
    95  			Created:  r.CreatedUnix.AsTime(),
    96  		})
    97  	}
    98  
    99  	ctx.JSON(http.StatusOK, result)
   100  }
   101  
   102  // PostIssueCommentReaction add a reaction to a comment of an issue
   103  func PostIssueCommentReaction(ctx *context.APIContext) {
   104  	// swagger:operation POST /repos/{owner}/{repo}/issues/comments/{id}/reactions issue issuePostCommentReaction
   105  	// ---
   106  	// summary: Add a reaction to a comment of an issue
   107  	// consumes:
   108  	// - application/json
   109  	// produces:
   110  	// - application/json
   111  	// parameters:
   112  	// - name: owner
   113  	//   in: path
   114  	//   description: owner of the repo
   115  	//   type: string
   116  	//   required: true
   117  	// - name: repo
   118  	//   in: path
   119  	//   description: name of the repo
   120  	//   type: string
   121  	//   required: true
   122  	// - name: id
   123  	//   in: path
   124  	//   description: id of the comment to edit
   125  	//   type: integer
   126  	//   format: int64
   127  	//   required: true
   128  	// - name: content
   129  	//   in: body
   130  	//   schema:
   131  	//     "$ref": "#/definitions/EditReactionOption"
   132  	// responses:
   133  	//   "200":
   134  	//     "$ref": "#/responses/Reaction"
   135  	//   "201":
   136  	//     "$ref": "#/responses/Reaction"
   137  	//   "403":
   138  	//     "$ref": "#/responses/forbidden"
   139  	//   "404":
   140  	//     "$ref": "#/responses/notFound"
   141  
   142  	form := web.GetForm(ctx).(*api.EditReactionOption)
   143  
   144  	changeIssueCommentReaction(ctx, *form, true)
   145  }
   146  
   147  // DeleteIssueCommentReaction remove a reaction from a comment of an issue
   148  func DeleteIssueCommentReaction(ctx *context.APIContext) {
   149  	// swagger:operation DELETE /repos/{owner}/{repo}/issues/comments/{id}/reactions issue issueDeleteCommentReaction
   150  	// ---
   151  	// summary: Remove a reaction from a comment of an issue
   152  	// consumes:
   153  	// - application/json
   154  	// produces:
   155  	// - application/json
   156  	// parameters:
   157  	// - name: owner
   158  	//   in: path
   159  	//   description: owner of the repo
   160  	//   type: string
   161  	//   required: true
   162  	// - name: repo
   163  	//   in: path
   164  	//   description: name of the repo
   165  	//   type: string
   166  	//   required: true
   167  	// - name: id
   168  	//   in: path
   169  	//   description: id of the comment to edit
   170  	//   type: integer
   171  	//   format: int64
   172  	//   required: true
   173  	// - name: content
   174  	//   in: body
   175  	//   schema:
   176  	//     "$ref": "#/definitions/EditReactionOption"
   177  	// responses:
   178  	//   "200":
   179  	//     "$ref": "#/responses/empty"
   180  	//   "403":
   181  	//     "$ref": "#/responses/forbidden"
   182  	//   "404":
   183  	//     "$ref": "#/responses/notFound"
   184  
   185  	form := web.GetForm(ctx).(*api.EditReactionOption)
   186  
   187  	changeIssueCommentReaction(ctx, *form, false)
   188  }
   189  
   190  func changeIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOption, isCreateType bool) {
   191  	comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(":id"))
   192  	if err != nil {
   193  		if issues_model.IsErrCommentNotExist(err) {
   194  			ctx.NotFound(err)
   195  		} else {
   196  			ctx.Error(http.StatusInternalServerError, "GetCommentByID", err)
   197  		}
   198  		return
   199  	}
   200  
   201  	if err = comment.LoadIssue(ctx); err != nil {
   202  		ctx.Error(http.StatusInternalServerError, "comment.LoadIssue() failed", err)
   203  		return
   204  	}
   205  
   206  	if comment.Issue.RepoID != ctx.Repo.Repository.ID {
   207  		ctx.NotFound()
   208  		return
   209  	}
   210  
   211  	if !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull) {
   212  		ctx.NotFound()
   213  		return
   214  	}
   215  
   216  	if comment.Issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull) {
   217  		ctx.Error(http.StatusForbidden, "ChangeIssueCommentReaction", errors.New("no permission to change reaction"))
   218  		return
   219  	}
   220  
   221  	if isCreateType {
   222  		// PostIssueCommentReaction part
   223  		reaction, err := issue_service.CreateCommentReaction(ctx, ctx.Doer, comment, form.Reaction)
   224  		if err != nil {
   225  			if issues_model.IsErrForbiddenIssueReaction(err) || errors.Is(err, user_model.ErrBlockedUser) {
   226  				ctx.Error(http.StatusForbidden, err.Error(), err)
   227  			} else if issues_model.IsErrReactionAlreadyExist(err) {
   228  				ctx.JSON(http.StatusOK, api.Reaction{
   229  					User:     convert.ToUser(ctx, ctx.Doer, ctx.Doer),
   230  					Reaction: reaction.Type,
   231  					Created:  reaction.CreatedUnix.AsTime(),
   232  				})
   233  			} else {
   234  				ctx.Error(http.StatusInternalServerError, "CreateCommentReaction", err)
   235  			}
   236  			return
   237  		}
   238  
   239  		ctx.JSON(http.StatusCreated, api.Reaction{
   240  			User:     convert.ToUser(ctx, ctx.Doer, ctx.Doer),
   241  			Reaction: reaction.Type,
   242  			Created:  reaction.CreatedUnix.AsTime(),
   243  		})
   244  	} else {
   245  		// DeleteIssueCommentReaction part
   246  		err = issues_model.DeleteCommentReaction(ctx, ctx.Doer.ID, comment.Issue.ID, comment.ID, form.Reaction)
   247  		if err != nil {
   248  			ctx.Error(http.StatusInternalServerError, "DeleteCommentReaction", err)
   249  			return
   250  		}
   251  		// ToDo respond 204
   252  		ctx.Status(http.StatusOK)
   253  	}
   254  }
   255  
   256  // GetIssueReactions list reactions of an issue
   257  func GetIssueReactions(ctx *context.APIContext) {
   258  	// swagger:operation GET /repos/{owner}/{repo}/issues/{index}/reactions issue issueGetIssueReactions
   259  	// ---
   260  	// summary: Get a list reactions of an issue
   261  	// consumes:
   262  	// - application/json
   263  	// produces:
   264  	// - application/json
   265  	// parameters:
   266  	// - name: owner
   267  	//   in: path
   268  	//   description: owner of the repo
   269  	//   type: string
   270  	//   required: true
   271  	// - name: repo
   272  	//   in: path
   273  	//   description: name of the repo
   274  	//   type: string
   275  	//   required: true
   276  	// - name: index
   277  	//   in: path
   278  	//   description: index of the issue
   279  	//   type: integer
   280  	//   format: int64
   281  	//   required: true
   282  	// - name: page
   283  	//   in: query
   284  	//   description: page number of results to return (1-based)
   285  	//   type: integer
   286  	// - name: limit
   287  	//   in: query
   288  	//   description: page size of results
   289  	//   type: integer
   290  	// responses:
   291  	//   "200":
   292  	//     "$ref": "#/responses/ReactionList"
   293  	//   "403":
   294  	//     "$ref": "#/responses/forbidden"
   295  	//   "404":
   296  	//     "$ref": "#/responses/notFound"
   297  
   298  	issue, err := issues_model.GetIssueWithAttrsByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
   299  	if err != nil {
   300  		if issues_model.IsErrIssueNotExist(err) {
   301  			ctx.NotFound()
   302  		} else {
   303  			ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
   304  		}
   305  		return
   306  	}
   307  
   308  	if !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull) {
   309  		ctx.Error(http.StatusForbidden, "GetIssueReactions", errors.New("no permission to get reactions"))
   310  		return
   311  	}
   312  
   313  	reactions, count, err := issues_model.FindIssueReactions(ctx, issue.ID, utils.GetListOptions(ctx))
   314  	if err != nil {
   315  		ctx.Error(http.StatusInternalServerError, "FindIssueReactions", err)
   316  		return
   317  	}
   318  	_, err = reactions.LoadUsers(ctx, ctx.Repo.Repository)
   319  	if err != nil {
   320  		ctx.Error(http.StatusInternalServerError, "ReactionList.LoadUsers()", err)
   321  		return
   322  	}
   323  
   324  	var result []api.Reaction
   325  	for _, r := range reactions {
   326  		result = append(result, api.Reaction{
   327  			User:     convert.ToUser(ctx, r.User, ctx.Doer),
   328  			Reaction: r.Type,
   329  			Created:  r.CreatedUnix.AsTime(),
   330  		})
   331  	}
   332  
   333  	ctx.SetTotalCountHeader(count)
   334  	ctx.JSON(http.StatusOK, result)
   335  }
   336  
   337  // PostIssueReaction add a reaction to an issue
   338  func PostIssueReaction(ctx *context.APIContext) {
   339  	// swagger:operation POST /repos/{owner}/{repo}/issues/{index}/reactions issue issuePostIssueReaction
   340  	// ---
   341  	// summary: Add a reaction to an issue
   342  	// consumes:
   343  	// - application/json
   344  	// produces:
   345  	// - application/json
   346  	// parameters:
   347  	// - name: owner
   348  	//   in: path
   349  	//   description: owner of the repo
   350  	//   type: string
   351  	//   required: true
   352  	// - name: repo
   353  	//   in: path
   354  	//   description: name of the repo
   355  	//   type: string
   356  	//   required: true
   357  	// - name: index
   358  	//   in: path
   359  	//   description: index of the issue
   360  	//   type: integer
   361  	//   format: int64
   362  	//   required: true
   363  	// - name: content
   364  	//   in: body
   365  	//   schema:
   366  	//     "$ref": "#/definitions/EditReactionOption"
   367  	// responses:
   368  	//   "200":
   369  	//     "$ref": "#/responses/Reaction"
   370  	//   "201":
   371  	//     "$ref": "#/responses/Reaction"
   372  	//   "403":
   373  	//     "$ref": "#/responses/forbidden"
   374  	//   "404":
   375  	//     "$ref": "#/responses/notFound"
   376  	form := web.GetForm(ctx).(*api.EditReactionOption)
   377  	changeIssueReaction(ctx, *form, true)
   378  }
   379  
   380  // DeleteIssueReaction remove a reaction from an issue
   381  func DeleteIssueReaction(ctx *context.APIContext) {
   382  	// swagger:operation DELETE /repos/{owner}/{repo}/issues/{index}/reactions issue issueDeleteIssueReaction
   383  	// ---
   384  	// summary: Remove a reaction from an issue
   385  	// consumes:
   386  	// - application/json
   387  	// produces:
   388  	// - application/json
   389  	// parameters:
   390  	// - name: owner
   391  	//   in: path
   392  	//   description: owner of the repo
   393  	//   type: string
   394  	//   required: true
   395  	// - name: repo
   396  	//   in: path
   397  	//   description: name of the repo
   398  	//   type: string
   399  	//   required: true
   400  	// - name: index
   401  	//   in: path
   402  	//   description: index of the issue
   403  	//   type: integer
   404  	//   format: int64
   405  	//   required: true
   406  	// - name: content
   407  	//   in: body
   408  	//   schema:
   409  	//     "$ref": "#/definitions/EditReactionOption"
   410  	// responses:
   411  	//   "200":
   412  	//     "$ref": "#/responses/empty"
   413  	//   "403":
   414  	//     "$ref": "#/responses/forbidden"
   415  	//   "404":
   416  	//     "$ref": "#/responses/notFound"
   417  	form := web.GetForm(ctx).(*api.EditReactionOption)
   418  	changeIssueReaction(ctx, *form, false)
   419  }
   420  
   421  func changeIssueReaction(ctx *context.APIContext, form api.EditReactionOption, isCreateType bool) {
   422  	issue, err := issues_model.GetIssueWithAttrsByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
   423  	if err != nil {
   424  		if issues_model.IsErrIssueNotExist(err) {
   425  			ctx.NotFound()
   426  		} else {
   427  			ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
   428  		}
   429  		return
   430  	}
   431  
   432  	if issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) {
   433  		ctx.Error(http.StatusForbidden, "ChangeIssueCommentReaction", errors.New("no permission to change reaction"))
   434  		return
   435  	}
   436  
   437  	if isCreateType {
   438  		// PostIssueReaction part
   439  		reaction, err := issue_service.CreateIssueReaction(ctx, ctx.Doer, issue, form.Reaction)
   440  		if err != nil {
   441  			if issues_model.IsErrForbiddenIssueReaction(err) || errors.Is(err, user_model.ErrBlockedUser) {
   442  				ctx.Error(http.StatusForbidden, err.Error(), err)
   443  			} else if issues_model.IsErrReactionAlreadyExist(err) {
   444  				ctx.JSON(http.StatusOK, api.Reaction{
   445  					User:     convert.ToUser(ctx, ctx.Doer, ctx.Doer),
   446  					Reaction: reaction.Type,
   447  					Created:  reaction.CreatedUnix.AsTime(),
   448  				})
   449  			} else {
   450  				ctx.Error(http.StatusInternalServerError, "CreateIssueReaction", err)
   451  			}
   452  			return
   453  		}
   454  
   455  		ctx.JSON(http.StatusCreated, api.Reaction{
   456  			User:     convert.ToUser(ctx, ctx.Doer, ctx.Doer),
   457  			Reaction: reaction.Type,
   458  			Created:  reaction.CreatedUnix.AsTime(),
   459  		})
   460  	} else {
   461  		// DeleteIssueReaction part
   462  		err = issues_model.DeleteIssueReaction(ctx, ctx.Doer.ID, issue.ID, form.Reaction)
   463  		if err != nil {
   464  			ctx.Error(http.StatusInternalServerError, "DeleteIssueReaction", err)
   465  			return
   466  		}
   467  		// ToDo respond 204
   468  		ctx.Status(http.StatusOK)
   469  	}
   470  }