package memory import ( "context" "sort" "telesrv/internal/domain" "time" ) func (s *MessageStore) EditMessage(_ context.Context, req domain.EditMessageRequest) (domain.EditMessageResult, error) { res := domain.EditMessageResult{OwnerUserID: req.OwnerUserID} if req.OwnerUserID == 0 || req.Peer.ID == 0 || req.ID <= 0 || req.ID > domain.MaxMessageBoxID { return res, domain.ErrMessageIDInvalid } if req.EditDate == 0 { req.EditDate = int(time.Now().Unix()) } s.mu.Lock() defer s.mu.Unlock() targetIndex := -1 var target domain.Message for i, msg := range s.m[req.OwnerUserID] { if msg.ID == req.ID && msg.Peer == req.Peer { targetIndex = i target = msg break } } if targetIndex < 0 { return res, domain.ErrMessageIDInvalid } authorEdit := target.Out && target.From.ID == req.OwnerUserID viaBotEdit := req.ViaBotEditBotID != 0 && target.ViaBotID == req.ViaBotEditBotID if !authorEdit && !viaBotEdit && !req.WebPageResolve && !validMemoryTodoParticipantEdit(req, target) { return res, domain.ErrMessageAuthorRequired } // WebPageResolve:服务端内部链接预览就地替换。幂等——仅当目标当前 media 仍是匹配 id 的 // pending 占位才替换;只换 media、不碰 body/entities/edit_date,事件为 web_page。 if req.WebPageResolve { if req.Media == nil || !domain.IsPendingWebPageMedia(target.Media, req.ExpectedWebPageID) { return res, domain.ErrMessageNotModified } } if req.Message == "" && req.Media == nil && target.Media.IsZero() { return res, domain.ErrMessageEmpty } if req.Media == nil && !req.SetReplyMarkup && target.Body == req.Message && equalMessageEntities(target.Entities, req.Entities) { return res, domain.ErrMessageNotModified } messageSenderID := target.From.ID for userID, messages := range s.m { for i, msg := range messages { if msg.UID == target.UID && msg.From.ID == messageSenderID { if req.WebPageResolve { media := *req.Media msg.Media = &media msg.Pts = s.nextPtsLocked(userID) s.m[userID][i] = msg res.Edited = append(res.Edited, domain.EditedMessageForUser{ UserID: userID, Message: cloneMessage(msg), Event: webPageEvent(msg), }) continue } 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 { // 替换 markup(nil/空 = 清空键盘);双盒一致。 msg.ReplyMarkup = cloneReplyMarkup(req.ReplyMarkup) } msg.EditDate = req.EditDate msg.Pts = s.nextPtsLocked(userID) s.m[userID][i] = msg event := editMessageEvent(msg) res.Edited = append(res.Edited, domain.EditedMessageForUser{ UserID: userID, Message: cloneMessage(msg), Event: event, }) } } } if s.dialogs != nil { s.dialogs.mu.Lock() for userID := range s.dialogs.m { list := s.dialogs.m[userID] list.Messages = cloneMessages(s.m[userID]) s.dialogs.m[userID] = list } s.dialogs.mu.Unlock() } sort.Slice(res.Edited, func(i, j int) bool { return res.Edited[i].UserID < res.Edited[j].UserID }) return res, nil } func validMemoryTodoParticipantEdit(req domain.EditMessageRequest, target domain.Message) bool { if !req.AllowTodoParticipantMutation || req.SetReplyMarkup || req.Media == nil || req.Media.Kind != domain.MessageMediaKindTodo || req.Media.Todo == nil { return false } if target.From.ID == req.OwnerUserID { return false } if target.Body != req.Message || !equalMessageEntities(target.Entities, req.Entities) { return false } if target.Media == nil || target.Media.Kind != domain.MessageMediaKindTodo || target.Media.Todo == nil { return false } return true }