You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
378 lines
10 KiB
378 lines
10 KiB
7 years ago
|
// Copyright 2017 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.
|
||
|
|
||
3 years ago
|
package issues
|
||
7 years ago
|
|
||
|
import (
|
||
|
"bytes"
|
||
3 years ago
|
"context"
|
||
7 years ago
|
"fmt"
|
||
|
|
||
3 years ago
|
"code.gitea.io/gitea/models/db"
|
||
3 years ago
|
repo_model "code.gitea.io/gitea/models/repo"
|
||
3 years ago
|
user_model "code.gitea.io/gitea/models/user"
|
||
3 years ago
|
"code.gitea.io/gitea/modules/container"
|
||
7 years ago
|
"code.gitea.io/gitea/modules/setting"
|
||
5 years ago
|
"code.gitea.io/gitea/modules/timeutil"
|
||
7 years ago
|
|
||
6 years ago
|
"xorm.io/builder"
|
||
7 years ago
|
)
|
||
|
|
||
3 years ago
|
// 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)
|
||
|
}
|
||
|
|
||
|
// 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)
|
||
|
}
|
||
|
|
||
7 years ago
|
// Reaction represents a reactions on issues and comments.
|
||
|
type Reaction struct {
|
||
4 years ago
|
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)"`
|
||
3 years ago
|
User *user_model.User `xorm:"-"`
|
||
5 years ago
|
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
|
||
7 years ago
|
}
|
||
|
|
||
3 years ago
|
// LoadUser load user of reaction
|
||
|
func (r *Reaction) LoadUser() (*user_model.User, error) {
|
||
|
if r.User != nil {
|
||
|
return r.User, nil
|
||
|
}
|
||
|
user, err := user_model.GetUserByIDCtx(db.DefaultContext, r.UserID)
|
||
|
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 }
|
||
|
|
||
3 years ago
|
func init() {
|
||
|
db.RegisterModel(new(Reaction))
|
||
|
}
|
||
|
|
||
7 years ago
|
// FindReactionsOptions describes the conditions to Find reactions
|
||
|
type FindReactionsOptions struct {
|
||
3 years ago
|
db.ListOptions
|
||
7 years ago
|
IssueID int64
|
||
|
CommentID int64
|
||
5 years ago
|
UserID int64
|
||
|
Reaction string
|
||
7 years ago
|
}
|
||
|
|
||
|
func (opts *FindReactionsOptions) toConds() builder.Cond {
|
||
4 years ago
|
// If Issue ID is set add to Query
|
||
|
cond := builder.NewCond()
|
||
7 years ago
|
if opts.IssueID > 0 {
|
||
|
cond = cond.And(builder.Eq{"reaction.issue_id": opts.IssueID})
|
||
|
}
|
||
4 years ago
|
// 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
|
||
7 years ago
|
if opts.CommentID > 0 {
|
||
|
cond = cond.And(builder.Eq{"reaction.comment_id": opts.CommentID})
|
||
5 years ago
|
} else if opts.CommentID == -1 {
|
||
|
cond = cond.And(builder.Eq{"reaction.comment_id": 0})
|
||
7 years ago
|
}
|
||
5 years ago
|
if opts.UserID > 0 {
|
||
5 years ago
|
cond = cond.And(builder.Eq{
|
||
|
"reaction.user_id": opts.UserID,
|
||
|
"reaction.original_author_id": 0,
|
||
|
})
|
||
5 years ago
|
}
|
||
|
if opts.Reaction != "" {
|
||
|
cond = cond.And(builder.Eq{"reaction.type": opts.Reaction})
|
||
|
}
|
||
5 years ago
|
|
||
7 years ago
|
return cond
|
||
|
}
|
||
|
|
||
5 years ago
|
// FindCommentReactions returns a ReactionList of all reactions from an comment
|
||
3 years ago
|
func FindCommentReactions(issueID, commentID int64) (ReactionList, int64, error) {
|
||
|
return FindReactions(db.DefaultContext, FindReactionsOptions{
|
||
|
IssueID: issueID,
|
||
|
CommentID: commentID,
|
||
4 years ago
|
})
|
||
5 years ago
|
}
|
||
|
|
||
|
// FindIssueReactions returns a ReactionList of all reactions from an issue
|
||
3 years ago
|
func FindIssueReactions(issueID int64, listOptions db.ListOptions) (ReactionList, int64, error) {
|
||
|
return FindReactions(db.DefaultContext, FindReactionsOptions{
|
||
5 years ago
|
ListOptions: listOptions,
|
||
3 years ago
|
IssueID: issueID,
|
||
5 years ago
|
CommentID: -1,
|
||
5 years ago
|
})
|
||
|
}
|
||
|
|
||
3 years ago
|
// 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).
|
||
5 years ago
|
Where(opts.toConds()).
|
||
5 years ago
|
In("reaction.`type`", setting.UI.Reactions).
|
||
5 years ago
|
Asc("reaction.issue_id", "reaction.comment_id", "reaction.created_unix", "reaction.id")
|
||
|
if opts.Page != 0 {
|
||
3 years ago
|
sess = db.SetSessionPagination(sess, &opts)
|
||
5 years ago
|
|
||
|
reactions := make([]*Reaction, 0, opts.PageSize)
|
||
3 years ago
|
count, err := sess.FindAndCount(&reactions)
|
||
|
return reactions, count, err
|
||
5 years ago
|
}
|
||
|
|
||
|
reactions := make([]*Reaction, 0, 10)
|
||
3 years ago
|
count, err := sess.FindAndCount(&reactions)
|
||
|
return reactions, count, err
|
||
7 years ago
|
}
|
||
|
|
||
3 years ago
|
func createReaction(ctx context.Context, opts *ReactionOptions) (*Reaction, error) {
|
||
7 years ago
|
reaction := &Reaction{
|
||
3 years ago
|
Type: opts.Type,
|
||
|
UserID: opts.DoerID,
|
||
|
IssueID: opts.IssueID,
|
||
|
CommentID: opts.CommentID,
|
||
7 years ago
|
}
|
||
5 years ago
|
findOpts := FindReactionsOptions{
|
||
3 years ago
|
IssueID: opts.IssueID,
|
||
|
CommentID: opts.CommentID,
|
||
5 years ago
|
Reaction: opts.Type,
|
||
3 years ago
|
UserID: opts.DoerID,
|
||
5 years ago
|
}
|
||
|
|
||
3 years ago
|
existingR, _, err := FindReactions(ctx, findOpts)
|
||
5 years ago
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
if len(existingR) > 0 {
|
||
|
return existingR[0], ErrReactionAlreadyExist{Reaction: opts.Type}
|
||
7 years ago
|
}
|
||
5 years ago
|
|
||
3 years ago
|
if err := db.Insert(ctx, reaction); err != nil {
|
||
7 years ago
|
return nil, err
|
||
|
}
|
||
|
|
||
|
return reaction, nil
|
||
|
}
|
||
|
|
||
|
// ReactionOptions defines options for creating or deleting reactions
|
||
|
type ReactionOptions struct {
|
||
3 years ago
|
Type string
|
||
|
DoerID int64
|
||
|
IssueID int64
|
||
|
CommentID int64
|
||
7 years ago
|
}
|
||
|
|
||
|
// CreateReaction creates reaction for issue or comment.
|
||
5 years ago
|
func CreateReaction(opts *ReactionOptions) (*Reaction, error) {
|
||
5 years ago
|
if !setting.UI.ReactionsMap[opts.Type] {
|
||
|
return nil, ErrForbiddenIssueReaction{opts.Type}
|
||
|
}
|
||
|
|
||
3 years ago
|
ctx, committer, err := db.TxContext()
|
||
|
if err != nil {
|
||
7 years ago
|
return nil, err
|
||
|
}
|
||
3 years ago
|
defer committer.Close()
|
||
7 years ago
|
|
||
3 years ago
|
reaction, err := createReaction(ctx, opts)
|
||
7 years ago
|
if err != nil {
|
||
5 years ago
|
return reaction, err
|
||
7 years ago
|
}
|
||
|
|
||
3 years ago
|
if err := committer.Commit(); err != nil {
|
||
7 years ago
|
return nil, err
|
||
|
}
|
||
|
return reaction, nil
|
||
|
}
|
||
|
|
||
|
// CreateIssueReaction creates a reaction on issue.
|
||
3 years ago
|
func CreateIssueReaction(doerID, issueID int64, content string) (*Reaction, error) {
|
||
7 years ago
|
return CreateReaction(&ReactionOptions{
|
||
3 years ago
|
Type: content,
|
||
|
DoerID: doerID,
|
||
|
IssueID: issueID,
|
||
7 years ago
|
})
|
||
|
}
|
||
|
|
||
|
// CreateCommentReaction creates a reaction on comment.
|
||
3 years ago
|
func CreateCommentReaction(doerID, issueID, commentID int64, content string) (*Reaction, error) {
|
||
7 years ago
|
return CreateReaction(&ReactionOptions{
|
||
3 years ago
|
Type: content,
|
||
|
DoerID: doerID,
|
||
|
IssueID: issueID,
|
||
|
CommentID: commentID,
|
||
7 years ago
|
})
|
||
|
}
|
||
|
|
||
|
// DeleteReaction deletes reaction for issue or comment.
|
||
3 years ago
|
func DeleteReaction(ctx context.Context, opts *ReactionOptions) error {
|
||
|
reaction := &Reaction{
|
||
|
Type: opts.Type,
|
||
|
UserID: opts.DoerID,
|
||
|
IssueID: opts.IssueID,
|
||
|
CommentID: opts.CommentID,
|
||
7 years ago
|
}
|
||
|
|
||
3 years ago
|
_, err := db.GetEngine(ctx).Where("original_author_id = 0").Delete(reaction)
|
||
|
return err
|
||
7 years ago
|
}
|
||
|
|
||
|
// DeleteIssueReaction deletes a reaction on issue.
|
||
3 years ago
|
func DeleteIssueReaction(doerID, issueID int64, content string) error {
|
||
|
return DeleteReaction(db.DefaultContext, &ReactionOptions{
|
||
|
Type: content,
|
||
|
DoerID: doerID,
|
||
|
IssueID: issueID,
|
||
7 years ago
|
})
|
||
|
}
|
||
|
|
||
|
// DeleteCommentReaction deletes a reaction on comment.
|
||
3 years ago
|
func DeleteCommentReaction(doerID, issueID, commentID int64, content string) error {
|
||
|
return DeleteReaction(db.DefaultContext, &ReactionOptions{
|
||
|
Type: content,
|
||
|
DoerID: doerID,
|
||
|
IssueID: issueID,
|
||
|
CommentID: commentID,
|
||
7 years ago
|
})
|
||
|
}
|
||
|
|
||
|
// 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 {
|
||
5 years ago
|
if reaction.OriginalAuthor == "" && reaction.UserID == userID {
|
||
7 years ago
|
return true
|
||
|
}
|
||
|
}
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
// GroupByType returns reactions grouped by type
|
||
|
func (list ReactionList) GroupByType() map[string]ReactionList {
|
||
4 years ago
|
reactions := make(map[string]ReactionList)
|
||
7 years ago
|
for _, reaction := range list {
|
||
|
reactions[reaction.Type] = append(reactions[reaction.Type], reaction)
|
||
|
}
|
||
|
return reactions
|
||
|
}
|
||
|
|
||
|
func (list ReactionList) getUserIDs() []int64 {
|
||
|
userIDs := make(map[int64]struct{}, len(list))
|
||
|
for _, reaction := range list {
|
||
5 years ago
|
if reaction.OriginalAuthor != "" {
|
||
|
continue
|
||
|
}
|
||
7 years ago
|
if _, ok := userIDs[reaction.UserID]; !ok {
|
||
|
userIDs[reaction.UserID] = struct{}{}
|
||
|
}
|
||
|
}
|
||
3 years ago
|
return container.KeysInt64(userIDs)
|
||
|
}
|
||
|
|
||
|
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
|
||
7 years ago
|
}
|
||
|
|
||
3 years ago
|
// LoadUsers loads reactions' all users
|
||
|
func (list ReactionList) LoadUsers(ctx context.Context, repo *repo_model.Repository) ([]*user_model.User, error) {
|
||
7 years ago
|
if len(list) == 0 {
|
||
|
return nil, nil
|
||
|
}
|
||
|
|
||
|
userIDs := list.getUserIDs()
|
||
3 years ago
|
userMaps := make(map[int64]*user_model.User, len(userIDs))
|
||
3 years ago
|
err := db.GetEngine(ctx).
|
||
7 years ago
|
In("id", userIDs).
|
||
|
Find(&userMaps)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("find user: %v", err)
|
||
|
}
|
||
|
|
||
|
for _, reaction := range list {
|
||
5 years ago
|
if reaction.OriginalAuthor != "" {
|
||
3 years ago
|
reaction.User = user_model.NewReplaceUser(fmt.Sprintf("%s(%s)", reaction.OriginalAuthor, repo.OriginalServiceType.Name()))
|
||
5 years ago
|
} else if user, ok := userMaps[reaction.UserID]; ok {
|
||
7 years ago
|
reaction.User = user
|
||
|
} else {
|
||
3 years ago
|
reaction.User = user_model.NewGhostUser()
|
||
7 years ago
|
}
|
||
|
}
|
||
|
return valuesUser(userMaps), nil
|
||
|
}
|
||
|
|
||
|
// GetFirstUsers returns first reacted user display names separated by comma
|
||
|
func (list ReactionList) GetFirstUsers() string {
|
||
|
var buffer bytes.Buffer
|
||
4 years ago
|
rem := setting.UI.ReactionMaxUserNum
|
||
7 years ago
|
for _, reaction := range list {
|
||
|
if buffer.Len() > 0 {
|
||
|
buffer.WriteString(", ")
|
||
|
}
|
||
|
buffer.WriteString(reaction.User.DisplayName())
|
||
|
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
|
||
|
}
|