owpengram-server/internal/store/memory/channel_message_delete.go

293 lines
10 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package memory
import (
"context"
"telesrv/internal/domain"
)
func (s *ChannelStore) DeleteChannelMessages(_ context.Context, req domain.DeleteChannelMessagesRequest) (domain.DeleteChannelMessagesResult, error) {
if req.UserID == 0 || req.ChannelID == 0 || len(req.IDs) == 0 {
return domain.DeleteChannelMessagesResult{}, domain.ErrChannelInvalid
}
if len(req.IDs) > domain.MaxDeleteMessageIDs {
return domain.DeleteChannelMessagesResult{}, domain.ErrChannelInvalid
}
s.mu.Lock()
defer s.mu.Unlock()
channel, err := s.channelForMemberLocked(req.UserID, req.ChannelID)
if err != nil {
return domain.DeleteChannelMessagesResult{}, err
}
member := s.members[req.ChannelID][req.UserID]
refs := make(map[int]domain.ChannelDiscussionRef, len(req.IDs))
for _, id := range req.IDs {
if msg, ok := s.findMessageLocked(req.ChannelID, id); ok && !msg.Deleted && msg.Discussion != nil {
refs[id] = *msg.Discussion
}
}
deleted, event, channel, err := s.deleteChannelMessagesLocked(channel, member, req.IDs, req.UserID, req.Date)
if err != nil {
return domain.DeleteChannelMessagesResult{}, err
}
// 被删 broadcast post 的讨论组转发根级联删除服务端动作creator 权限)。
var cascades []domain.ChannelCascadeDelete
byChannel := make(map[int64][]int)
for _, id := range deleted {
if ref, ok := refs[id]; ok && ref.ChannelID != 0 && ref.MessageID != 0 {
byChannel[ref.ChannelID] = append(byChannel[ref.ChannelID], ref.MessageID)
}
}
for groupID, rootIDs := range byChannel {
group, ok := s.channels[groupID]
if !ok || group.Deleted {
continue
}
systemMember := domain.ChannelMember{ChannelID: groupID, UserID: req.UserID, Role: domain.ChannelRoleCreator, Status: domain.ChannelMemberActive}
groupDeleted, groupEvent, group, err := s.deleteChannelMessagesLocked(group, systemMember, rootIDs, req.UserID, req.Date)
if err != nil || len(groupDeleted) == 0 {
continue
}
cascades = append(cascades, domain.ChannelCascadeDelete{
Channel: group,
Event: cloneChannelEvent(groupEvent),
Recipients: s.activeMemberIDsLocked(groupID, 0, 0),
})
}
return domain.DeleteChannelMessagesResult{
Channel: channel,
Event: cloneChannelEvent(event),
DeletedIDs: append([]int(nil), deleted...),
Recipients: s.activeMemberIDsLocked(req.ChannelID, 0, 0),
DiscussionDeletes: cascades,
}, nil
}
func (s *ChannelStore) DeleteChannelHistory(_ context.Context, req domain.DeleteChannelHistoryRequest) (domain.DeleteChannelHistoryResult, error) {
if req.UserID == 0 || req.ChannelID == 0 {
return domain.DeleteChannelHistoryResult{}, domain.ErrChannelInvalid
}
s.mu.Lock()
defer s.mu.Unlock()
channel, err := s.channelForMemberLocked(req.UserID, req.ChannelID)
if err != nil {
return domain.DeleteChannelHistoryResult{}, err
}
if req.Date <= 0 {
req.Date = channel.Date
if req.Date <= 0 {
req.Date = 1
}
}
maxID := req.MaxID
if maxID <= 0 || maxID > channel.TopMessageID {
maxID = channel.TopMessageID
}
member := s.members[req.ChannelID][req.UserID]
if !req.ForEveryone {
appliedMinID := maxInt(member.AvailableMinID, maxID)
changed := appliedMinID > member.AvailableMinID
if changed {
anchorDate := req.Date
if msg, ok := s.findMessageLocked(req.ChannelID, appliedMinID); ok && msg.Date > 0 {
anchorDate = msg.Date
}
if anchorDate <= 0 {
anchorDate = channel.Date
}
member.HistoryClearAnchorID = appliedMinID
member.HistoryClearAnchorDate = anchorDate
if s.historyClearDates[req.ChannelID] == nil {
s.historyClearDates[req.ChannelID] = make(map[int64]int)
}
s.historyClearDates[req.ChannelID][req.UserID] = req.Date
}
member.AvailableMinID = appliedMinID
member.ReadInboxMaxID = maxInt(member.ReadInboxMaxID, appliedMinID)
member.UnreadMark = false
s.members[req.ChannelID][req.UserID] = member
s.deleteChannelUnreadMentionsUpToLocked(req.UserID, req.ChannelID, appliedMinID)
if dialog, ok := s.dialogs[req.UserID][req.ChannelID]; ok {
dialog.UnreadReactions = s.countChannelUnreadReactionsLocked(req.UserID, req.ChannelID, 0)
s.dialogs[req.UserID][req.ChannelID] = dialog
}
if s.dialogs[req.UserID] == nil {
s.dialogs[req.UserID] = make(map[int64]domain.ChannelDialog)
}
s.dialogs[req.UserID][req.ChannelID] = s.dialogForUserLocked(req.UserID, channel)
return domain.DeleteChannelHistoryResult{
Channel: channel,
AvailableMinID: appliedMinID,
AvailableMinChanged: changed,
}, nil
}
if !canDeleteAnyChannelMessage(member) {
return domain.DeleteChannelHistoryResult{}, domain.ErrChannelAdminRequired
}
// id=1 是建群服务消息,全员清空必须保留:它是清空后会话仅剩的
// top message没有它客户端会把 lastMessage 视为空并从聊天列表
// 隐藏该会话(成员资格仍在,但会话条目对全员消失)。
ids := make([]int, 0, domain.MaxDeleteHistoryBatch)
for i := len(s.messages[req.ChannelID]) - 1; i >= 0; i-- {
msg := s.messages[req.ChannelID][i]
if msg.Deleted || msg.ID > maxID || msg.ID <= 1 {
continue
}
ids = append(ids, msg.ID)
if len(ids) >= domain.MaxDeleteHistoryBatch {
break
}
}
deleted, event, channel, err := s.deleteChannelMessagesLocked(channel, member, ids, req.UserID, req.Date)
if err != nil {
return domain.DeleteChannelHistoryResult{}, err
}
offset := 0
if len(deleted) == domain.MaxDeleteHistoryBatch {
offset = 1
}
return domain.DeleteChannelHistoryResult{
Channel: channel,
Event: cloneChannelEvent(event),
DeletedIDs: append([]int(nil), deleted...),
Recipients: s.activeMemberIDsLocked(req.ChannelID, 0, 0),
Offset: offset,
}, nil
}
func (s *ChannelStore) DeleteChannelParticipantHistory(_ context.Context, req domain.DeleteChannelParticipantHistoryRequest) (domain.DeleteChannelHistoryResult, error) {
if req.UserID == 0 || req.ChannelID == 0 || req.ParticipantUserID == 0 {
return domain.DeleteChannelHistoryResult{}, domain.ErrChannelInvalid
}
s.mu.Lock()
defer s.mu.Unlock()
channel, err := s.channelForMemberLocked(req.UserID, req.ChannelID)
if err != nil {
return domain.DeleteChannelHistoryResult{}, err
}
member := s.members[req.ChannelID][req.UserID]
if !canDeleteAnyChannelMessage(member) {
return domain.DeleteChannelHistoryResult{}, domain.ErrChannelAdminRequired
}
// 同全员清空id=1 建群服务消息不随发送者(创建者)历史一起删除,
// 否则会话会因 lastMessage 为空从全员聊天列表隐藏。
ids := make([]int, 0, domain.MaxDeleteHistoryBatch)
for i := len(s.messages[req.ChannelID]) - 1; i >= 0; i-- {
msg := s.messages[req.ChannelID][i]
if msg.Deleted || msg.SenderUserID != req.ParticipantUserID || msg.ID <= 1 {
continue
}
ids = append(ids, msg.ID)
if len(ids) >= domain.MaxDeleteHistoryBatch {
break
}
}
deleted, event, channel, err := s.deleteChannelMessagesLocked(channel, member, ids, req.UserID, req.Date)
if err != nil {
return domain.DeleteChannelHistoryResult{}, err
}
offset := 0
if len(deleted) == domain.MaxDeleteHistoryBatch {
offset = 1
}
return domain.DeleteChannelHistoryResult{
Channel: channel,
Event: cloneChannelEvent(event),
DeletedIDs: append([]int(nil), deleted...),
Recipients: s.activeMemberIDsLocked(req.ChannelID, 0, 0),
Offset: offset,
}, nil
}
func (s *ChannelStore) deleteChannelMessagesLocked(channel domain.Channel, member domain.ChannelMember, ids []int, actorUserID int64, date int) ([]int, domain.ChannelUpdateEvent, domain.Channel, error) {
if len(ids) == 0 {
return nil, domain.ChannelUpdateEvent{}, channel, nil
}
seen := make(map[int]struct{}, len(ids))
deleted := make([]int, 0, len(ids))
for _, id := range ids {
if id <= 0 || id > domain.MaxMessageBoxID {
return nil, domain.ChannelUpdateEvent{}, channel, domain.ErrMessageIDInvalid
}
if _, ok := seen[id]; ok {
continue
}
seen[id] = struct{}{}
idx, ok := s.findMessageIndexLocked(channel.ID, id)
if !ok || s.messages[channel.ID][idx].Deleted {
continue
}
msg := s.messages[channel.ID][idx]
if msg.SenderUserID != actorUserID && !canDeleteAnyChannelMessage(member) {
return nil, domain.ChannelUpdateEvent{}, channel, domain.ErrChannelAdminRequired
}
if id <= 1 {
// id=1 建群服务消息是清空后会话仅剩的兜底 top message所有
// 删除入口统一静默跳过(官方客户端对它禁用删除)。
continue
}
msg.Deleted = true
s.messages[channel.ID][idx] = msg
deleted = append(deleted, id)
s.appendChannelAdminLogLocked(domain.ChannelAdminLogEvent{
ChannelID: channel.ID,
UserID: actorUserID,
Date: date,
Type: domain.ChannelAdminLogDeleteMessage,
Message: ptrChannelMessage(msg),
Query: msg.Body,
})
}
if len(deleted) == 0 {
return nil, domain.ChannelUpdateEvent{}, channel, nil
}
s.deleteChannelUnreadMentionsLocked(channel.ID, deleted)
pts := s.nextChannelPtsNLocked(channel.ID, len(deleted))
channel.Pts = pts
channel.TopMessageID = s.topNonDeletedMessageIDLocked(channel.ID)
// 删除即从置顶集合移除(读路径过滤 NOT deleted重算最新置顶缓存。
channel.PinnedMessageID = s.latestPinnedMessageIDLocked(channel.ID)
s.channels[channel.ID] = channel
for userID, member := range s.members[channel.ID] {
if member.Status != domain.ChannelMemberActive {
continue
}
if s.dialogs[userID] == nil {
s.dialogs[userID] = make(map[int64]domain.ChannelDialog)
}
dialog := s.dialogForUserLocked(userID, channel)
s.dialogs[userID][channel.ID] = dialog
}
event := domain.ChannelUpdateEvent{
ChannelID: channel.ID,
Type: domain.ChannelUpdateDeleteMessages,
Pts: pts,
PtsCount: len(deleted),
Date: date,
MessageIDs: append([]int(nil), deleted...),
SenderUserID: actorUserID,
}
s.appendChannelEventLocked(event)
for _, id := range deleted {
msg, ok := s.findMessageLocked(channel.ID, id)
if !ok || msg.RandomID == 0 || msg.SenderUserID == 0 {
continue
}
key := channelMessageReplayKey{channelID: channel.ID, messageID: msg.ID}
cloned := cloneChannelEvent(event)
s.deleteReceipts[key] = &cloned
}
return deleted, event, channel, nil
}
func (s *ChannelStore) topNonDeletedMessageIDLocked(channelID int64) int {
for i := len(s.messages[channelID]) - 1; i >= 0; i-- {
if !s.messages[channelID][i].Deleted {
return s.messages[channelID][i].ID
}
}
return 0
}
func canDeleteAnyChannelMessage(member domain.ChannelMember) bool {
return member.Role == domain.ChannelRoleCreator || (member.Role == domain.ChannelRoleAdmin && member.AdminRights.DeleteMessages)
}