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

410 lines
15 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"
"sort"
"strings"
"telesrv/internal/domain"
)
func (s *ChannelStore) EditChannelMessage(_ context.Context, req domain.EditChannelMessageRequest) (domain.EditChannelMessageResult, error) {
if req.UserID == 0 || req.ChannelID == 0 || req.ID <= 0 {
return domain.EditChannelMessageResult{}, domain.ErrChannelInvalid
}
s.mu.Lock()
defer s.mu.Unlock()
channel, err := s.channelForMemberLocked(req.UserID, req.ChannelID)
if err != nil {
return domain.EditChannelMessageResult{}, err
}
member := s.members[req.ChannelID][req.UserID]
idx, ok := s.findMessageIndexLocked(req.ChannelID, req.ID)
if !ok || s.messages[req.ChannelID][idx].Deleted || s.messages[req.ChannelID][idx].Action != nil {
return domain.EditChannelMessageResult{}, domain.ErrMessageIDInvalid
}
prevMsg := s.messages[req.ChannelID][idx]
msg := prevMsg
finalMedia := msg.Media
if req.Media != nil {
finalMedia = req.Media
}
finalRich := msg.RichMessage
if req.SetRichMessage {
finalRich = req.RichMessage
}
if strings.TrimSpace(req.Message) == "" && finalMedia.IsZero() && finalRich.IsZero() {
return domain.EditChannelMessageResult{}, domain.ErrChannelInvalid
}
// WebPageResolve频道链接预览就地替换服务端内部幂等守卫即授权。只换 media、
// 不碰 body/entities/edit_date事件为 channel_web_page。
if req.WebPageResolve {
if req.Media == nil || !domain.IsPendingWebPageMedia(msg.Media, req.ExpectedWebPageID) {
return domain.EditChannelMessageResult{}, domain.ErrMessageNotModified
}
pts := s.nextChannelPtsLocked(req.ChannelID)
media := *req.Media
msg.Media = &media
msg.Pts = pts
s.messages[req.ChannelID][idx] = msg
channel.Pts = pts
s.channels[req.ChannelID] = channel
event := domain.ChannelUpdateEvent{
ChannelID: req.ChannelID,
Type: domain.ChannelUpdateWebPage,
Pts: pts,
PtsCount: 1,
Date: msg.Date,
Message: cloneChannelMessage(msg),
SenderUserID: req.UserID,
}
s.events[req.ChannelID] = append(s.events[req.ChannelID], event)
return domain.EditChannelMessageResult{
Channel: channel,
Message: cloneChannelMessage(msg),
Event: cloneChannelEvent(event),
Recipients: s.activeMemberIDsLocked(req.ChannelID, 0, 0),
}, nil
}
// participant todo 协作须满足与正常发消息相同的权限,禁言成员/无发帖权订阅者
// 不得借 OthersCanComplete/Append 绕过发言限制(与 postgres 对齐)。
participantTodoEdit := isChannelTodoParticipantEdit(req, msg) && canSendChannelMessage(channel, member)
viaBotEditRequested := req.ViaBotEditBotID != 0
viaBotEdit := viaBotEditRequested && msg.ViaBotID == req.ViaBotEditBotID
if viaBotEditRequested && !viaBotEdit {
return domain.EditChannelMessageResult{}, domain.ErrMessageAuthorRequired
}
if !viaBotEdit && msg.SenderUserID != req.UserID && !canEditChannelMessage(member) && !participantTodoEdit {
return domain.EditChannelMessageResult{}, domain.ErrMessageAuthorRequired
}
richChanged := req.SetRichMessage && !richMessagesEqual(msg.RichMessage, req.RichMessage)
if req.Media == nil && !req.SetReplyMarkup && !richChanged && msg.Body == req.Message && sameMessageEntities(msg.Entities, req.Entities) {
return domain.EditChannelMessageResult{}, domain.ErrMessageNotModified
}
pts := s.nextChannelPtsLocked(req.ChannelID)
msg.Body = req.Message
msg.Entities = append([]domain.MessageEntity(nil), req.Entities...)
if req.Media != nil {
media := *req.Media
msg.Media = &media
}
if req.SetReplyMarkup {
msg.ReplyMarkup = cloneReplyMarkup(req.ReplyMarkup)
}
if req.SetRichMessage {
msg.RichMessage = cloneRichMessage(req.RichMessage)
}
msg.EditDate = req.EditDate
msg.Pts = pts
s.messages[req.ChannelID][idx] = msg
channel.Pts = pts
s.channels[req.ChannelID] = channel
event := domain.ChannelUpdateEvent{
ChannelID: req.ChannelID,
Type: domain.ChannelUpdateEditMessage,
Pts: pts,
PtsCount: 1,
Date: req.EditDate,
Message: cloneChannelMessage(msg),
SenderUserID: req.UserID,
}
s.events[req.ChannelID] = append(s.events[req.ChannelID], event)
s.appendChannelAdminLogLocked(domain.ChannelAdminLogEvent{
ChannelID: req.ChannelID,
UserID: req.UserID,
Date: req.EditDate,
Type: domain.ChannelAdminLogEditMessage,
PrevMessage: ptrChannelMessage(prevMsg),
NewMessage: ptrChannelMessage(msg),
Query: msg.Body,
})
var serviceMsg domain.ChannelMessage
var serviceEvent domain.ChannelUpdateEvent
if req.TodoServiceAction != nil {
action := cloneChannelMessageAction(req.TodoServiceAction)
if action == nil {
return domain.EditChannelMessageResult{}, domain.ErrChannelInvalid
}
replyTo := channelTodoServiceReply(msg)
servicePts := s.nextChannelPtsLocked(req.ChannelID)
serviceMsg = domain.ChannelMessage{
ChannelID: req.ChannelID,
ID: s.nextChannelMessageIDLocked(req.ChannelID),
SenderUserID: req.UserID,
From: domain.Peer{Type: domain.PeerTypeUser, ID: req.UserID},
Date: req.EditDate,
Post: channel.Broadcast,
Silent: msg.Silent,
NoForwards: msg.NoForwards || channel.NoForwards,
ReplyTo: replyTo,
Action: action,
Pts: servicePts,
}
serviceEvent = domain.ChannelUpdateEvent{
ChannelID: req.ChannelID,
Type: domain.ChannelUpdateNewMessage,
Pts: servicePts,
PtsCount: 1,
Date: req.EditDate,
Message: cloneChannelMessage(serviceMsg),
SenderUserID: req.UserID,
}
s.messages[req.ChannelID] = append(s.messages[req.ChannelID], serviceMsg)
s.events[req.ChannelID] = append(s.events[req.ChannelID], serviceEvent)
s.updateForumTopicTopMessageLocked(req.ChannelID, serviceMsg)
channel.TopMessageID = serviceMsg.ID
channel.Pts = servicePts
s.channels[req.ChannelID] = channel
for userID, member := range s.members[req.ChannelID] {
if member.Status == domain.ChannelMemberActive {
s.upsertChannelDialogLocked(userID, channel, serviceMsg, userID == req.UserID)
}
}
}
// 编辑对账 @ 集合新增者补提及被移除的实体提及删除reply 隐式提及保留。
// 仅在 body/entities 实际变化时执行media-only 编辑geolive/todo/poll不带
// MentionUserIDs无条件对账会误删 caption 里仍未读的 @ 提及(与 postgres 对齐)。
textChanged := prevMsg.Body != req.Message || !sameMessageEntities(prevMsg.Entities, req.Entities)
if textChanged && (!channel.Broadcast || channel.Megagroup) {
keep := make(map[int64]struct{}, len(req.MentionUserIDs)+1)
for _, id := range req.MentionUserIDs {
if id != 0 {
keep[id] = struct{}{}
}
}
if msg.ReplyTo != nil && msg.ReplyTo.MessageID > 0 {
if target, ok := s.findMessageLocked(req.ChannelID, msg.ReplyTo.MessageID); ok && target.SenderUserID != 0 {
keep[target.SenderUserID] = struct{}{}
}
}
added := make([]int64, 0, len(req.MentionUserIDs))
for _, id := range req.MentionUserIDs {
if id == 0 {
continue
}
if _, ok := s.mentions[id][req.ChannelID][msg.ID]; !ok {
added = append(added, id)
}
}
for userID, byChannel := range s.mentions {
if _, ok := byChannel[req.ChannelID][msg.ID]; !ok {
continue
}
if _, ok := keep[userID]; ok {
continue
}
delete(byChannel[req.ChannelID], msg.ID)
if dialogs := s.dialogs[userID]; dialogs != nil {
if dialog, ok := dialogs[req.ChannelID]; ok {
dialog.UnreadMentions = s.countChannelUnreadMentionsLocked(userID, req.ChannelID, 0)
dialogs[req.ChannelID] = dialog
}
}
}
if len(added) > 0 {
s.addChannelUnreadMentionsLocked(req.ChannelID, msg, req.UserID, added)
for _, id := range added {
if dialogs := s.dialogs[id]; dialogs != nil {
if dialog, ok := dialogs[req.ChannelID]; ok {
dialog.UnreadMentions = s.countChannelUnreadMentionsLocked(id, req.ChannelID, 0)
dialogs[req.ChannelID] = dialog
}
}
}
}
}
return domain.EditChannelMessageResult{
Channel: channel,
Message: cloneChannelMessage(msg),
Event: cloneChannelEvent(event),
ServiceMessage: cloneChannelMessage(serviceMsg),
ServiceEvent: cloneChannelEvent(serviceEvent),
Recipients: s.activeMemberIDsLocked(req.ChannelID, 0, 0),
}, nil
}
func isChannelTodoParticipantEdit(req domain.EditChannelMessageRequest, msg domain.ChannelMessage) bool {
if !req.AllowTodoParticipantMutation || req.SetReplyMarkup || req.Media == nil || req.Media.Kind != domain.MessageMediaKindTodo || req.Media.Todo == nil {
return false
}
if msg.Media == nil || msg.Media.Kind != domain.MessageMediaKindTodo || msg.Media.Todo == nil {
return false
}
if req.TodoServiceAction == nil {
return false
}
switch req.TodoServiceAction.Type {
case domain.ChannelActionTodoCompletions:
if !msg.Media.Todo.OthersCanComplete {
return false
}
case domain.ChannelActionTodoAppendTasks:
if !msg.Media.Todo.OthersCanAppend {
return false
}
default:
return false
}
return msg.Body == req.Message && sameMessageEntities(msg.Entities, req.Entities)
}
func channelTodoServiceReply(msg domain.ChannelMessage) *domain.MessageReply {
reply := &domain.MessageReply{
Peer: domain.Peer{Type: domain.PeerTypeChannel, ID: msg.ChannelID},
MessageID: msg.ID,
}
if msg.ReplyTo != nil {
reply.TopMessageID = msg.ReplyTo.TopMessageID
reply.ForumTopic = msg.ReplyTo.ForumTopic
}
return reply
}
func (s *ChannelStore) UpdatePinnedMessage(_ context.Context, req domain.UpdateChannelPinnedMessageRequest) (domain.UpdateChannelPinnedMessageResult, error) {
if req.UserID == 0 || req.ChannelID == 0 || req.MessageID <= 0 {
return domain.UpdateChannelPinnedMessageResult{}, domain.ErrChannelInvalid
}
s.mu.Lock()
defer s.mu.Unlock()
channel, err := s.channelForMemberLocked(req.UserID, req.ChannelID)
if err != nil {
return domain.UpdateChannelPinnedMessageResult{}, err
}
member := s.members[req.ChannelID][req.UserID]
if !canPinChannelMessages(channel, member) {
return domain.UpdateChannelPinnedMessageResult{}, domain.ErrChannelAdminRequired
}
msg, ok := s.findMessageLocked(req.ChannelID, req.MessageID)
if !ok || msg.Deleted {
return domain.UpdateChannelPinnedMessageResult{}, domain.ErrMessageIDInvalid
}
// 多置顶模型pin/unpin 只翻转目标消息自身的 pinned flag不影响其它
// 置顶;与官方一致不设数量上限。
if msg.Pinned == req.Pinned {
return domain.UpdateChannelPinnedMessageResult{}, domain.ErrChannelNotModified
}
for i := range s.messages[req.ChannelID] {
if s.messages[req.ChannelID][i].ID == req.MessageID {
s.messages[req.ChannelID][i].Pinned = req.Pinned
break
}
}
pts := s.nextChannelPtsLocked(req.ChannelID)
channel.PinnedMessageID = s.latestPinnedMessageIDLocked(req.ChannelID)
channel.Pts = pts
s.channels[req.ChannelID] = channel
event := domain.ChannelUpdateEvent{
ChannelID: req.ChannelID,
Type: domain.ChannelUpdatePinnedMessages,
Pts: pts,
PtsCount: 1,
Date: req.Date,
MessageIDs: []int{req.MessageID},
SenderUserID: req.UserID,
Pinned: req.Pinned,
}
s.events[req.ChannelID] = append(s.events[req.ChannelID], event)
logMsg := msg
logMsg.Pinned = req.Pinned
s.appendChannelAdminLogLocked(domain.ChannelAdminLogEvent{
ChannelID: req.ChannelID,
UserID: req.UserID,
Date: req.Date,
Type: domain.ChannelAdminLogUpdatePinned,
Message: ptrChannelMessage(logMsg),
Query: msg.Body,
})
return domain.UpdateChannelPinnedMessageResult{
Channel: channel,
Event: cloneChannelEvent(event),
Recipients: s.activeMemberIDsLocked(req.ChannelID, 0, 0),
}, nil
}
// latestPinnedMessageIDLocked 返回最新置顶消息 idchannelFull.pinned_msg_id 缓存语义)。
func (s *ChannelStore) latestPinnedMessageIDLocked(channelID int64) int {
latest := 0
for _, m := range s.messages[channelID] {
if m.Pinned && !m.Deleted && m.ID > latest {
latest = m.ID
}
}
return latest
}
// UnpinAllChannelMessages 清空全部置顶并以单条 channel 事件携带全部 id。
func (s *ChannelStore) UnpinAllChannelMessages(_ context.Context, req domain.UnpinAllChannelMessagesRequest) (domain.UpdateChannelPinnedMessageResult, error) {
if req.UserID == 0 || req.ChannelID == 0 {
return domain.UpdateChannelPinnedMessageResult{}, domain.ErrChannelInvalid
}
s.mu.Lock()
defer s.mu.Unlock()
channel, err := s.channelForMemberLocked(req.UserID, req.ChannelID)
if err != nil {
return domain.UpdateChannelPinnedMessageResult{}, err
}
member := s.members[req.ChannelID][req.UserID]
if !canPinChannelMessages(channel, member) {
return domain.UpdateChannelPinnedMessageResult{}, domain.ErrChannelAdminRequired
}
cleared := []int(nil)
for i := range s.messages[req.ChannelID] {
if s.messages[req.ChannelID][i].Pinned && !s.messages[req.ChannelID][i].Deleted {
s.messages[req.ChannelID][i].Pinned = false
cleared = append(cleared, s.messages[req.ChannelID][i].ID)
}
}
if len(cleared) == 0 {
return domain.UpdateChannelPinnedMessageResult{}, domain.ErrChannelNotModified
}
sort.Ints(cleared)
pts := s.nextChannelPtsLocked(req.ChannelID)
channel.PinnedMessageID = 0
channel.Pts = pts
s.channels[req.ChannelID] = channel
event := domain.ChannelUpdateEvent{
ChannelID: req.ChannelID,
Type: domain.ChannelUpdatePinnedMessages,
Pts: pts,
PtsCount: 1,
Date: req.Date,
MessageIDs: cleared,
SenderUserID: req.UserID,
Pinned: false,
}
s.events[req.ChannelID] = append(s.events[req.ChannelID], event)
return domain.UpdateChannelPinnedMessageResult{
Channel: channel,
Event: cloneChannelEvent(event),
Recipients: s.activeMemberIDsLocked(req.ChannelID, 0, 0),
}, nil
}
// ClearDanglingPinnedMessage 把指向已删除消息的置顶值清零unpinAll 自愈)。
func (s *ChannelStore) ClearDanglingPinnedMessage(_ context.Context, channelID int64, messageID int) error {
if channelID == 0 || messageID <= 0 {
return domain.ErrChannelInvalid
}
s.mu.Lock()
defer s.mu.Unlock()
channel, ok := s.channels[channelID]
if !ok {
return domain.ErrChannelInvalid
}
if channel.PinnedMessageID == messageID {
channel.PinnedMessageID = 0
s.channels[channelID] = channel
}
return nil
}
func canPinChannelMessages(channel domain.Channel, member domain.ChannelMember) bool {
if member.Role == domain.ChannelRoleCreator || (member.Role == domain.ChannelRoleAdmin && member.AdminRights.PinMessages) {
return true
}
return channel.Megagroup && !channel.DefaultBannedRights.PinMessages && !member.BannedRights.PinMessages
}
func canEditChannelMessage(member domain.ChannelMember) bool {
return member.Role == domain.ChannelRoleCreator || (member.Role == domain.ChannelRoleAdmin && member.AdminRights.EditMessages)
}