status-go/protocol/messenger_messages.go
Jonathan Rainville 92ba63b282
fix(edit)_: make sure the contentType stays the same after an edit (#6133)
Fixes https://github.com/status-im/status-desktop/issues/16741

The issue was that in image messages, you can update the text, but then the ContentType would become Text and lose the image.
The solution is to ignore ContentType changes, since there is no way to change the type of message.
2024-12-03 10:04:21 -05:00

567 lines
15 KiB
Go

package protocol
import (
"context"
"crypto/ecdsa"
"errors"
"github.com/golang/protobuf/proto"
"github.com/status-im/status-go/protocol/common"
"github.com/status-im/status-go/protocol/protobuf"
"github.com/status-im/status-go/protocol/requests"
"go.uber.org/zap"
)
var ErrInvalidEditOrDeleteAuthor = errors.New("sender is not the author of the message")
var ErrInvalidDeleteTypeAuthor = errors.New("message type cannot be deleted")
var ErrInvalidEditContentType = errors.New("only text or emoji messages can be replaced")
var ErrInvalidDeletePermission = errors.New("don't have enough permission to delete")
func (m *Messenger) EditMessage(ctx context.Context, request *requests.EditMessage) (*MessengerResponse, error) {
err := request.Validate()
if err != nil {
return nil, err
}
message, err := m.persistence.MessageByID(request.ID.String())
if err != nil {
return nil, err
}
if message.From != common.PubkeyToHex(&m.identity.PublicKey) {
return nil, ErrInvalidEditOrDeleteAuthor
}
if message.ContentType != protobuf.ChatMessage_TEXT_PLAIN && message.ContentType != protobuf.ChatMessage_EMOJI && message.ContentType != protobuf.ChatMessage_IMAGE && message.ContentType != protobuf.ChatMessage_BRIDGE_MESSAGE {
return nil, ErrInvalidEditContentType
}
// A valid added chat is required.
chat, ok := m.allChats.Load(message.ChatId)
if !ok {
return nil, ErrChatNotFound
}
messages, err := m.getOtherMessagesInAlbum(message, message.LocalChatID)
if err != nil {
return nil, err
}
response := &MessengerResponse{}
for _, message := range messages {
//Add LinkPreviews only to first message
if len(request.LinkPreviews) > 0 {
message.LinkPreviews = request.LinkPreviews
}
if len(request.StatusLinkPreviews) > 0 {
message.StatusLinkPreviews = request.StatusLinkPreviews
}
clock, _ := chat.NextClockAndTimestamp(m.getTimesource())
editMessage := NewEditMessage()
replacedText, err := m.mentionsManager.ReplaceWithPublicKey(message.ChatId, request.Text)
if err != nil {
m.logger.Error("failed to replace text with public key", zap.String("chatID", message.ChatId), zap.String("text", request.Text))
// use original text as fallback
replacedText = request.Text
}
editMessage.Text = replacedText
editMessage.ContentType = message.ContentType // The contentType cannot change
editMessage.ChatId = message.ChatId
editMessage.MessageId = message.ID
editMessage.Clock = clock
// We consider link previews non-critical data, so we do not want to block
// messages from being sent.
unfurledLinks, err := message.ConvertLinkPreviewsToProto()
if err != nil {
m.logger.Error("failed to convert link previews", zap.Error(err))
} else {
editMessage.UnfurledLinks = unfurledLinks
}
unfurledStatusLinks, err := message.ConvertStatusLinkPreviewsToProto()
if err != nil {
m.logger.Error("failed to convert status link previews", zap.Error(err))
} else {
editMessage.UnfurledStatusLinks = unfurledStatusLinks
}
err = m.applyEditMessage(editMessage.EditMessage, message)
if err != nil {
return nil, err
}
encodedMessage, err := m.encodeChatEntity(chat, editMessage)
if err != nil {
return nil, err
}
rawMessage := common.RawMessage{
LocalChatID: chat.ID,
Payload: encodedMessage,
MessageType: protobuf.ApplicationMetadataMessage_EDIT_MESSAGE,
SkipGroupMessageWrap: true,
ResendType: chat.DefaultResendType(),
}
_, err = m.dispatchMessage(ctx, rawMessage)
if err != nil {
return nil, err
}
if chat.LastMessage != nil && chat.LastMessage.ID == message.ID {
chat.LastMessage = message
err := m.saveChat(chat)
if err != nil {
return nil, err
}
}
response.AddMessage(message)
}
// pull updated messages
updatedMessages, err := m.persistence.MessagesByResponseTo(request.ID.String())
if err != nil {
return nil, err
}
response.AddMessages(updatedMessages)
err = m.prepareMessages(response.messages)
if err != nil {
return nil, err
}
response.AddChat(chat)
return response, nil
}
func (m *Messenger) CanDeleteMessageForEveryoneInCommunity(communityID string, publicKey *ecdsa.PublicKey) bool {
if communityID != "" {
community, err := m.communitiesManager.GetByIDString(communityID)
if err != nil {
m.logger.Error("failed to find community", zap.String("communityID", communityID), zap.Error(err))
return false
}
return community.CanDeleteMessageForEveryone(publicKey)
}
return false
}
func (m *Messenger) CanDeleteMessageForEveryoneInPrivateGroupChat(chat *Chat, publicKey *ecdsa.PublicKey) bool {
group, err := newProtocolGroupFromChat(chat)
if err != nil {
m.logger.Error("failed to find group", zap.String("chatID", chat.ID), zap.Error(err))
return false
}
admins := group.Admins()
return stringSliceContains(admins, common.PubkeyToHex(publicKey))
}
func (m *Messenger) DeleteMessageAndSend(ctx context.Context, messageID string) (*MessengerResponse, error) {
message, err := m.persistence.MessageByID(messageID)
if err != nil {
return nil, err
}
// A valid added chat is required.
chat, ok := m.allChats.Load(message.ChatId)
if !ok {
return nil, ErrChatNotFound
}
var canDeleteMessageForEveryone = false
var deletedBy string
if message.From != common.PubkeyToHex(&m.identity.PublicKey) {
if message.MessageType == protobuf.MessageType_COMMUNITY_CHAT {
communityID := chat.CommunityID
canDeleteMessageForEveryone = m.CanDeleteMessageForEveryoneInCommunity(communityID, &m.identity.PublicKey)
if !canDeleteMessageForEveryone {
return nil, ErrInvalidDeletePermission
}
} else if message.MessageType == protobuf.MessageType_PRIVATE_GROUP {
canDeleteMessageForEveryone = m.CanDeleteMessageForEveryoneInPrivateGroupChat(chat, &m.identity.PublicKey)
if !canDeleteMessageForEveryone {
return nil, ErrInvalidDeletePermission
}
}
// only add DeletedBy when not deleted by message.From
deletedBy = contactIDFromPublicKey(m.IdentityPublicKey())
if !canDeleteMessageForEveryone {
return nil, ErrInvalidEditOrDeleteAuthor
}
}
// Only certain types of messages can be deleted
if message.ContentType != protobuf.ChatMessage_TEXT_PLAIN &&
message.ContentType != protobuf.ChatMessage_BRIDGE_MESSAGE &&
message.ContentType != protobuf.ChatMessage_STICKER &&
message.ContentType != protobuf.ChatMessage_EMOJI &&
message.ContentType != protobuf.ChatMessage_IMAGE &&
message.ContentType != protobuf.ChatMessage_AUDIO {
return nil, ErrInvalidDeleteTypeAuthor
}
messagesToDelete, err := m.getOtherMessagesInAlbum(message, message.ChatId)
if err != nil {
return nil, err
}
clock, _ := chat.NextClockAndTimestamp(m.getTimesource())
deleteMessage := NewDeleteMessage()
deleteMessage.ChatId = message.ChatId
deleteMessage.MessageId = messageID
deleteMessage.Clock = clock
deleteMessage.DeletedBy = deletedBy
encodedMessage, err := m.encodeChatEntity(chat, deleteMessage)
if err != nil {
return nil, err
}
rawMessage := common.RawMessage{
LocalChatID: chat.ID,
Payload: encodedMessage,
MessageType: protobuf.ApplicationMetadataMessage_DELETE_MESSAGE,
SkipGroupMessageWrap: true,
ResendType: chat.DefaultResendType(),
}
_, err = m.dispatchMessage(ctx, rawMessage)
if err != nil {
return nil, err
}
response := &MessengerResponse{}
for _, messageToDelete := range messagesToDelete {
messageToDelete.Deleted = true
messageToDelete.DeletedBy = deletedBy
err = m.persistence.SaveMessages([]*common.Message{messageToDelete})
if err != nil {
return nil, err
}
response.AddMessage(messageToDelete)
response.AddRemovedMessage(&RemovedMessage{MessageID: messageToDelete.ID, ChatID: chat.ID, DeletedBy: deletedBy})
if chat.LastMessage != nil && chat.LastMessage.ID == messageToDelete.ID {
chat.LastMessage = messageToDelete
err = m.saveChat(chat)
if err != nil {
return nil, err
}
}
// pull updated messages
updatedMessages, err := m.persistence.MessagesByResponseTo(messageToDelete.ID)
if err != nil {
return nil, err
}
response.AddMessages(updatedMessages)
err = m.prepareMessages(response.messages)
if err != nil {
return nil, err
}
}
response.AddChat(chat)
return response, nil
}
func (m *Messenger) DeleteMessageForMeAndSync(ctx context.Context, localChatID string, messageID string) (*MessengerResponse, error) {
message, err := m.persistence.MessageByID(messageID)
if err != nil {
return nil, err
}
// A valid added chat is required.
chat, ok := m.allChats.Load(localChatID)
if !ok {
return nil, ErrChatNotFound
}
// Only certain types of messages can be deleted
if message.ContentType != protobuf.ChatMessage_TEXT_PLAIN &&
message.ContentType != protobuf.ChatMessage_STICKER &&
message.ContentType != protobuf.ChatMessage_EMOJI &&
message.ContentType != protobuf.ChatMessage_IMAGE &&
message.ContentType != protobuf.ChatMessage_AUDIO {
return nil, ErrInvalidDeleteTypeAuthor
}
messagesToDelete, err := m.getOtherMessagesInAlbum(message, localChatID)
if err != nil {
return nil, err
}
response := &MessengerResponse{}
for _, messageToDelete := range messagesToDelete {
messageToDelete.DeletedForMe = true
err = m.persistence.SaveMessages([]*common.Message{messageToDelete})
if err != nil {
return nil, err
}
if chat.LastMessage != nil && chat.LastMessage.ID == messageToDelete.ID {
chat.LastMessage = messageToDelete
err = m.saveChat(chat)
if err != nil {
return nil, err
}
}
response.AddMessage(messageToDelete)
// pull updated messages
updatedMessages, err := m.persistence.MessagesByResponseTo(messageToDelete.ID)
if err != nil {
return nil, err
}
response.AddMessages(updatedMessages)
err = m.prepareMessages(response.messages)
if err != nil {
return nil, err
}
}
response.AddChat(chat)
err = m.withChatClock(func(chatID string, clock uint64) error {
deletedForMeMessage := &protobuf.SyncDeleteForMeMessage{
MessageId: messageID,
Clock: clock,
}
if m.hasPairedDevices() {
encodedMessage, err2 := proto.Marshal(deletedForMeMessage)
if err2 != nil {
return err2
}
rawMessage := common.RawMessage{
LocalChatID: chatID,
Payload: encodedMessage,
MessageType: protobuf.ApplicationMetadataMessage_SYNC_DELETE_FOR_ME_MESSAGE,
ResendType: common.ResendTypeDataSync,
}
_, err2 = m.dispatchMessage(ctx, rawMessage)
return err2
}
return m.persistence.SaveOrUpdateDeleteForMeMessage(deletedForMeMessage)
})
if err != nil {
return response, err
}
return response, nil
}
func (m *Messenger) applyEditMessage(editMessage *protobuf.EditMessage, message *common.Message) error {
if err := ValidateText(editMessage.Text); err != nil {
return err
}
if editMessage.ContentType != protobuf.ChatMessage_BRIDGE_MESSAGE {
message.Text = editMessage.Text
} else {
message.GetBridgeMessage().Content = editMessage.Text
}
message.EditedAt = editMessage.Clock
message.UnfurledLinks = editMessage.UnfurledLinks
message.UnfurledStatusLinks = editMessage.UnfurledStatusLinks
if editMessage.ContentType != protobuf.ChatMessage_UNKNOWN_CONTENT_TYPE {
message.ContentType = editMessage.ContentType
}
// Save original message as edit so we can retrieve history
if message.EditedAt == 0 {
originalEdit := NewEditMessage()
originalEdit.Clock = message.Clock
originalEdit.LocalChatID = message.LocalChatID
originalEdit.MessageId = message.ID
originalEdit.Text = message.Text
originalEdit.ContentType = message.ContentType
originalEdit.From = message.From
originalEdit.UnfurledLinks = message.UnfurledLinks
originalEdit.UnfurledStatusLinks = message.UnfurledStatusLinks
err := m.persistence.SaveEdit(originalEdit)
if err != nil {
return err
}
}
err := message.PrepareContent(common.PubkeyToHex(&m.identity.PublicKey))
if err != nil {
return err
}
return m.persistence.SaveMessages([]*common.Message{message})
}
func (m *Messenger) applyDeleteMessage(messageDeletes []*DeleteMessage, message *common.Message) error {
if messageDeletes[0].From != message.From {
return ErrInvalidEditOrDeleteAuthor
}
message.Deleted = true
message.DeletedBy = messageDeletes[0].DeletedBy
err := message.PrepareContent(common.PubkeyToHex(&m.identity.PublicKey))
if err != nil {
return err
}
err = m.persistence.SaveMessages([]*common.Message{message})
if err != nil {
return err
}
return nil
}
func (m *Messenger) applyDeleteForMeMessage(message *common.Message) error {
message.DeletedForMe = true
err := message.PrepareContent(m.myHexIdentity())
if err != nil {
return err
}
err = m.persistence.SaveMessages([]*common.Message{message})
if err != nil {
return err
}
return nil
}
func (m *Messenger) addContactRequestPropagatedState(message *common.Message) error {
chat, ok := m.allChats.Load(message.LocalChatID)
if !ok {
return ErrChatNotFound
}
if !chat.OneToOne() {
return nil
}
contact, err := m.BuildContact(&requests.BuildContact{PublicKey: chat.ID})
if err != nil {
return err
}
message.ContactRequestPropagatedState = contact.ContactRequestPropagatedState()
return nil
}
func (m *Messenger) SendOneToOneMessage(request *requests.SendOneToOneMessage) (*MessengerResponse, error) {
if err := request.Validate(); err != nil {
return nil, err
}
chatID, err := request.HexID()
if err != nil {
return nil, err
}
_, ok := m.allChats.Load(chatID)
if !ok {
// Only one to one chan be muted when it's not in the database
publicKey, err := common.HexToPubkey(chatID)
if err != nil {
return nil, err
}
// Create a one to one chat
chat := CreateOneToOneChat(chatID, publicKey, m.getTimesource())
err = m.initChatSyncFields(chat)
if err != nil {
return nil, err
}
err = m.saveChat(chat)
if err != nil {
return nil, err
}
}
message := common.NewMessage()
message.Text = request.Message
message.ChatId = chatID
message.ContentType = protobuf.ChatMessage_TEXT_PLAIN
return m.sendChatMessage(context.Background(), message)
}
func (m *Messenger) SendGroupChatMessage(request *requests.SendGroupChatMessage) (*MessengerResponse, error) {
if err := request.Validate(); err != nil {
return nil, err
}
chatID := request.ID
_, ok := m.allChats.Load(chatID)
if !ok {
return nil, ErrChatNotFound
}
message := common.NewMessage()
message.Text = request.Message
message.ChatId = chatID
message.ContentType = protobuf.ChatMessage_TEXT_PLAIN
return m.sendChatMessage(context.Background(), message)
}
func (m *Messenger) deleteCommunityMemberMessages(member string, communityID string, deleteMessages []*protobuf.DeleteCommunityMemberMessage) (*MessengerResponse, error) {
messagesToDelete := deleteMessages
var err error
if len(deleteMessages) == 0 {
messagesToDelete, err = m.persistence.GetCommunityMemberMessagesToDelete(member, communityID)
if err != nil {
return nil, err
}
}
response := &MessengerResponse{}
if len(messagesToDelete) == 0 {
return response, nil
}
for _, messageToDelete := range messagesToDelete {
updatedMessages, err := m.persistence.MessagesByResponseTo(messageToDelete.Id)
if err != nil {
return nil, err
}
response.AddMessages(updatedMessages)
}
response.AddDeletedMessages(messagesToDelete)
messageIDs := make([]string, 0, len(messagesToDelete))
for _, rm := range messagesToDelete {
messageIDs = append(messageIDs, rm.Id)
}
if err = m.persistence.DeleteMessages(messageIDs); err != nil {
return nil, err
}
return response, nil
}