2024-10-30 20:47:24 +08:00
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
2024-10-25 01:35:19 +08:00
package conversations
// This comment.go was refactored from issues/comment.go to make it context-agnostic to improve reusability.
import (
"context"
"fmt"
2024-10-30 20:37:04 +08:00
"html/template"
2024-10-25 01:35:19 +08:00
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
2024-10-30 01:25:02 +08:00
"code.gitea.io/gitea/modules/container"
2024-10-25 01:35:19 +08:00
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/translation"
"code.gitea.io/gitea/modules/util"
"xorm.io/builder"
)
// ErrCommentNotExist represents a "CommentNotExist" kind of error.
type ErrCommentNotExist struct {
ID int64
ConversationID int64
}
// IsErrCommentNotExist checks if an error is a ErrCommentNotExist.
func IsErrCommentNotExist ( err error ) bool {
_ , ok := err . ( ErrCommentNotExist )
return ok
}
func ( err ErrCommentNotExist ) Error ( ) string {
return fmt . Sprintf ( "comment does not exist [id: %d, conversation_id: %d]" , err . ID , err . ConversationID )
}
func ( err ErrCommentNotExist ) Unwrap ( ) error {
return util . ErrNotExist
}
var ErrCommentAlreadyChanged = util . NewInvalidArgumentErrorf ( "the comment is already changed" )
// CommentType defines whether a comment is just a simple comment, an action (like close) or a reference.
type CommentType int
// CommentTypeUndefined is used to search for comments of any type
const CommentTypeUndefined CommentType = - 1
const (
CommentTypeComment CommentType = iota // 0 Plain comment, can be associated with a commit (CommitID > 0) and a line (LineNum > 0)
CommentTypeLock // 1 Lock an conversation, giving only collaborators access
CommentTypeUnlock // 2 Unlocks a previously locked conversation
CommentTypeAddDependency
CommentTypeRemoveDependency
)
var commentStrings = [ ] string {
"comment" ,
"lock" ,
"unlock" ,
}
func ( t CommentType ) String ( ) string {
return commentStrings [ t ]
}
func AsCommentType ( typeName string ) CommentType {
for index , name := range commentStrings {
if typeName == name {
return CommentType ( index )
}
}
return CommentTypeUndefined
}
func ( t CommentType ) HasContentSupport ( ) bool {
switch t {
case CommentTypeComment :
return true
}
return false
}
func ( t CommentType ) HasAttachmentSupport ( ) bool {
switch t {
case CommentTypeComment :
return true
}
return false
}
func ( t CommentType ) HasMailReplySupport ( ) bool {
switch t {
case CommentTypeComment :
return true
}
return false
}
2024-10-31 21:46:00 +08:00
// ConversationComment represents a comment in commit and conversation page.
// ConversationComment struct should not contain any pointers unrelated to Conversation unless absolutely necessary.
2024-10-25 01:35:19 +08:00
// To have pointers outside of conversation, create another comment type (e.g. ConversationComment) and use a converter to load it in.
// The database data for the comments however, for all comment types, are defined here.
2024-10-31 21:46:00 +08:00
type ConversationComment struct {
2024-10-25 01:35:19 +08:00
ID int64 ` xorm:"pk autoincr" `
Type CommentType ` xorm:"INDEX" `
PosterID int64 ` xorm:"INDEX" `
Poster * user_model . User ` xorm:"-" `
OriginalAuthor string
2024-11-07 23:06:48 +08:00
OriginalAuthorID int64 ` xorm:"INDEX" `
2024-10-25 01:35:19 +08:00
Attachments [ ] * repo_model . Attachment ` xorm:"-" `
Reactions ReactionList ` xorm:"-" `
Content string ` xorm:"LONGTEXT" `
ContentVersion int ` xorm:"NOT NULL DEFAULT 0" `
2024-11-07 22:53:15 +08:00
ConversationID int64 ` xorm:"INDEX" `
Conversation * Conversation ` xorm:"-" `
2024-10-25 01:35:19 +08:00
CreatedUnix timeutil . TimeStamp ` xorm:"INDEX created" `
UpdatedUnix timeutil . TimeStamp ` xorm:"INDEX updated" `
RenderedContent template . HTML ` xorm:"-" `
ShowRole RoleDescriptor ` xorm:"-" `
}
func init ( ) {
2024-10-31 21:46:00 +08:00
db . RegisterModel ( new ( ConversationComment ) )
2024-10-25 01:35:19 +08:00
}
// LoadPoster loads comment poster
2024-10-31 21:46:00 +08:00
func ( c * ConversationComment ) LoadPoster ( ctx context . Context ) ( err error ) {
2024-10-25 01:35:19 +08:00
if c . Poster != nil {
return nil
}
c . Poster , err = user_model . GetPossibleUserByID ( ctx , c . PosterID )
if err != nil {
if user_model . IsErrUserNotExist ( err ) {
c . PosterID = user_model . GhostUserID
c . Poster = user_model . NewGhostUser ( )
} else {
log . Error ( "getUserByID[%d]: %v" , c . ID , err )
}
}
return err
}
// LoadReactions loads comment reactions
2024-10-31 21:46:00 +08:00
func ( c * ConversationComment ) LoadReactions ( ctx context . Context , repo * repo_model . Repository ) ( err error ) {
2024-10-25 01:35:19 +08:00
if c . Reactions != nil {
return nil
}
c . Reactions , _ , err = FindReactions ( ctx , FindReactionsOptions {
ConversationID : c . ConversationID ,
CommentID : c . ID ,
} )
if err != nil {
return err
}
// Load reaction user data
if _ , err := c . Reactions . LoadUsers ( ctx , repo ) ; err != nil {
return err
}
return nil
}
// AfterDelete is invoked from XORM after the object is deleted.
2024-10-31 21:46:00 +08:00
func ( c * ConversationComment ) AfterDelete ( ctx context . Context ) {
2024-10-25 01:35:19 +08:00
if c . ID <= 0 {
return
}
_ , err := repo_model . DeleteAttachmentsByComment ( ctx , c . ID , true )
if err != nil {
log . Info ( "Could not delete files for comment %d on conversation #%d: %s" , c . ID , c . ConversationID , err )
}
}
// RoleInRepo presents the user's participation in the repo
type RoleInRepo string
// RoleDescriptor defines comment "role" tags
type RoleDescriptor struct {
IsPoster bool
RoleInRepo RoleInRepo
}
// Enumerate all the role tags.
const (
RoleRepoOwner RoleInRepo = "owner"
RoleRepoMember RoleInRepo = "member"
RoleRepoCollaborator RoleInRepo = "collaborator"
RoleRepoFirstTimeContributor RoleInRepo = "first_time_contributor"
RoleRepoContributor RoleInRepo = "contributor"
)
// LocaleString returns the locale string name of the role
func ( r RoleInRepo ) LocaleString ( lang translation . Locale ) string {
return lang . TrString ( "repo.conversations.role." + string ( r ) )
}
// LocaleHelper returns the locale tooltip of the role
func ( r RoleInRepo ) LocaleHelper ( lang translation . Locale ) string {
return lang . TrString ( "repo.conversations.role." + string ( r ) + "_helper" )
}
// CreateCommentOptions defines options for creating comment
type CreateCommentOptions struct {
Type CommentType
Doer * user_model . User
Repo * repo_model . Repository
Attachments [ ] string // UUIDs of attachments
ConversationID int64
Conversation * Conversation
Content string
DependentConversationID int64
}
// CreateComment creates comment with context
2024-10-31 21:46:00 +08:00
func CreateComment ( ctx context . Context , opts * CreateCommentOptions ) ( _ * ConversationComment , err error ) {
2024-10-25 01:35:19 +08:00
ctx , committer , err := db . TxContext ( ctx )
if err != nil {
return nil , err
}
defer committer . Close ( )
e := db . GetEngine ( ctx )
2024-10-31 21:46:00 +08:00
comment := & ConversationComment {
2024-10-25 01:35:19 +08:00
Type : opts . Type ,
PosterID : opts . Doer . ID ,
Poster : opts . Doer ,
2024-10-25 22:05:56 +08:00
Content : opts . Content ,
2024-11-06 18:09:24 +08:00
Conversation : opts . Conversation ,
2024-11-06 19:22:08 +08:00
ConversationID : opts . Conversation . ID ,
2024-10-25 01:35:19 +08:00
}
if _ , err = e . Insert ( comment ) ; err != nil {
return nil , err
}
if err = opts . Repo . LoadOwner ( ctx ) ; err != nil {
return nil , err
}
2024-11-06 19:22:08 +08:00
if err = updateCommentInfos ( ctx , opts ) ; err != nil {
2024-11-06 18:09:24 +08:00
return nil , err
}
2024-10-25 01:35:19 +08:00
if err = committer . Commit ( ) ; err != nil {
return nil , err
}
return comment , nil
}
// GetCommentByID returns the comment by given ID.
2024-10-31 21:46:00 +08:00
func GetCommentByID ( ctx context . Context , id int64 ) ( * ConversationComment , error ) {
c := new ( ConversationComment )
2024-10-25 01:35:19 +08:00
has , err := db . GetEngine ( ctx ) . ID ( id ) . Get ( c )
if err != nil {
return nil , err
} else if ! has {
return nil , ErrCommentNotExist { id , 0 }
}
return c , nil
}
// FindCommentsOptions describes the conditions to Find comments
type FindCommentsOptions struct {
db . ListOptions
RepoID int64
ConversationID int64
Since int64
Before int64
Type CommentType
ConversationIDs [ ] int64
}
// ToConds implements FindOptions interface
func ( opts FindCommentsOptions ) ToConds ( ) builder . Cond {
cond := builder . NewCond ( )
if opts . RepoID > 0 {
cond = cond . And ( builder . Eq { "conversation.repo_id" : opts . RepoID } )
}
if opts . ConversationID > 0 {
2024-10-31 21:46:00 +08:00
cond = cond . And ( builder . Eq { "conversation_comment.conversation_id" : opts . ConversationID } )
2024-10-25 01:35:19 +08:00
} else if len ( opts . ConversationIDs ) > 0 {
2024-10-31 21:46:00 +08:00
cond = cond . And ( builder . In ( "conversation_comment.conversation_id" , opts . ConversationIDs ) )
2024-10-25 01:35:19 +08:00
}
if opts . Since > 0 {
2024-10-31 21:46:00 +08:00
cond = cond . And ( builder . Gte { "conversation_comment.updated_unix" : opts . Since } )
2024-10-25 01:35:19 +08:00
}
if opts . Before > 0 {
2024-10-31 21:46:00 +08:00
cond = cond . And ( builder . Lte { "conversation_comment.updated_unix" : opts . Before } )
2024-10-25 01:35:19 +08:00
}
if opts . Type != CommentTypeUndefined {
2024-10-31 21:46:00 +08:00
cond = cond . And ( builder . Eq { "conversation_comment.type" : opts . Type } )
2024-10-25 01:35:19 +08:00
}
return cond
}
// FindComments returns all comments according options
func FindComments ( ctx context . Context , opts * FindCommentsOptions ) ( CommentList , error ) {
2024-10-31 21:46:00 +08:00
comments := make ( [ ] * ConversationComment , 0 , 10 )
2024-10-25 01:35:19 +08:00
sess := db . GetEngine ( ctx ) . Where ( opts . ToConds ( ) )
2024-10-25 22:05:56 +08:00
if opts . RepoID > 0 {
2024-10-31 21:46:00 +08:00
sess . Join ( "INNER" , "conversation" , "conversation.id = conversation_comment.conversation_id" )
2024-10-25 01:35:19 +08:00
}
if opts . Page != 0 {
sess = db . SetSessionPagination ( sess , opts )
}
// WARNING: If you change this order you will need to fix createCodeComment
return comments , sess .
2024-10-31 21:46:00 +08:00
Asc ( "conversation_comment.created_unix" ) .
Asc ( "conversation_comment.id" ) .
2024-10-25 01:35:19 +08:00
Find ( & comments )
}
// CountComments count all comments according options by ignoring pagination
func CountComments ( ctx context . Context , opts * FindCommentsOptions ) ( int64 , error ) {
sess := db . GetEngine ( ctx ) . Where ( opts . ToConds ( ) )
if opts . RepoID > 0 {
2024-10-31 21:46:00 +08:00
sess . Join ( "INNER" , "conversation" , "conversation.id = conversation_comment.conversation_id" )
2024-10-25 01:35:19 +08:00
}
2024-10-31 21:46:00 +08:00
return sess . Count ( & ConversationComment { } )
2024-10-25 01:35:19 +08:00
}
// UpdateCommentInvalidate updates comment invalidated column
2024-10-31 21:46:00 +08:00
func UpdateCommentInvalidate ( ctx context . Context , c * ConversationComment ) error {
2024-10-25 01:35:19 +08:00
_ , err := db . GetEngine ( ctx ) . ID ( c . ID ) . Cols ( "invalidated" ) . Update ( c )
return err
}
2024-10-31 21:46:00 +08:00
// UpdateComment updates information of comment
func UpdateComment ( ctx context . Context , c * ConversationComment , contentVersion int , doer * user_model . User ) error {
2024-10-25 01:35:19 +08:00
ctx , committer , err := db . TxContext ( ctx )
if err != nil {
return err
}
defer committer . Close ( )
sess := db . GetEngine ( ctx )
c . ContentVersion = contentVersion + 1
affected , err := sess . ID ( c . ID ) . AllCols ( ) . Where ( "content_version = ?" , contentVersion ) . Update ( c )
if err != nil {
return err
}
if affected == 0 {
return ErrCommentAlreadyChanged
}
if err := committer . Commit ( ) ; err != nil {
return fmt . Errorf ( "commit: %w" , err )
}
return nil
}
// DeleteComment deletes the comment
2024-10-31 21:46:00 +08:00
func DeleteComment ( ctx context . Context , comment * ConversationComment ) error {
2024-10-25 01:35:19 +08:00
e := db . GetEngine ( ctx )
if _ , err := e . ID ( comment . ID ) . NoAutoCondition ( ) . Delete ( comment ) ; err != nil {
return err
}
if _ , err := db . DeleteByBean ( ctx , & ConversationContentHistory {
CommentID : comment . ID ,
} ) ; err != nil {
return err
}
2024-11-06 18:09:24 +08:00
if comment . Type == CommentTypeComment {
if _ , err := e . ID ( comment . ConversationID ) . Decr ( "num_comments" ) . Update ( new ( Conversation ) ) ; err != nil {
return err
}
}
2024-10-25 01:35:19 +08:00
if _ , err := e . Table ( "action" ) .
Where ( "comment_id = ?" , comment . ID ) .
Update ( map [ string ] any {
"is_deleted" : true ,
} ) ; err != nil {
return err
}
return DeleteReaction ( ctx , & ReactionOptions { CommentID : comment . ID } )
}
// UpdateCommentsMigrationsByType updates comments' migrations information via given git service type and original id and poster id
func UpdateCommentsMigrationsByType ( ctx context . Context , tp structs . GitServiceType , originalAuthorID string , posterID int64 ) error {
2024-10-31 21:46:00 +08:00
_ , err := db . GetEngine ( ctx ) . Table ( "conversation_comment" ) .
Join ( "INNER" , "conversation" , "conversation.id = conversation_comment.conversation_id" ) .
2024-10-25 01:35:19 +08:00
Join ( "INNER" , "repository" , "conversation.repo_id = repository.id" ) .
Where ( "repository.original_service_type = ?" , tp ) .
2024-10-31 21:46:00 +08:00
And ( "conversation_comment.original_author_id = ?" , originalAuthorID ) .
2024-10-25 01:35:19 +08:00
Update ( map [ string ] any {
"poster_id" : posterID ,
"original_author" : "" ,
"original_author_id" : 0 ,
} )
return err
}
2024-10-31 21:46:00 +08:00
func UpdateAttachments ( ctx context . Context , opts * CreateCommentOptions , comment * ConversationComment ) error {
2024-10-25 01:35:19 +08:00
attachments , err := repo_model . GetAttachmentsByUUIDs ( ctx , opts . Attachments )
if err != nil {
return fmt . Errorf ( "getAttachmentsByUUIDs [uuids: %v]: %w" , opts . Attachments , err )
}
for i := range attachments {
attachments [ i ] . ConversationID = comment . ConversationID
attachments [ i ] . CommentID = comment . ID
// No assign value could be 0, so ignore AllCols().
if _ , err = db . GetEngine ( ctx ) . ID ( attachments [ i ] . ID ) . Update ( attachments [ i ] ) ; err != nil {
return fmt . Errorf ( "update attachment [%d]: %w" , attachments [ i ] . ID , err )
}
}
comment . Attachments = attachments
return nil
}
// LoadConversation loads the conversation reference for the comment
2024-10-31 21:46:00 +08:00
func ( c * ConversationComment ) LoadConversation ( ctx context . Context ) ( err error ) {
2024-10-25 01:35:19 +08:00
if c . Conversation != nil {
return nil
}
c . Conversation , err = GetConversationByID ( ctx , c . ConversationID )
return err
}
// LoadAttachments loads attachments (it never returns error, the error during `GetAttachmentsByCommentIDCtx` is ignored)
2024-10-31 21:46:00 +08:00
func ( c * ConversationComment ) LoadAttachments ( ctx context . Context ) error {
2024-10-25 01:35:19 +08:00
if len ( c . Attachments ) > 0 {
return nil
}
var err error
c . Attachments , err = repo_model . GetAttachmentsByCommentID ( ctx , c . ID )
if err != nil {
log . Error ( "getAttachmentsByCommentID[%d]: %v" , c . ID , err )
}
return nil
}
// UpdateAttachments update attachments by UUIDs for the comment
2024-10-31 21:46:00 +08:00
func ( c * ConversationComment ) UpdateAttachments ( ctx context . Context , uuids [ ] string ) error {
2024-10-25 01:35:19 +08:00
ctx , committer , err := db . TxContext ( ctx )
if err != nil {
return err
}
defer committer . Close ( )
attachments , err := repo_model . GetAttachmentsByUUIDs ( ctx , uuids )
if err != nil {
return fmt . Errorf ( "getAttachmentsByUUIDs [uuids: %v]: %w" , uuids , err )
}
for i := 0 ; i < len ( attachments ) ; i ++ {
attachments [ i ] . ConversationID = c . ConversationID
attachments [ i ] . CommentID = c . ID
if err := repo_model . UpdateAttachment ( ctx , attachments [ i ] ) ; err != nil {
return fmt . Errorf ( "update attachment [id: %d]: %w" , attachments [ i ] . ID , err )
}
}
return committer . Commit ( )
}
2024-10-30 01:25:02 +08:00
// HashTag returns unique hash tag for conversation.
2024-10-31 21:46:00 +08:00
func ( c * ConversationComment ) HashTag ( ) string {
2024-10-30 20:37:04 +08:00
return fmt . Sprintf ( "comment-%d" , c . ID )
2024-10-25 01:35:19 +08:00
}
2024-10-31 21:46:00 +08:00
func ( c * ConversationComment ) hashLink ( ) string {
2024-10-25 01:35:19 +08:00
return "#" + c . HashTag ( )
}
// HTMLURL formats a URL-string to the conversation-comment
2024-10-31 21:46:00 +08:00
func ( c * ConversationComment ) HTMLURL ( ctx context . Context ) string {
2024-10-25 01:35:19 +08:00
err := c . LoadConversation ( ctx )
if err != nil { // Silently dropping errors :unamused:
log . Error ( "LoadConversation(%d): %v" , c . ConversationID , err )
return ""
}
err = c . Conversation . LoadRepo ( ctx )
if err != nil { // Silently dropping errors :unamused:
log . Error ( "loadRepo(%d): %v" , c . Conversation . RepoID , err )
return ""
}
2024-10-30 20:37:04 +08:00
return c . Conversation . HTMLURL ( ) + c . hashLink ( )
2024-10-25 01:35:19 +08:00
}
// APIURL formats a API-string to the conversation-comment
2024-10-31 21:46:00 +08:00
func ( c * ConversationComment ) APIURL ( ctx context . Context ) string {
2024-10-25 01:35:19 +08:00
err := c . LoadConversation ( ctx )
if err != nil { // Silently dropping errors :unamused:
log . Error ( "LoadConversation(%d): %v" , c . ConversationID , err )
return ""
}
err = c . Conversation . LoadRepo ( ctx )
if err != nil { // Silently dropping errors :unamused:
log . Error ( "loadRepo(%d): %v" , c . Conversation . RepoID , err )
return ""
}
return fmt . Sprintf ( "%s/conversations/comments/%d" , c . Conversation . Repo . APIURL ( ) , c . ID )
}
// HasOriginalAuthor returns if a comment was migrated and has an original author.
2024-10-31 21:46:00 +08:00
func ( c * ConversationComment ) HasOriginalAuthor ( ) bool {
2024-10-25 01:35:19 +08:00
return c . OriginalAuthor != "" && c . OriginalAuthorID != 0
}
2024-10-25 22:05:56 +08:00
2024-10-31 21:46:00 +08:00
func ( c * ConversationComment ) ConversationURL ( ctx context . Context ) string {
2024-10-25 22:05:56 +08:00
err := c . LoadConversation ( ctx )
if err != nil { // Silently dropping errors :unamused:
log . Error ( "LoadConversation(%d): %v" , c . ConversationID , err )
return ""
}
err = c . Conversation . LoadRepo ( ctx )
if err != nil { // Silently dropping errors :unamused:
log . Error ( "loadRepo(%d): %v" , c . Conversation . RepoID , err )
return ""
}
return c . Conversation . HTMLURL ( )
}
2024-10-30 01:25:02 +08:00
// InsertConversationComments inserts many comments of conversations.
2024-10-31 21:46:00 +08:00
func InsertConversationComments ( ctx context . Context , comments [ ] * ConversationComment ) error {
2024-10-30 01:25:02 +08:00
if len ( comments ) == 0 {
return nil
}
2024-10-31 21:46:00 +08:00
conversationIDs := container . FilterSlice ( comments , func ( comment * ConversationComment ) ( int64 , bool ) {
2024-10-30 01:25:02 +08:00
return comment . ConversationID , true
} )
ctx , committer , err := db . TxContext ( ctx )
if err != nil {
return err
}
defer committer . Close ( )
for _ , comment := range comments {
if _ , err := db . GetEngine ( ctx ) . NoAutoTime ( ) . Insert ( comment ) ; err != nil {
return err
}
for _ , reaction := range comment . Reactions {
reaction . ConversationID = comment . ConversationID
reaction . CommentID = comment . ID
}
if len ( comment . Reactions ) > 0 {
if err := db . Insert ( ctx , comment . Reactions ) ; err != nil {
return err
}
}
}
for _ , conversationID := range conversationIDs {
2024-11-06 19:22:08 +08:00
if _ , err := db . Exec ( ctx , "UPDATE conversation set num_comments = (SELECT count(*) FROM conversation_comment WHERE conversation_id = ? AND `type`=?) WHERE id = ?" ,
2024-10-30 01:25:02 +08:00
conversationID , CommentTypeComment , conversationID ) ; err != nil {
return err
}
}
return committer . Commit ( )
}
2024-11-06 18:09:24 +08:00
2024-11-06 19:22:08 +08:00
func updateCommentInfos ( ctx context . Context , opts * CreateCommentOptions ) ( err error ) {
2024-11-06 18:09:24 +08:00
// Check comment type.
switch opts . Type {
case CommentTypeComment :
if _ , err = db . Exec ( ctx , "UPDATE `conversation` SET num_comments=num_comments+1 WHERE id=?" , opts . Conversation . ID ) ; err != nil {
return err
}
}
// update the conversation's updated_unix column
return UpdateConversationCols ( ctx , opts . Conversation , "updated_unix" )
}