feat: sync rich text message support
This commit is contained in:
parent
4d3bbeabd8
commit
7c9d8dda16
41 changed files with 1164 additions and 94 deletions
|
|
@ -9,8 +9,7 @@ import (
|
|||
)
|
||||
|
||||
func (s *ChannelStore) EditChannelMessage(_ context.Context, req domain.EditChannelMessageRequest) (domain.EditChannelMessageResult, error) {
|
||||
// 空文本只在媒体替换(live location 续报/停止)时合法。
|
||||
if req.UserID == 0 || req.ChannelID == 0 || req.ID <= 0 || (strings.TrimSpace(req.Message) == "" && req.Media == nil) {
|
||||
if req.UserID == 0 || req.ChannelID == 0 || req.ID <= 0 {
|
||||
return domain.EditChannelMessageResult{}, domain.ErrChannelInvalid
|
||||
}
|
||||
s.mu.Lock()
|
||||
|
|
@ -26,6 +25,17 @@ func (s *ChannelStore) EditChannelMessage(_ context.Context, req domain.EditChan
|
|||
}
|
||||
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 {
|
||||
|
|
@ -67,7 +77,8 @@ func (s *ChannelStore) EditChannelMessage(_ context.Context, req domain.EditChan
|
|||
if !viaBotEdit && msg.SenderUserID != req.UserID && !canEditChannelMessage(member) && !participantTodoEdit {
|
||||
return domain.EditChannelMessageResult{}, domain.ErrMessageAuthorRequired
|
||||
}
|
||||
if req.Media == nil && !req.SetReplyMarkup && msg.Body == req.Message && sameMessageEntities(msg.Entities, req.Entities) {
|
||||
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)
|
||||
|
|
@ -80,6 +91,9 @@ func (s *ChannelStore) EditChannelMessage(_ context.Context, req domain.EditChan
|
|||
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
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ func cloneChannelMessage(in domain.ChannelMessage) domain.ChannelMessage {
|
|||
in.ReplyTo = cloneMessageReply(in.ReplyTo)
|
||||
in.Forward = cloneMessageForward(in.Forward)
|
||||
in.ReplyMarkup = cloneReplyMarkup(in.ReplyMarkup)
|
||||
in.RichMessage = cloneRichMessage(in.RichMessage)
|
||||
in.Discussion = cloneChannelDiscussionRef(in.Discussion)
|
||||
in.Replies = cloneChannelMessageReplies(in.Replies)
|
||||
in.Reactions = cloneChannelMessageReactionsPtr(in.Reactions)
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ func (s *ChannelStore) SendChannelMessage(_ context.Context, req domain.SendChan
|
|||
if req.UserID == 0 || req.ChannelID == 0 {
|
||||
return domain.SendChannelMessageResult{}, domain.ErrChannelInvalid
|
||||
}
|
||||
if strings.TrimSpace(req.Message) == "" && req.Action == nil && req.Media.IsZero() {
|
||||
if strings.TrimSpace(req.Message) == "" && req.Action == nil && req.Media.IsZero() && req.RichMessage.IsZero() {
|
||||
return domain.SendChannelMessageResult{}, domain.ErrChannelInvalid
|
||||
}
|
||||
s.mu.Lock()
|
||||
|
|
@ -83,6 +83,7 @@ func (s *ChannelStore) SendChannelMessage(_ context.Context, req domain.SendChan
|
|||
NoForwards: req.NoForwards || channel.NoForwards || linked.NoForwards,
|
||||
Body: req.Message,
|
||||
Entities: append([]domain.MessageEntity(nil), req.Entities...),
|
||||
RichMessage: cloneRichMessage(req.RichMessage),
|
||||
Forward: &domain.MessageForward{From: domain.Peer{Type: domain.PeerTypeChannel, ID: channel.ID}, Date: req.Date, ChannelPost: msgID, SavedFrom: domain.Peer{Type: domain.PeerTypeChannel, ID: channel.ID}, SavedFromMsgID: msgID},
|
||||
ViaBotID: req.ViaBotID,
|
||||
GroupedID: req.GroupedID,
|
||||
|
|
@ -131,6 +132,7 @@ func (s *ChannelStore) SendChannelMessage(_ context.Context, req domain.SendChan
|
|||
Body: req.Message,
|
||||
Entities: append([]domain.MessageEntity(nil), req.Entities...),
|
||||
Media: req.Media,
|
||||
RichMessage: cloneRichMessage(req.RichMessage),
|
||||
ReplyTo: replyTo,
|
||||
Forward: cloneMessageForward(req.Forward),
|
||||
ViaBotID: req.ViaBotID,
|
||||
|
|
|
|||
|
|
@ -840,6 +840,7 @@ func cloneDialogDraft(draft domain.DialogDraft) domain.DialogDraft {
|
|||
webpage := *draft.WebPage
|
||||
draft.WebPage = &webpage
|
||||
}
|
||||
draft.RichMessage = cloneRichMessage(draft.RichMessage)
|
||||
return draft
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -41,10 +41,19 @@ func (s *MessageStore) EditMessage(_ context.Context, req domain.EditMessageRequ
|
|||
return res, domain.ErrMessageNotModified
|
||||
}
|
||||
}
|
||||
if req.Message == "" && req.Media == nil && target.Media.IsZero() {
|
||||
finalMedia := target.Media
|
||||
if req.Media != nil {
|
||||
finalMedia = req.Media
|
||||
}
|
||||
finalRich := target.RichMessage
|
||||
if req.SetRichMessage {
|
||||
finalRich = req.RichMessage
|
||||
}
|
||||
if req.Message == "" && finalMedia.IsZero() && finalRich.IsZero() {
|
||||
return res, domain.ErrMessageEmpty
|
||||
}
|
||||
if req.Media == nil && !req.SetReplyMarkup && target.Body == req.Message && target.HideEdited == req.HideEdited && equalMessageEntities(target.Entities, req.Entities) {
|
||||
richChanged := req.SetRichMessage && !richMessagesEqual(target.RichMessage, req.RichMessage)
|
||||
if req.Media == nil && !req.SetReplyMarkup && !richChanged && target.Body == req.Message && target.HideEdited == req.HideEdited && equalMessageEntities(target.Entities, req.Entities) {
|
||||
return res, domain.ErrMessageNotModified
|
||||
}
|
||||
messageSenderID := target.From.ID
|
||||
|
|
@ -73,6 +82,9 @@ func (s *MessageStore) EditMessage(_ context.Context, req domain.EditMessageRequ
|
|||
// 替换 markup(nil/空 = 清空键盘);双盒一致。
|
||||
msg.ReplyMarkup = cloneReplyMarkup(req.ReplyMarkup)
|
||||
}
|
||||
if req.SetRichMessage {
|
||||
msg.RichMessage = cloneRichMessage(req.RichMessage)
|
||||
}
|
||||
msg.EditDate = req.EditDate
|
||||
msg.HideEdited = req.HideEdited
|
||||
msg.Pts = s.nextPtsLocked(userID)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package memory
|
|||
import (
|
||||
"encoding/binary"
|
||||
"hash/fnv"
|
||||
"reflect"
|
||||
"sort"
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
|
@ -118,6 +119,13 @@ func cloneRichMessage(m *domain.MessageRichMessage) *domain.MessageRichMessage {
|
|||
return &clone
|
||||
}
|
||||
|
||||
func richMessagesEqual(a, b *domain.MessageRichMessage) bool {
|
||||
if a.IsZero() && b.IsZero() {
|
||||
return true
|
||||
}
|
||||
return reflect.DeepEqual(a, b)
|
||||
}
|
||||
|
||||
func cloneMessageReply(reply *domain.MessageReply) *domain.MessageReply {
|
||||
if reply == nil {
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -9,6 +9,12 @@ import (
|
|||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
var validRichMessageBlocks = []byte{
|
||||
0x15, 0xc4, 0xb5, 0x1c, // vector
|
||||
0x01, 0x00, 0x00, 0x00, // count
|
||||
0x8a, 0x7e, 0x56, 0x13, // pageBlockUnsupported
|
||||
}
|
||||
|
||||
func TestMessageStoreSendPrivateTextCreatesBothOwnerBoxes(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
dialogs := NewDialogStore()
|
||||
|
|
@ -84,6 +90,41 @@ func TestMessageStoreSendPrivateTextCreatesBothOwnerBoxes(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestMessageStoreEditRichOnlyMessageUsesFinalContentState(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
messages := NewMessageStore()
|
||||
sent, err := messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
|
||||
SenderUserID: 1000000001,
|
||||
RecipientUserID: 1000000002,
|
||||
RandomID: 201,
|
||||
Date: 1700000200,
|
||||
RichMessage: &domain.MessageRichMessage{Blocks: validRichMessageBlocks},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("SendPrivateText rich-only: %v", err)
|
||||
}
|
||||
|
||||
edited, err := messages.EditMessage(ctx, domain.EditMessageRequest{
|
||||
OwnerUserID: 1000000001,
|
||||
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: 1000000002},
|
||||
ID: sent.SenderMessage.ID,
|
||||
Message: "",
|
||||
HideEdited: true,
|
||||
EditDate: 1700000210,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("EditMessage rich-only hide-edit: %v", err)
|
||||
}
|
||||
if len(edited.Edited) != 2 {
|
||||
t.Fatalf("edited boxes = %d, want 2", len(edited.Edited))
|
||||
}
|
||||
for _, item := range edited.Edited {
|
||||
if item.Message.RichMessage.IsZero() {
|
||||
t.Fatalf("edited rich-only box for user %d lost rich message: %+v", item.UserID, item.Message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageStoreWebViewDataServiceActionRoundTrip(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
messages := NewMessageStore()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue