mirror of
https://github.com/go-gitea/gitea.git
synced 2025-02-27 02:37:40 +08:00
247 lines
8.3 KiB
Go
247 lines
8.3 KiB
Go
// Copyright 2024 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package conversations
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"code.gitea.io/gitea/models/avatars"
|
|
"code.gitea.io/gitea/models/db"
|
|
"code.gitea.io/gitea/modules/log"
|
|
"code.gitea.io/gitea/modules/timeutil"
|
|
"code.gitea.io/gitea/modules/util"
|
|
|
|
"xorm.io/builder"
|
|
)
|
|
|
|
// ConversationContentHistory save conversation/comment content history revisions.
|
|
type ConversationContentHistory struct {
|
|
ID int64 `xorm:"pk autoincr"`
|
|
PosterID int64
|
|
ConversationID int64 `xorm:"INDEX"`
|
|
CommentID int64 `xorm:"INDEX"`
|
|
EditedUnix timeutil.TimeStamp `xorm:"INDEX"`
|
|
ContentText string `xorm:"LONGTEXT"`
|
|
IsFirstCreated bool
|
|
IsDeleted bool
|
|
}
|
|
|
|
// TableName provides the real table name
|
|
func (m *ConversationContentHistory) TableName() string {
|
|
return "conversation_content_history"
|
|
}
|
|
|
|
func init() {
|
|
db.RegisterModel(new(ConversationContentHistory))
|
|
}
|
|
|
|
// SaveConversationContentHistory save history
|
|
func SaveConversationContentHistory(ctx context.Context, posterID, conversationID, commentID int64, editTime timeutil.TimeStamp, contentText string, isFirstCreated bool) error {
|
|
ch := &ConversationContentHistory{
|
|
PosterID: posterID,
|
|
ConversationID: conversationID,
|
|
CommentID: commentID,
|
|
ContentText: contentText,
|
|
EditedUnix: editTime,
|
|
IsFirstCreated: isFirstCreated,
|
|
}
|
|
if err := db.Insert(ctx, ch); err != nil {
|
|
log.Error("can not save conversation content history. err=%v", err)
|
|
return err
|
|
}
|
|
// We only keep at most 20 history revisions now. It is enough in most cases.
|
|
// If there is a special requirement to keep more, we can consider introducing a new setting option then, but not now.
|
|
KeepLimitedContentHistory(ctx, conversationID, commentID, 20)
|
|
return nil
|
|
}
|
|
|
|
// KeepLimitedContentHistory keeps at most `limit` history revisions, it will hard delete out-dated revisions, sorting by revision interval
|
|
// we can ignore all errors in this function, so we just log them
|
|
func KeepLimitedContentHistory(ctx context.Context, conversationID, commentID int64, limit int) {
|
|
type IDEditTime struct {
|
|
ID int64
|
|
EditedUnix timeutil.TimeStamp
|
|
}
|
|
|
|
var res []*IDEditTime
|
|
err := db.GetEngine(ctx).Select("id, edited_unix").Table("conversation_content_history").
|
|
Where(builder.Eq{"conversation_id": conversationID, "comment_id": commentID}).
|
|
OrderBy("edited_unix ASC").
|
|
Find(&res)
|
|
if err != nil {
|
|
log.Error("can not query content history for deletion, err=%v", err)
|
|
return
|
|
}
|
|
if len(res) <= 2 {
|
|
return
|
|
}
|
|
|
|
outDatedCount := len(res) - limit
|
|
for outDatedCount > 0 {
|
|
var indexToDelete int
|
|
minEditedInterval := -1
|
|
// find a history revision with minimal edited interval to delete, the first and the last should never be deleted
|
|
for i := 1; i < len(res)-1; i++ {
|
|
editedInterval := int(res[i].EditedUnix - res[i-1].EditedUnix)
|
|
if minEditedInterval == -1 || editedInterval < minEditedInterval {
|
|
minEditedInterval = editedInterval
|
|
indexToDelete = i
|
|
}
|
|
}
|
|
if indexToDelete == 0 {
|
|
break
|
|
}
|
|
|
|
// hard delete the found one
|
|
_, err = db.GetEngine(ctx).Delete(&ConversationContentHistory{ID: res[indexToDelete].ID})
|
|
if err != nil {
|
|
log.Error("can not delete out-dated content history, err=%v", err)
|
|
break
|
|
}
|
|
res = append(res[:indexToDelete], res[indexToDelete+1:]...)
|
|
outDatedCount--
|
|
}
|
|
}
|
|
|
|
// QueryConversationContentHistoryEditedCountMap query related history count of each comment (comment_id = 0 means the main conversation)
|
|
// only return the count map for "edited" (history revision count > 1) conversations or comments.
|
|
func QueryConversationContentHistoryEditedCountMap(dbCtx context.Context, conversationID int64) (map[int64]int, error) {
|
|
type HistoryCountRecord struct {
|
|
CommentID int64
|
|
HistoryCount int
|
|
}
|
|
records := make([]*HistoryCountRecord, 0)
|
|
|
|
err := db.GetEngine(dbCtx).Select("comment_id, COUNT(1) as history_count").
|
|
Table("conversation_content_history").
|
|
Where(builder.Eq{"conversation_id": conversationID}).
|
|
GroupBy("comment_id").
|
|
Having("count(1) > 1").
|
|
Find(&records)
|
|
if err != nil {
|
|
log.Error("can not query conversation content history count map. err=%v", err)
|
|
return nil, err
|
|
}
|
|
|
|
res := map[int64]int{}
|
|
for _, r := range records {
|
|
res[r.CommentID] = r.HistoryCount
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
// ConversationContentListItem the list for web ui
|
|
type ConversationContentListItem struct {
|
|
UserID int64
|
|
UserName string
|
|
UserFullName string
|
|
UserAvatarLink string
|
|
|
|
HistoryID int64
|
|
EditedUnix timeutil.TimeStamp
|
|
IsFirstCreated bool
|
|
IsDeleted bool
|
|
}
|
|
|
|
// FetchConversationContentHistoryList fetch list
|
|
func FetchConversationContentHistoryList(dbCtx context.Context, conversationID, commentID int64) ([]*ConversationContentListItem, error) {
|
|
res := make([]*ConversationContentListItem, 0)
|
|
err := db.GetEngine(dbCtx).Select("u.id as user_id, u.name as user_name, u.full_name as user_full_name,"+
|
|
"h.id as history_id, h.edited_unix, h.is_first_created, h.is_deleted").
|
|
Table([]string{"conversation_content_history", "h"}).
|
|
Join("LEFT", []string{"user", "u"}, "h.poster_id = u.id").
|
|
Where(builder.Eq{"conversation_id": conversationID, "comment_id": commentID}).
|
|
OrderBy("edited_unix DESC").
|
|
Find(&res)
|
|
if err != nil {
|
|
log.Error("can not fetch conversation content history list. err=%v", err)
|
|
return nil, err
|
|
}
|
|
|
|
for _, item := range res {
|
|
if item.UserID > 0 {
|
|
item.UserAvatarLink = avatars.GenerateUserAvatarFastLink(item.UserName, 0)
|
|
} else {
|
|
item.UserAvatarLink = avatars.DefaultAvatarLink()
|
|
}
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
// HasConversationContentHistory check if a ContentHistory entry exists
|
|
func HasConversationContentHistory(dbCtx context.Context, conversationID, commentID int64) (bool, error) {
|
|
exists, err := db.GetEngine(dbCtx).Where(builder.Eq{"conversation_id": conversationID, "comment_id": commentID}).Exist(&ConversationContentHistory{})
|
|
if err != nil {
|
|
return false, fmt.Errorf("can not check conversation content history. err: %w", err)
|
|
}
|
|
return exists, err
|
|
}
|
|
|
|
// SoftDeleteConversationContentHistory soft delete
|
|
func SoftDeleteConversationContentHistory(dbCtx context.Context, historyID int64) error {
|
|
if _, err := db.GetEngine(dbCtx).ID(historyID).Cols("is_deleted", "content_text").Update(&ConversationContentHistory{
|
|
IsDeleted: true,
|
|
ContentText: "",
|
|
}); err != nil {
|
|
log.Error("failed to soft delete conversation content history. err=%v", err)
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ErrConversationContentHistoryNotExist not exist error
|
|
type ErrConversationContentHistoryNotExist struct {
|
|
ID int64
|
|
}
|
|
|
|
// Error error string
|
|
func (err ErrConversationContentHistoryNotExist) Error() string {
|
|
return fmt.Sprintf("conversation content history does not exist [id: %d]", err.ID)
|
|
}
|
|
|
|
func (err ErrConversationContentHistoryNotExist) Unwrap() error {
|
|
return util.ErrNotExist
|
|
}
|
|
|
|
// GetConversationContentHistoryByID get conversation content history
|
|
func GetConversationContentHistoryByID(dbCtx context.Context, id int64) (*ConversationContentHistory, error) {
|
|
h := &ConversationContentHistory{}
|
|
has, err := db.GetEngine(dbCtx).ID(id).Get(h)
|
|
if err != nil {
|
|
return nil, err
|
|
} else if !has {
|
|
return nil, ErrConversationContentHistoryNotExist{id}
|
|
}
|
|
return h, nil
|
|
}
|
|
|
|
// GetConversationContentHistoryAndPrev get a history and the previous non-deleted history (to compare)
|
|
func GetConversationContentHistoryAndPrev(dbCtx context.Context, conversationID, id int64) (history, prevHistory *ConversationContentHistory, err error) {
|
|
history = &ConversationContentHistory{}
|
|
has, err := db.GetEngine(dbCtx).Where("id=? AND conversation_id=?", id, conversationID).Get(history)
|
|
if err != nil {
|
|
log.Error("failed to get conversation content history %v. err=%v", id, err)
|
|
return nil, nil, err
|
|
} else if !has {
|
|
log.Error("conversation content history does not exist. id=%v. err=%v", id, err)
|
|
return nil, nil, &ErrConversationContentHistoryNotExist{id}
|
|
}
|
|
|
|
prevHistory = &ConversationContentHistory{}
|
|
has, err = db.GetEngine(dbCtx).Where(builder.Eq{"conversation_id": history.ConversationID, "comment_id": history.CommentID, "is_deleted": false}).
|
|
And(builder.Lt{"edited_unix": history.EditedUnix}).
|
|
OrderBy("edited_unix DESC").Limit(1).
|
|
Get(prevHistory)
|
|
|
|
if err != nil {
|
|
log.Error("failed to get conversation content history %v. err=%v", id, err)
|
|
return nil, nil, err
|
|
} else if !has {
|
|
return history, nil, nil
|
|
}
|
|
|
|
return history, prevHistory, nil
|
|
}
|