2024-10-30 20:47:24 +08:00
|
|
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
2024-10-25 01:35:19 +08:00
|
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
|
|
|
|
package conversations
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"fmt"
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
"code.gitea.io/gitea/models/db"
|
|
|
|
"code.gitea.io/gitea/models/organization"
|
|
|
|
"code.gitea.io/gitea/models/perm"
|
|
|
|
access_model "code.gitea.io/gitea/models/perm/access"
|
|
|
|
repo_model "code.gitea.io/gitea/models/repo"
|
|
|
|
system_model "code.gitea.io/gitea/models/system"
|
|
|
|
"code.gitea.io/gitea/models/unit"
|
|
|
|
user_model "code.gitea.io/gitea/models/user"
|
|
|
|
"code.gitea.io/gitea/modules/references"
|
|
|
|
api "code.gitea.io/gitea/modules/structs"
|
|
|
|
|
|
|
|
"xorm.io/builder"
|
|
|
|
)
|
|
|
|
|
|
|
|
// UpdateConversationCols updates cols of conversation
|
|
|
|
func UpdateConversationCols(ctx context.Context, conversation *Conversation, cols ...string) error {
|
|
|
|
if _, err := db.GetEngine(ctx).ID(conversation.ID).Cols(cols...).Update(conversation); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// UpdateConversationAttachments update attachments by UUIDs for the conversation
|
|
|
|
func UpdateConversationAttachments(ctx context.Context, conversationID int64, uuids []string) (err error) {
|
|
|
|
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 = conversationID
|
|
|
|
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()
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewConversationOptions represents the options of a new conversation.
|
|
|
|
type NewConversationOptions struct {
|
|
|
|
Repo *repo_model.Repository
|
|
|
|
Conversation *Conversation
|
|
|
|
LabelIDs []int64
|
|
|
|
Attachments []string // In UUID format.
|
|
|
|
IsPull bool
|
|
|
|
}
|
|
|
|
|
|
|
|
// UpdateConversationMentions updates conversation-user relations for mentioned users.
|
|
|
|
func UpdateConversationMentions(ctx context.Context, conversationID int64, mentions []*user_model.User) error {
|
|
|
|
if len(mentions) == 0 {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
ids := make([]int64, len(mentions))
|
|
|
|
for i, u := range mentions {
|
|
|
|
ids[i] = u.ID
|
|
|
|
}
|
|
|
|
if err := UpdateConversationUsersByMentions(ctx, conversationID, ids); err != nil {
|
|
|
|
return fmt.Errorf("UpdateConversationUsersByMentions: %w", err)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// FindAndUpdateConversationMentions finds users mentioned in the given content string, and saves them in the database.
|
|
|
|
func FindAndUpdateConversationMentions(ctx context.Context, conversation *Conversation, doer *user_model.User, content string) (mentions []*user_model.User, err error) {
|
|
|
|
rawMentions := references.FindAllMentionsMarkdown(content)
|
|
|
|
mentions, err = ResolveConversationMentionsByVisibility(ctx, conversation, doer, rawMentions)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("UpdateConversationMentions [%d]: %w", conversation.ID, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
notBlocked := make([]*user_model.User, 0, len(mentions))
|
|
|
|
for _, user := range mentions {
|
|
|
|
if !user_model.IsUserBlockedBy(ctx, doer, user.ID) {
|
|
|
|
notBlocked = append(notBlocked, user)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
mentions = notBlocked
|
|
|
|
|
|
|
|
if err = UpdateConversationMentions(ctx, conversation.ID, mentions); err != nil {
|
|
|
|
return nil, fmt.Errorf("UpdateConversationMentions [%d]: %w", conversation.ID, err)
|
|
|
|
}
|
|
|
|
return mentions, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// ResolveConversationMentionsByVisibility returns the users mentioned in an conversation, removing those that
|
|
|
|
// don't have access to reading it. Teams are expanded into their users, but organizations are ignored.
|
|
|
|
func ResolveConversationMentionsByVisibility(ctx context.Context, conversation *Conversation, doer *user_model.User, mentions []string) (users []*user_model.User, err error) {
|
|
|
|
if len(mentions) == 0 {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
if err = conversation.LoadRepo(ctx); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
resolved := make(map[string]bool, 10)
|
|
|
|
var mentionTeams []string
|
|
|
|
|
|
|
|
if err := conversation.Repo.LoadOwner(ctx); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
repoOwnerIsOrg := conversation.Repo.Owner.IsOrganization()
|
|
|
|
if repoOwnerIsOrg {
|
|
|
|
mentionTeams = make([]string, 0, 5)
|
|
|
|
}
|
|
|
|
|
|
|
|
resolved[doer.LowerName] = true
|
|
|
|
for _, name := range mentions {
|
|
|
|
name := strings.ToLower(name)
|
|
|
|
if _, ok := resolved[name]; ok {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if repoOwnerIsOrg && strings.Contains(name, "/") {
|
|
|
|
names := strings.Split(name, "/")
|
|
|
|
if len(names) < 2 || names[0] != conversation.Repo.Owner.LowerName {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
mentionTeams = append(mentionTeams, names[1])
|
|
|
|
resolved[name] = true
|
|
|
|
} else {
|
|
|
|
resolved[name] = false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if conversation.Repo.Owner.IsOrganization() && len(mentionTeams) > 0 {
|
|
|
|
teams := make([]*organization.Team, 0, len(mentionTeams))
|
|
|
|
if err := db.GetEngine(ctx).
|
|
|
|
Join("INNER", "team_repo", "team_repo.team_id = team.id").
|
|
|
|
Where("team_repo.repo_id=?", conversation.Repo.ID).
|
|
|
|
In("team.lower_name", mentionTeams).
|
|
|
|
Find(&teams); err != nil {
|
|
|
|
return nil, fmt.Errorf("find mentioned teams: %w", err)
|
|
|
|
}
|
|
|
|
if len(teams) != 0 {
|
|
|
|
checked := make([]int64, 0, len(teams))
|
|
|
|
unittype := unit.TypeConversations
|
|
|
|
for _, team := range teams {
|
|
|
|
if team.AccessMode >= perm.AccessModeAdmin {
|
|
|
|
checked = append(checked, team.ID)
|
|
|
|
resolved[conversation.Repo.Owner.LowerName+"/"+team.LowerName] = true
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
has, err := db.GetEngine(ctx).Get(&organization.TeamUnit{OrgID: conversation.Repo.Owner.ID, TeamID: team.ID, Type: unittype})
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("get team units (%d): %w", team.ID, err)
|
|
|
|
}
|
|
|
|
if has {
|
|
|
|
checked = append(checked, team.ID)
|
|
|
|
resolved[conversation.Repo.Owner.LowerName+"/"+team.LowerName] = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if len(checked) != 0 {
|
|
|
|
teamusers := make([]*user_model.User, 0, 20)
|
|
|
|
if err := db.GetEngine(ctx).
|
|
|
|
Join("INNER", "team_user", "team_user.uid = `user`.id").
|
|
|
|
In("`team_user`.team_id", checked).
|
|
|
|
And("`user`.is_active = ?", true).
|
|
|
|
And("`user`.prohibit_login = ?", false).
|
|
|
|
Find(&teamusers); err != nil {
|
|
|
|
return nil, fmt.Errorf("get teams users: %w", err)
|
|
|
|
}
|
|
|
|
if len(teamusers) > 0 {
|
|
|
|
users = make([]*user_model.User, 0, len(teamusers))
|
|
|
|
for _, user := range teamusers {
|
|
|
|
if already, ok := resolved[user.LowerName]; !ok || !already {
|
|
|
|
users = append(users, user)
|
|
|
|
resolved[user.LowerName] = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Remove names already in the list to avoid querying the database if pending names remain
|
|
|
|
mentionUsers := make([]string, 0, len(resolved))
|
|
|
|
for name, already := range resolved {
|
|
|
|
if !already {
|
|
|
|
mentionUsers = append(mentionUsers, name)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if len(mentionUsers) == 0 {
|
|
|
|
return users, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if users == nil {
|
|
|
|
users = make([]*user_model.User, 0, len(mentionUsers))
|
|
|
|
}
|
|
|
|
|
|
|
|
unchecked := make([]*user_model.User, 0, len(mentionUsers))
|
|
|
|
if err := db.GetEngine(ctx).
|
|
|
|
Where("`user`.is_active = ?", true).
|
|
|
|
And("`user`.prohibit_login = ?", false).
|
|
|
|
In("`user`.lower_name", mentionUsers).
|
|
|
|
Find(&unchecked); err != nil {
|
|
|
|
return nil, fmt.Errorf("find mentioned users: %w", err)
|
|
|
|
}
|
|
|
|
for _, user := range unchecked {
|
|
|
|
if already := resolved[user.LowerName]; already || user.IsOrganization() {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
// Normal users must have read access to the referencing conversation
|
|
|
|
perm, err := access_model.GetUserRepoPermission(ctx, conversation.Repo, user)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("GetUserRepoPermission [%d]: %w", user.ID, err)
|
|
|
|
}
|
|
|
|
if !perm.CanReadConversations() {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
users = append(users, user)
|
|
|
|
}
|
|
|
|
|
|
|
|
return users, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// UpdateConversationsMigrationsByType updates all migrated repositories' conversations from gitServiceType to replace originalAuthorID to posterID
|
|
|
|
func UpdateConversationsMigrationsByType(ctx context.Context, gitServiceType api.GitServiceType, originalAuthorID string, posterID int64) error {
|
|
|
|
_, err := db.GetEngine(ctx).Table("conversation").
|
|
|
|
Where("repo_id IN (SELECT id FROM repository WHERE original_service_type = ?)", gitServiceType).
|
|
|
|
And("original_author_id = ?", originalAuthorID).
|
|
|
|
Update(map[string]any{
|
|
|
|
"poster_id": posterID,
|
|
|
|
"original_author": "",
|
|
|
|
"original_author_id": 0,
|
|
|
|
})
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// UpdateReactionsMigrationsByType updates all migrated repositories' reactions from gitServiceType to replace originalAuthorID to posterID
|
|
|
|
func UpdateReactionsMigrationsByType(ctx context.Context, gitServiceType api.GitServiceType, originalAuthorID string, userID int64) error {
|
|
|
|
_, err := db.GetEngine(ctx).Table("reaction").
|
|
|
|
Where("original_author_id = ?", originalAuthorID).
|
|
|
|
And(migratedConversationCond(gitServiceType)).
|
|
|
|
Update(map[string]any{
|
|
|
|
"user_id": userID,
|
|
|
|
"original_author": "",
|
|
|
|
"original_author_id": 0,
|
|
|
|
})
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// DeleteConversationsByRepoID deletes conversations by repositories id
|
|
|
|
func DeleteConversationsByRepoID(ctx context.Context, repoID int64) (attachmentPaths []string, err error) {
|
|
|
|
// MariaDB has a performance bug: https://jira.mariadb.org/browse/MDEV-16289
|
|
|
|
// so here it uses "DELETE ... WHERE IN" with pre-queried IDs.
|
|
|
|
sess := db.GetEngine(ctx)
|
|
|
|
|
|
|
|
for {
|
|
|
|
conversationIDs := make([]int64, 0, db.DefaultMaxInSize)
|
|
|
|
|
|
|
|
err := sess.Table(&Conversation{}).Where("repo_id = ?", repoID).OrderBy("id").Limit(db.DefaultMaxInSize).Cols("id").Find(&conversationIDs)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(conversationIDs) == 0 {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
// Delete content histories
|
|
|
|
_, err = sess.In("conversation_id", conversationIDs).Delete(&ConversationContentHistory{})
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Delete comments and attachments
|
2024-10-31 21:46:00 +08:00
|
|
|
_, err = sess.In("conversation_id", conversationIDs).Delete(&ConversationComment{})
|
2024-10-25 01:35:19 +08:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
_, err = sess.In("conversation_id", conversationIDs).Delete(&ConversationUser{})
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2024-10-28 16:33:51 +08:00
|
|
|
_, err = sess.In("conversation_id", conversationIDs).Delete(&CommentReaction{})
|
2024-10-25 01:35:19 +08:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2024-10-31 21:46:00 +08:00
|
|
|
_, err = sess.In("dependent_conversation_id", conversationIDs).Delete(&ConversationComment{})
|
2024-10-25 01:35:19 +08:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var attachments []*repo_model.Attachment
|
|
|
|
err = sess.In("conversation_id", conversationIDs).Find(&attachments)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
for j := range attachments {
|
|
|
|
attachmentPaths = append(attachmentPaths, attachments[j].RelativePath())
|
|
|
|
}
|
|
|
|
|
|
|
|
_, err = sess.In("conversation_id", conversationIDs).Delete(&repo_model.Attachment{})
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
_, err = sess.In("id", conversationIDs).Delete(&Conversation{})
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return attachmentPaths, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// DeleteOrphanedConversations delete conversations without a repo
|
|
|
|
func DeleteOrphanedConversations(ctx context.Context) error {
|
|
|
|
var attachmentPaths []string
|
|
|
|
err := db.WithTx(ctx, func(ctx context.Context) error {
|
|
|
|
var ids []int64
|
|
|
|
|
|
|
|
if err := db.GetEngine(ctx).Table("conversation").Distinct("conversation.repo_id").
|
|
|
|
Join("LEFT", "repository", "conversation.repo_id=repository.id").
|
|
|
|
Where(builder.IsNull{"repository.id"}).GroupBy("conversation.repo_id").
|
|
|
|
Find(&ids); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
for i := range ids {
|
|
|
|
paths, err := DeleteConversationsByRepoID(ctx, ids[i])
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
attachmentPaths = append(attachmentPaths, paths...)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Remove conversation attachment files.
|
|
|
|
for i := range attachmentPaths {
|
|
|
|
system_model.RemoveAllWithNotice(ctx, "Delete conversation attachment", attachmentPaths[i])
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewConversationWithIndex creates conversation with given index
|
|
|
|
func NewConversationWithIndex(ctx context.Context, opts NewConversationOptions) (err error) {
|
|
|
|
e := db.GetEngine(ctx)
|
|
|
|
|
|
|
|
if opts.Conversation.Index <= 0 {
|
|
|
|
return fmt.Errorf("no conversation index provided")
|
|
|
|
}
|
|
|
|
if opts.Conversation.ID > 0 {
|
|
|
|
return fmt.Errorf("conversation exist")
|
|
|
|
}
|
|
|
|
|
|
|
|
if _, err := e.Insert(opts.Conversation); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := repo_model.UpdateRepoConversationNumbers(ctx, opts.Conversation.RepoID, false); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if err = NewConversationUsers(ctx, opts.Repo, opts.Conversation); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(opts.Attachments) > 0 {
|
|
|
|
attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, opts.Attachments)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %w", opts.Attachments, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
for i := 0; i < len(attachments); i++ {
|
|
|
|
attachments[i].ConversationID = opts.Conversation.ID
|
|
|
|
if _, err = e.ID(attachments[i].ID).Update(attachments[i]); err != nil {
|
|
|
|
return fmt.Errorf("update attachment [id: %d]: %w", attachments[i].ID, err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return opts.Conversation.LoadAttributes(ctx)
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewConversation creates new conversation with labels for repository.
|
|
|
|
func NewConversation(ctx context.Context, repo *repo_model.Repository, conversation *Conversation, uuids []string) (err error) {
|
|
|
|
ctx, committer, err := db.TxContext(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer committer.Close()
|
|
|
|
|
|
|
|
idx, err := db.GetNextResourceIndex(ctx, "conversation_index", repo.ID)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("generate conversation index failed: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
conversation.Index = idx
|
|
|
|
|
|
|
|
if err = NewConversationWithIndex(ctx, NewConversationOptions{
|
|
|
|
Repo: repo,
|
|
|
|
Conversation: conversation,
|
|
|
|
Attachments: uuids,
|
|
|
|
}); err != nil {
|
|
|
|
if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) || IsErrNewConversationInsert(err) {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return fmt.Errorf("newConversation: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if err = committer.Commit(); err != nil {
|
2024-11-04 00:34:32 +08:00
|
|
|
return fmt.Errorf("commit: %w", err)
|
2024-10-25 01:35:19 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|