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
|
|
|
|
OriginalAuthorID int64
|
|
|
|
|
|
|
|
Attachments []*repo_model.Attachment `xorm:"-"`
|
|
|
|
Reactions ReactionList `xorm:"-"`
|
|
|
|
|
|
|
|
Content string `xorm:"LONGTEXT"`
|
|
|
|
ContentVersion int `xorm:"NOT NULL DEFAULT 0"`
|
|
|
|
|
|
|
|
ConversationID int64 `xorm:"INDEX"`
|
|
|
|
Conversation *Conversation
|
|
|
|
|
|
|
|
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-10-25 01:35:19 +08:00
|
|
|
ConversationID: opts.ConversationID,
|
|
|
|
}
|
|
|
|
if _, err = e.Insert(comment); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if err = opts.Repo.LoadOwner(ctx); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
|
|
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 {
|
|
|
|
if _, err := db.Exec(ctx, "UPDATE conversation set num_comments = (SELECT count(*) FROM comment WHERE conversation_id = ? AND `type`=?) WHERE id = ?",
|
|
|
|
conversationID, CommentTypeComment, conversationID); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return committer.Commit()
|
|
|
|
}
|