2017-12-04 01:14:26 +02:00
// Copyright 2017 The Gitea Authors. All rights reserved.
2022-11-27 13:20:29 -05:00
// SPDX-License-Identifier: MIT
2017-12-04 01:14:26 +02:00
2022-03-31 17:20:39 +08:00
package issues
2017-12-04 01:14:26 +02:00
import (
"bytes"
2022-03-31 17:20:39 +08:00
"context"
2017-12-04 01:14:26 +02:00
"fmt"
2021-09-19 19:49:59 +08:00
"code.gitea.io/gitea/models/db"
2021-12-10 09:27:50 +08:00
repo_model "code.gitea.io/gitea/models/repo"
2021-11-24 17:49:20 +08:00
user_model "code.gitea.io/gitea/models/user"
2022-03-31 17:20:39 +08:00
"code.gitea.io/gitea/modules/container"
2017-12-05 22:57:01 +02:00
"code.gitea.io/gitea/modules/setting"
2019-08-15 22:46:21 +08:00
"code.gitea.io/gitea/modules/timeutil"
2022-10-18 06:50:37 +01:00
"code.gitea.io/gitea/modules/util"
2017-12-05 22:57:01 +02:00
2019-06-23 23:22:43 +08:00
"xorm.io/builder"
2017-12-04 01:14:26 +02:00
)
2022-03-31 17:20:39 +08:00
// 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 )
}
2022-10-18 06:50:37 +01:00
func ( err ErrForbiddenIssueReaction ) Unwrap ( ) error {
return util . ErrPermissionDenied
}
2022-03-31 17:20:39 +08:00
// ErrReactionAlreadyExist is used when a existing reaction was try to created
type ErrReactionAlreadyExist struct {
Reaction string
}
// IsErrReactionAlreadyExist checks if an error is a ErrReactionAlreadyExist.
func IsErrReactionAlreadyExist ( err error ) bool {
_ , ok := err . ( ErrReactionAlreadyExist )
return ok
}
func ( err ErrReactionAlreadyExist ) Error ( ) string {
return fmt . Sprintf ( "reaction '%s' already exists" , err . Reaction )
}
2022-10-18 06:50:37 +01:00
func ( err ErrReactionAlreadyExist ) Unwrap ( ) error {
return util . ErrAlreadyExist
}
2017-12-04 01:14:26 +02:00
// Reaction represents a reactions on issues and comments.
type Reaction struct {
2020-11-10 22:37:11 +00:00
ID int64 ` xorm:"pk autoincr" `
Type string ` xorm:"INDEX UNIQUE(s) NOT NULL" `
IssueID int64 ` xorm:"INDEX UNIQUE(s) NOT NULL" `
CommentID int64 ` xorm:"INDEX UNIQUE(s)" `
UserID int64 ` xorm:"INDEX UNIQUE(s) NOT NULL" `
OriginalAuthorID int64 ` xorm:"INDEX UNIQUE(s) NOT NULL DEFAULT(0)" `
OriginalAuthor string ` xorm:"INDEX UNIQUE(s)" `
2021-11-24 17:49:20 +08:00
User * user_model . User ` xorm:"-" `
2020-01-15 19:14:07 +08:00
CreatedUnix timeutil . TimeStamp ` xorm:"INDEX created" `
2017-12-04 01:14:26 +02:00
}
2022-03-31 17:20:39 +08:00
// LoadUser load user of reaction
2023-09-25 15:17:37 +02:00
func ( r * Reaction ) LoadUser ( ctx context . Context ) ( * user_model . User , error ) {
2022-03-31 17:20:39 +08:00
if r . User != nil {
return r . User , nil
}
2023-09-25 15:17:37 +02:00
user , err := user_model . GetUserByID ( ctx , r . UserID )
2022-03-31 17:20:39 +08:00
if err != nil {
return nil , err
}
r . User = user
return user , nil
}
// RemapExternalUser ExternalUserRemappable interface
func ( r * Reaction ) RemapExternalUser ( externalName string , externalID , userID int64 ) error {
r . OriginalAuthor = externalName
r . OriginalAuthorID = externalID
r . UserID = userID
return nil
}
// GetUserID ExternalUserRemappable interface
func ( r * Reaction ) GetUserID ( ) int64 { return r . UserID }
// GetExternalName ExternalUserRemappable interface
func ( r * Reaction ) GetExternalName ( ) string { return r . OriginalAuthor }
// GetExternalID ExternalUserRemappable interface
func ( r * Reaction ) GetExternalID ( ) int64 { return r . OriginalAuthorID }
2021-09-19 19:49:59 +08:00
func init ( ) {
db . RegisterModel ( new ( Reaction ) )
}
2017-12-04 01:14:26 +02:00
// FindReactionsOptions describes the conditions to Find reactions
type FindReactionsOptions struct {
2021-09-24 19:32:56 +08:00
db . ListOptions
2017-12-04 01:14:26 +02:00
IssueID int64
CommentID int64
2019-12-31 09:21:21 +01:00
UserID int64
Reaction string
2017-12-04 01:14:26 +02:00
}
func ( opts * FindReactionsOptions ) toConds ( ) builder . Cond {
2021-03-15 02:52:12 +08:00
// If Issue ID is set add to Query
cond := builder . NewCond ( )
2017-12-04 01:14:26 +02:00
if opts . IssueID > 0 {
cond = cond . And ( builder . Eq { "reaction.issue_id" : opts . IssueID } )
}
2021-03-15 02:52:12 +08:00
// 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
2017-12-04 01:14:26 +02:00
if opts . CommentID > 0 {
cond = cond . And ( builder . Eq { "reaction.comment_id" : opts . CommentID } )
2019-12-07 23:04:19 +01:00
} else if opts . CommentID == - 1 {
cond = cond . And ( builder . Eq { "reaction.comment_id" : 0 } )
2017-12-04 01:14:26 +02:00
}
2019-12-31 09:21:21 +01:00
if opts . UserID > 0 {
2020-01-15 19:14:07 +08:00
cond = cond . And ( builder . Eq {
"reaction.user_id" : opts . UserID ,
"reaction.original_author_id" : 0 ,
} )
2019-12-31 09:21:21 +01:00
}
if opts . Reaction != "" {
cond = cond . And ( builder . Eq { "reaction.type" : opts . Reaction } )
}
2019-12-07 23:04:19 +01:00
2017-12-04 01:14:26 +02:00
return cond
}
2019-12-07 23:04:19 +01:00
// FindCommentReactions returns a ReactionList of all reactions from an comment
2023-09-25 15:17:37 +02:00
func FindCommentReactions ( ctx context . Context , issueID , commentID int64 ) ( ReactionList , int64 , error ) {
return FindReactions ( ctx , FindReactionsOptions {
2022-03-31 17:20:39 +08:00
IssueID : issueID ,
CommentID : commentID ,
2021-03-15 02:52:12 +08:00
} )
2019-12-07 23:04:19 +01:00
}
// FindIssueReactions returns a ReactionList of all reactions from an issue
2023-09-25 15:17:37 +02:00
func FindIssueReactions ( ctx context . Context , issueID int64 , listOptions db . ListOptions ) ( ReactionList , int64 , error ) {
return FindReactions ( ctx , FindReactionsOptions {
2020-01-24 19:00:29 +00:00
ListOptions : listOptions ,
2022-03-31 17:20:39 +08:00
IssueID : issueID ,
2020-01-24 19:00:29 +00:00
CommentID : - 1 ,
2019-12-07 23:04:19 +01:00
} )
}
2022-03-31 17:20:39 +08:00
// FindReactions returns a ReactionList of all reactions from an issue or a comment
func FindReactions ( ctx context . Context , opts FindReactionsOptions ) ( ReactionList , int64 , error ) {
sess := db . GetEngine ( ctx ) .
2020-01-24 19:00:29 +00:00
Where ( opts . toConds ( ) ) .
2019-12-18 14:07:36 +01:00
In ( "reaction.`type`" , setting . UI . Reactions ) .
2020-01-24 19:00:29 +00:00
Asc ( "reaction.issue_id" , "reaction.comment_id" , "reaction.created_unix" , "reaction.id" )
2024-11-24 17:56:50 -08:00
if opts . Page > 0 {
2021-12-15 06:39:34 +01:00
sess = db . SetSessionPagination ( sess , & opts )
2020-01-24 19:00:29 +00:00
reactions := make ( [ ] * Reaction , 0 , opts . PageSize )
2021-12-15 06:39:34 +01:00
count , err := sess . FindAndCount ( & reactions )
return reactions , count , err
2020-01-24 19:00:29 +00:00
}
reactions := make ( [ ] * Reaction , 0 , 10 )
2021-12-15 06:39:34 +01:00
count , err := sess . FindAndCount ( & reactions )
return reactions , count , err
2017-12-04 01:14:26 +02:00
}
2022-03-31 17:20:39 +08:00
func createReaction ( ctx context . Context , opts * ReactionOptions ) ( * Reaction , error ) {
2017-12-04 01:14:26 +02:00
reaction := & Reaction {
2022-03-31 17:20:39 +08:00
Type : opts . Type ,
UserID : opts . DoerID ,
IssueID : opts . IssueID ,
CommentID : opts . CommentID ,
2017-12-04 01:14:26 +02:00
}
2019-12-31 09:21:21 +01:00
findOpts := FindReactionsOptions {
2022-03-31 17:20:39 +08:00
IssueID : opts . IssueID ,
CommentID : opts . CommentID ,
2019-12-31 09:21:21 +01:00
Reaction : opts . Type ,
2022-03-31 17:20:39 +08:00
UserID : opts . DoerID ,
2019-12-31 09:21:21 +01:00
}
2022-09-17 19:54:03 +08:00
if findOpts . CommentID == 0 {
// explicit search of Issue Reactions where CommentID = 0
findOpts . CommentID = - 1
}
2019-12-31 09:21:21 +01:00
2022-03-31 17:20:39 +08:00
existingR , _ , err := FindReactions ( ctx , findOpts )
2019-12-31 09:21:21 +01:00
if err != nil {
return nil , err
}
if len ( existingR ) > 0 {
return existingR [ 0 ] , ErrReactionAlreadyExist { Reaction : opts . Type }
2017-12-04 01:14:26 +02:00
}
2019-12-31 09:21:21 +01:00
2022-03-31 17:20:39 +08:00
if err := db . Insert ( ctx , reaction ) ; err != nil {
2017-12-04 01:14:26 +02:00
return nil , err
}
return reaction , nil
}
// ReactionOptions defines options for creating or deleting reactions
type ReactionOptions struct {
2022-03-31 17:20:39 +08:00
Type string
DoerID int64
IssueID int64
CommentID int64
2017-12-04 01:14:26 +02:00
}
// CreateReaction creates reaction for issue or comment.
2023-09-25 15:17:37 +02:00
func CreateReaction ( ctx context . Context , opts * ReactionOptions ) ( * Reaction , error ) {
2022-10-12 07:18:26 +02:00
if ! setting . UI . ReactionsLookup . Contains ( opts . Type ) {
2019-12-07 23:04:19 +01:00
return nil , ErrForbiddenIssueReaction { opts . Type }
}
2023-09-25 15:17:37 +02:00
ctx , committer , err := db . TxContext ( ctx )
2021-11-21 23:41:00 +08:00
if err != nil {
2017-12-04 01:14:26 +02:00
return nil , err
}
2021-11-21 23:41:00 +08:00
defer committer . Close ( )
2017-12-04 01:14:26 +02:00
2022-03-31 17:20:39 +08:00
reaction , err := createReaction ( ctx , opts )
2017-12-04 01:14:26 +02:00
if err != nil {
2019-12-31 09:21:21 +01:00
return reaction , err
2017-12-04 01:14:26 +02:00
}
2021-11-21 23:41:00 +08:00
if err := committer . Commit ( ) ; err != nil {
2017-12-04 01:14:26 +02:00
return nil , err
}
return reaction , nil
}
// DeleteReaction deletes reaction for issue or comment.
2022-03-31 17:20:39 +08:00
func DeleteReaction ( ctx context . Context , opts * ReactionOptions ) error {
reaction := & Reaction {
Type : opts . Type ,
UserID : opts . DoerID ,
IssueID : opts . IssueID ,
CommentID : opts . CommentID ,
2017-12-04 01:14:26 +02:00
}
2022-09-17 19:54:03 +08:00
sess := db . GetEngine ( ctx ) . Where ( "original_author_id = 0" )
if opts . CommentID == - 1 {
reaction . CommentID = 0
sess . MustCols ( "comment_id" )
}
_ , err := sess . Delete ( reaction )
2022-03-31 17:20:39 +08:00
return err
2017-12-04 01:14:26 +02:00
}
// DeleteIssueReaction deletes a reaction on issue.
2023-09-25 15:17:37 +02:00
func DeleteIssueReaction ( ctx context . Context , doerID , issueID int64 , content string ) error {
return DeleteReaction ( ctx , & ReactionOptions {
2022-09-17 19:54:03 +08:00
Type : content ,
DoerID : doerID ,
IssueID : issueID ,
CommentID : - 1 ,
2017-12-04 01:14:26 +02:00
} )
}
// DeleteCommentReaction deletes a reaction on comment.
2023-09-25 15:17:37 +02:00
func DeleteCommentReaction ( ctx context . Context , doerID , issueID , commentID int64 , content string ) error {
return DeleteReaction ( ctx , & ReactionOptions {
2022-03-31 17:20:39 +08:00
Type : content ,
DoerID : doerID ,
IssueID : issueID ,
CommentID : commentID ,
2017-12-04 01:14:26 +02:00
} )
}
// ReactionList represents list of reactions
type ReactionList [ ] * Reaction
// HasUser check if user has reacted
func ( list ReactionList ) HasUser ( userID int64 ) bool {
if userID == 0 {
return false
}
for _ , reaction := range list {
2020-01-15 19:14:07 +08:00
if reaction . OriginalAuthor == "" && reaction . UserID == userID {
2017-12-04 01:14:26 +02:00
return true
}
}
return false
}
// GroupByType returns reactions grouped by type
func ( list ReactionList ) GroupByType ( ) map [ string ] ReactionList {
2021-03-15 02:52:12 +08:00
reactions := make ( map [ string ] ReactionList )
2017-12-04 01:14:26 +02:00
for _ , reaction := range list {
reactions [ reaction . Type ] = append ( reactions [ reaction . Type ] , reaction )
}
return reactions
}
func ( list ReactionList ) getUserIDs ( ) [ ] int64 {
2024-04-09 14:27:30 +02:00
return container . FilterSlice ( list , func ( reaction * Reaction ) ( int64 , bool ) {
2020-01-15 19:14:07 +08:00
if reaction . OriginalAuthor != "" {
2024-04-09 14:27:30 +02:00
return 0 , false
2020-01-15 19:14:07 +08:00
}
2024-04-09 14:27:30 +02:00
return reaction . UserID , true
} )
2022-03-31 17:20:39 +08:00
}
func valuesUser ( m map [ int64 ] * user_model . User ) [ ] * user_model . User {
values := make ( [ ] * user_model . User , 0 , len ( m ) )
for _ , v := range m {
values = append ( values , v )
}
return values
2017-12-04 01:14:26 +02:00
}
2022-03-31 17:20:39 +08:00
// LoadUsers loads reactions' all users
func ( list ReactionList ) LoadUsers ( ctx context . Context , repo * repo_model . Repository ) ( [ ] * user_model . User , error ) {
2017-12-04 01:14:26 +02:00
if len ( list ) == 0 {
return nil , nil
}
userIDs := list . getUserIDs ( )
2021-11-24 17:49:20 +08:00
userMaps := make ( map [ int64 ] * user_model . User , len ( userIDs ) )
2022-03-31 17:20:39 +08:00
err := db . GetEngine ( ctx ) .
2017-12-04 01:14:26 +02:00
In ( "id" , userIDs ) .
Find ( & userMaps )
if err != nil {
2022-10-24 21:29:17 +02:00
return nil , fmt . Errorf ( "find user: %w" , err )
2017-12-04 01:14:26 +02:00
}
for _ , reaction := range list {
2020-01-15 19:14:07 +08:00
if reaction . OriginalAuthor != "" {
2021-11-24 17:49:20 +08:00
reaction . User = user_model . NewReplaceUser ( fmt . Sprintf ( "%s(%s)" , reaction . OriginalAuthor , repo . OriginalServiceType . Name ( ) ) )
2020-01-15 19:14:07 +08:00
} else if user , ok := userMaps [ reaction . UserID ] ; ok {
2017-12-04 01:14:26 +02:00
reaction . User = user
} else {
2021-11-24 17:49:20 +08:00
reaction . User = user_model . NewGhostUser ( )
2017-12-04 01:14:26 +02:00
}
}
return valuesUser ( userMaps ) , nil
}
// GetFirstUsers returns first reacted user display names separated by comma
func ( list ReactionList ) GetFirstUsers ( ) string {
var buffer bytes . Buffer
2021-03-15 02:52:12 +08:00
rem := setting . UI . ReactionMaxUserNum
2017-12-04 01:14:26 +02:00
for _ , reaction := range list {
if buffer . Len ( ) > 0 {
buffer . WriteString ( ", " )
}
2023-08-10 21:20:01 +00:00
buffer . WriteString ( reaction . User . Name )
2017-12-04 01:14:26 +02:00
if rem -- ; rem == 0 {
break
}
}
return buffer . String ( )
}
// GetMoreUserCount returns count of not shown users in reaction tooltip
func ( list ReactionList ) GetMoreUserCount ( ) int {
if len ( list ) <= setting . UI . ReactionMaxUserNum {
return 0
}
return len ( list ) - setting . UI . ReactionMaxUserNum
}