mirror of
https://github.com/status-im/status-go.git
synced 2025-01-21 20:20:29 +00:00
92ba63b282
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.
567 lines
15 KiB
Go
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
|
|
}
|