diff --git a/VERSION b/VERSION index dee8ea10e..fb1b5e098 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.145.0 +0.145.1 diff --git a/protocol/messenger.go b/protocol/messenger.go index e6fc2f892..5712f22e5 100644 --- a/protocol/messenger.go +++ b/protocol/messenger.go @@ -6176,3 +6176,19 @@ func (m *Messenger) myHexIdentity() string { func (m *Messenger) GetMentionsManager() *MentionManager { return m.mentionsManager } + +func (m *Messenger) getMessagesToDelete(message *common.Message, chatID string) ([]*common.Message, error) { + var messagesToDelete []*common.Message + // In case of Image messages, we need to delete all the images in the album + if message.ContentType == protobuf.ChatMessage_IMAGE { + image := message.GetImage() + messagesInTheAlbum, err := m.persistence.albumMessages(chatID, image.GetAlbumId()) + if err != nil { + return nil, err + } + messagesToDelete = append(messagesToDelete, messagesInTheAlbum...) + } else { + messagesToDelete = append(messagesToDelete, message) + } + return messagesToDelete, nil +} diff --git a/protocol/messenger_delete_message_test.go b/protocol/messenger_delete_message_test.go index 4939fd8cc..53909c73a 100644 --- a/protocol/messenger_delete_message_test.go +++ b/protocol/messenger_delete_message_test.go @@ -238,3 +238,156 @@ func (s *MessengerDeleteMessageSuite) TestDeleteMessageFirstThenMessage() { s.Require().Len(state.Response.RemovedMessages(), 0) s.Require().Nil(state.Response.Chats()[0].LastMessage) } + +func (s *MessengerDeleteMessageSuite) TestDeleteImageMessage() { + theirMessenger := s.newMessenger() + _, err := theirMessenger.Start() + s.Require().NoError(err) + + theirChat := CreateOneToOneChat("Their 1TO1", &s.privateKey.PublicKey, s.m.transport) + err = theirMessenger.SaveChat(theirChat) + s.Require().NoError(err) + + ourChat := CreateOneToOneChat("Our 1TO1", &theirMessenger.identity.PublicKey, s.m.transport) + err = s.m.SaveChat(ourChat) + s.Require().NoError(err) + + messageCount := 3 + var album []*common.Message + for i := 0; i < messageCount; i++ { + image, err := buildImageWithoutAlbumIDMessage(*ourChat) + s.NoError(err) + album = append(album, image) + } + + response, err := s.m.SendChatMessages(context.Background(), album) + s.NoError(err) + + // Check that album count was the number of the images sent + imagesCount := uint32(0) + for _, message := range response.Messages() { + if message.ContentType == protobuf.ChatMessage_IMAGE { + imagesCount++ + } + } + for _, message := range response.Messages() { + s.Require().NotNil(message.GetImage()) + s.Require().Equal(message.GetImage().AlbumImagesCount, imagesCount) + } + + s.Require().Equal(messageCount, len(response.Messages()), "it returns the messages") + s.Require().NoError(err) + s.Require().Len(response.Messages(), messageCount) + + response, err = WaitOnMessengerResponse( + theirMessenger, + func(r *MessengerResponse) bool { return len(r.messages) == messageCount }, + "no messages", + ) + + s.Require().NoError(err) + s.Require().Len(response.Chats(), 1) + s.Require().Len(response.Messages(), messageCount) + for _, message := range response.Messages() { + image := message.GetImage() + s.Require().NotNil(image, "Message.ID=%s", message.ID) + s.Require().Equal(image.AlbumImagesCount, imagesCount) + s.Require().NotEmpty(image.AlbumId, "Message.ID=%s", message.ID) + } + + firstMessageID := response.Messages()[0].ID + sendResponse, err := s.m.DeleteMessageAndSend(context.Background(), firstMessageID) + + s.Require().NoError(err) + s.Require().Len(sendResponse.Messages(), 0) + s.Require().Len(sendResponse.RemovedMessages(), 3) + s.Require().Equal(sendResponse.RemovedMessages()[0].DeletedBy, "") + s.Require().Len(sendResponse.Chats(), 1) + // LastMessage is removed + s.Require().Nil(sendResponse.Chats()[0].LastMessage) + + // Main instance user attempts to delete the message it received from theirMessenger + _, err = theirMessenger.DeleteMessageAndSend(context.Background(), firstMessageID) + + s.Require().ErrorContains(err, "Chat not found") +} + +func (s *MessengerDeleteMessageSuite) TestDeleteImageMessageFirstThenMessage() { + theirMessenger := s.newMessenger() + _, err := theirMessenger.Start() + s.Require().NoError(err) + + theirChat := CreateOneToOneChat("Their 1TO1", &s.privateKey.PublicKey, s.m.transport) + err = theirMessenger.SaveChat(theirChat) + s.Require().NoError(err) + + contact, err := BuildContactFromPublicKey(&theirMessenger.identity.PublicKey) + s.Require().NoError(err) + + ourChat := CreateOneToOneChat("Our 1TO1", &theirMessenger.identity.PublicKey, s.m.transport) + err = s.m.SaveChat(ourChat) + s.Require().NoError(err) + messageID1 := "message-id1" + messageID2 := "message-id2" + + messageCount := 2 + var album []*common.Message + for i := 0; i < messageCount; i++ { + image, err := buildImageWithoutAlbumIDMessage(*ourChat) + image.Clock = 1 + s.NoError(err) + album = append(album, image) + } + + deleteMessage := DeleteMessage{ + DeleteMessage: protobuf.DeleteMessage{ + Clock: 2, + MessageType: protobuf.MessageType_ONE_TO_ONE, + MessageId: messageID1, + ChatId: theirChat.ID, + }, + From: common.PubkeyToHex(&theirMessenger.identity.PublicKey), + } + + state := &ReceivedMessageState{ + Response: &MessengerResponse{}, + } + + // Handle Delete first + err = s.m.HandleDeleteMessage(state, deleteMessage) + s.Require().NoError(err) + + // Handle first image message + state = &ReceivedMessageState{ + Response: &MessengerResponse{}, + CurrentMessageState: &CurrentMessageState{ + Message: album[0].ChatMessage, + MessageID: messageID1, + WhisperTimestamp: s.m.getTimesource().GetCurrentTime(), + Contact: contact, + PublicKey: &theirMessenger.identity.PublicKey, + }, + } + err = s.m.HandleChatMessage(state) + s.Require().NoError(err) + s.Require().Len(state.Response.Messages(), 0) // Message should not be added to response + s.Require().Len(state.Response.RemovedMessages(), 0) + s.Require().Nil(state.Response.Chats()[0].LastMessage) + + // Handle second image message + state = &ReceivedMessageState{ + Response: &MessengerResponse{}, + CurrentMessageState: &CurrentMessageState{ + Message: album[1].ChatMessage, + MessageID: messageID2, + WhisperTimestamp: s.m.getTimesource().GetCurrentTime(), + Contact: contact, + PublicKey: &theirMessenger.identity.PublicKey, + }, + } + err = s.m.HandleChatMessage(state) + s.Require().NoError(err) + s.Require().Len(state.Response.Messages(), 0) // Message should not be added to response even if we didn't delete that ID + s.Require().Len(state.Response.RemovedMessages(), 0) + s.Require().Nil(state.Response.Chats()[0].LastMessage) +} diff --git a/protocol/messenger_handler.go b/protocol/messenger_handler.go index ca0bfec85..26d1f63ad 100644 --- a/protocol/messenger_handler.go +++ b/protocol/messenger_handler.go @@ -1583,44 +1583,51 @@ func (m *Messenger) HandleDeleteMessage(state *ReceivedMessageState, deleteMessa } } - // Update message and return it - originalMessage.Deleted = true - originalMessage.DeletedBy = deleteMessage.DeleteMessage.DeletedBy - - err := m.persistence.SaveMessages([]*common.Message{originalMessage}) + messagesToDelete, err := m.getMessagesToDelete(originalMessage, deleteMessage.ChatId) if err != nil { return err } - m.logger.Debug("deleting activity center notification for message", zap.String("chatID", chat.ID), zap.String("messageID", deleteMessage.MessageId)) - err = m.persistence.DeleteActivityCenterNotificationForMessage(chat.ID, deleteMessage.MessageId) - - if err != nil { - m.logger.Warn("failed to delete notifications for deleted message", zap.Error(err)) - return err - } - - if chat.LastMessage != nil && chat.LastMessage.ID == originalMessage.ID { - if err := m.updateLastMessage(chat); err != nil { + for _, messageToDelete := range messagesToDelete { + messageToDelete.Deleted = true + messageToDelete.DeletedBy = deleteMessage.DeleteMessage.DeletedBy + err := m.persistence.SaveMessages([]*common.Message{messageToDelete}) + if err != nil { return err } - if chat.LastMessage != nil && !chat.LastMessage.Seen && chat.OneToOne() && !chat.Active { - m.createMessageNotification(chat, state) + m.logger.Debug("deleting activity center notification for message", zap.String("chatID", chat.ID), zap.String("messageID", messageToDelete.ID)) + err = m.persistence.DeleteActivityCenterNotificationForMessage(chat.ID, messageToDelete.ID) + + if err != nil { + m.logger.Warn("failed to delete notifications for deleted message", zap.Error(err)) + return err + } + + state.Response.AddRemovedMessage(&RemovedMessage{MessageID: messageToDelete.ID, ChatID: chat.ID, DeletedBy: deleteMessage.DeleteMessage.DeletedBy}) + state.Response.AddNotification(DeletedMessageNotification(messageToDelete.ID, chat)) + state.Response.AddActivityCenterNotification(&ActivityCenterNotification{ + ID: types.FromHex(messageToDelete.ID), + Deleted: true, + }) + + if chat.LastMessage != nil && chat.LastMessage.ID == originalMessage.ID { + if err := m.updateLastMessage(chat); err != nil { + return err + } + + if chat.LastMessage != nil && !chat.LastMessage.Seen && chat.OneToOne() && !chat.Active { + m.createMessageNotification(chat, state) + } } } - state.Response.AddRemovedMessage(&RemovedMessage{MessageID: messageID, ChatID: chat.ID, DeletedBy: deleteMessage.DeleteMessage.DeletedBy}) state.Response.AddChat(chat) - state.Response.AddNotification(DeletedMessageNotification(messageID, chat)) - state.Response.AddActivityCenterNotification(&ActivityCenterNotification{ - ID: types.FromHex(messageID), - Deleted: true, - }) return nil } +// TODO do this delete too func (m *Messenger) HandleDeleteForMeMessage(state *ReceivedMessageState, deleteForMeMessage DeleteForMeMessage) error { if err := ValidateDeleteForMeMessage(deleteForMeMessage.DeleteForMeMessage); err != nil { return err @@ -1648,29 +1655,35 @@ func (m *Messenger) HandleDeleteForMeMessage(state *ReceivedMessageState, delete return errors.New("chat not found") } - // Update message and return it - originalMessage.DeletedForMe = true - - err := m.persistence.SaveMessages([]*common.Message{originalMessage}) + messagesToDelete, err := m.getMessagesToDelete(originalMessage, deleteForMeMessage.LocalChatID) if err != nil { return err } - m.logger.Debug("deleting activity center notification for message", zap.String("chatID", chat.ID), zap.String("messageID", deleteForMeMessage.MessageId)) + for _, messageToDelete := range messagesToDelete { + messageToDelete.DeletedForMe = true - err = m.persistence.DeleteActivityCenterNotificationForMessage(chat.ID, deleteForMeMessage.MessageId) - if err != nil { - m.logger.Warn("failed to delete notifications for deleted message", zap.Error(err)) - return err - } - - if chat.LastMessage != nil && chat.LastMessage.ID == originalMessage.ID { - if err := m.updateLastMessage(chat); err != nil { + err := m.persistence.SaveMessages([]*common.Message{messageToDelete}) + if err != nil { return err } - } - state.Response.AddMessage(originalMessage) + m.logger.Debug("deleting activity center notification for message", zap.String("chatID", chat.ID), zap.String("messageID", messageToDelete.ID)) + + err = m.persistence.DeleteActivityCenterNotificationForMessage(chat.ID, messageToDelete.ID) + if err != nil { + m.logger.Warn("failed to delete notifications for deleted message", zap.Error(err)) + return err + } + + if chat.LastMessage != nil && chat.LastMessage.ID == messageToDelete.ID { + if err := m.updateLastMessage(chat); err != nil { + return err + } + } + + state.Response.AddMessage(messageToDelete) + } state.Response.AddChat(chat) return nil @@ -2628,34 +2641,82 @@ func (m *Messenger) checkForEdits(message *common.Message) error { return nil } +func (m *Messenger) getMessagesToCheckForDelete(message *common.Message) ([]*common.Message, error) { + var messagesToCheck []*common.Message + if message.ContentType == protobuf.ChatMessage_IMAGE { + image := message.GetImage() + messagesInTheAlbum, err := m.persistence.albumMessages(message.ChatId, image.GetAlbumId()) + if err != nil { + return nil, err + } + messagesToCheck = append(messagesToCheck, messagesInTheAlbum...) + } + messagesToCheck = append(messagesToCheck, message) + return messagesToCheck, nil +} + func (m *Messenger) checkForDeletes(message *common.Message) error { - // Check for any pending deletes - // If any pending deletes are available and valid, apply them - messageDeletes, err := m.persistence.GetDeletes(message.ID, message.From) + messagesToCheck, err := m.getMessagesToCheckForDelete(message) if err != nil { return err } - if len(messageDeletes) == 0 { - return nil - } + var messageDeletes []*DeleteMessage + applyDelete := false + for _, messageToCheck := range messagesToCheck { + if !applyDelete { + // Check for any pending deletes + // If any pending deletes are available and valid, apply them + messageDeletes, err = m.persistence.GetDeletes(messageToCheck.ID, messageToCheck.From) + if err != nil { + return err + } - return m.applyDeleteMessage(messageDeletes, message) + if len(messageDeletes) == 0 { + continue + } + } + // Once one messageDelete has been found, we apply it to all the images in the album + applyDelete = true + + err := m.applyDeleteMessage(messageDeletes, messageToCheck) + if err != nil { + return err + } + } + return nil } func (m *Messenger) checkForDeleteForMes(message *common.Message) error { - // Check for any pending delete for mes - // If any pending deletes are available and valid, apply them - messageDeleteForMes, err := m.persistence.GetDeleteForMes(message.ID, message.From) + messagesToCheck, err := m.getMessagesToCheckForDelete(message) if err != nil { return err } - if len(messageDeleteForMes) == 0 { - return nil - } + var messageDeleteForMes []*DeleteForMeMessage + applyDelete := false + for _, messageToCheck := range messagesToCheck { + if !applyDelete { + // Check for any pending delete for mes + // If any pending deletes are available and valid, apply them + messageDeleteForMes, err = m.persistence.GetDeleteForMes(messageToCheck.ID, messageToCheck.From) + if err != nil { + return err + } - return m.applyDeleteForMeMessage(messageDeleteForMes, message) + if len(messageDeleteForMes) == 0 { + continue + } + } + // Once one messageDeleteForMes has been found, we apply it to all the images in the album + applyDelete = true + + err := m.applyDeleteForMeMessage(messageDeleteForMes, messageToCheck) + if err != nil { + return err + } + } + return nil } func (m *Messenger) isMessageAllowedFrom(publicKey string, chat *Chat) (bool, error) { diff --git a/protocol/messenger_messages.go b/protocol/messenger_messages.go index d466253a1..ef0934624 100644 --- a/protocol/messenger_messages.go +++ b/protocol/messenger_messages.go @@ -157,6 +157,11 @@ func (m *Messenger) DeleteMessageAndSend(ctx context.Context, messageID string) return nil, ErrInvalidDeleteTypeAuthor } + messagesToDelete, err := m.getMessagesToDelete(message, message.ChatId) + if err != nil { + return nil, err + } + clock, _ := chat.NextClockAndTimestamp(m.getTimesource()) deleteMessage := &DeleteMessage{} @@ -184,22 +189,24 @@ func (m *Messenger) DeleteMessageAndSend(ctx context.Context, messageID string) return nil, err } - message.Deleted = true - message.DeletedBy = deletedBy - err = m.persistence.SaveMessages([]*common.Message{message}) - if err != nil { - return nil, err - } - - if chat.LastMessage != nil && chat.LastMessage.ID == message.ID { - if err := m.updateLastMessage(chat); err != nil { + 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 { + if err := m.updateLastMessage(chat); err != nil { + return nil, err + } + } } - response := &MessengerResponse{} - response.AddMessage(message) - response.AddRemovedMessage(&RemovedMessage{MessageID: messageID, ChatID: chat.ID, DeletedBy: deletedBy}) response.AddChat(chat) return response, nil @@ -226,20 +233,27 @@ func (m *Messenger) DeleteMessageForMeAndSync(ctx context.Context, chatID string return nil, ErrInvalidDeleteTypeAuthor } - message.DeletedForMe = true - err = m.persistence.SaveMessages([]*common.Message{message}) + messagesToDelete, err := m.getMessagesToDelete(message, message.ChatId) if err != nil { return nil, err } - if chat.LastMessage != nil && chat.LastMessage.ID == message.ID { - if err := m.updateLastMessage(chat); err != nil { + response := &MessengerResponse{} + for _, messageToDelete := range messagesToDelete { + messageToDelete.DeletedForMe = true + err = m.persistence.SaveMessages([]*common.Message{messageToDelete}) + if err != nil { return nil, err } - } - response := &MessengerResponse{} - response.AddMessage(message) + if chat.LastMessage != nil && chat.LastMessage.ID == messageToDelete.ID { + if err := m.updateLastMessage(chat); err != nil { + return nil, err + } + } + + response.AddMessage(messageToDelete) + } response.AddChat(chat) if m.hasPairedDevices() { diff --git a/protocol/messenger_send_images_album_test.go b/protocol/messenger_send_images_album_test.go index 01d833698..5eb14e579 100644 --- a/protocol/messenger_send_images_album_test.go +++ b/protocol/messenger_send_images_album_test.go @@ -3,8 +3,6 @@ package protocol import ( "context" "crypto/ecdsa" - "io/ioutil" - "os" "testing" "github.com/stretchr/testify/suite" @@ -64,34 +62,6 @@ func (s *MessengerSendImagesAlbumSuite) newMessenger() *Messenger { return messenger } -func buildImageWithoutAlbumIDMessage(s *MessengerSendImagesAlbumSuite, chat Chat) *common.Message { - file, err := os.Open("../_assets/tests/test.jpg") - s.Require().NoError(err) - defer file.Close() - - payload, err := ioutil.ReadAll(file) - s.Require().NoError(err) - - clock, timestamp := chat.NextClockAndTimestamp(&testTimeSource{}) - message := &common.Message{} - message.ChatId = chat.ID - message.Clock = clock - message.Timestamp = timestamp - message.WhisperTimestamp = clock - message.LocalChatID = chat.ID - message.MessageType = protobuf.MessageType_ONE_TO_ONE - message.ContentType = protobuf.ChatMessage_IMAGE - - image := protobuf.ImageMessage{ - Payload: payload, - Type: protobuf.ImageType_JPEG, - Width: 1200, - Height: 1000, - } - message.Payload = &protobuf.ChatMessage_Image{Image: &image} - return message -} - func (s *MessengerSendImagesAlbumSuite) TestAlbumImageMessagesSend() { theirMessenger := s.newMessenger() _, err := theirMessenger.Start() @@ -109,7 +79,9 @@ func (s *MessengerSendImagesAlbumSuite) TestAlbumImageMessagesSend() { var album []*common.Message for i := 0; i < messageCount; i++ { - album = append(album, buildImageWithoutAlbumIDMessage(s, *ourChat)) + image, err := buildImageWithoutAlbumIDMessage(*ourChat) + s.NoError(err) + album = append(album, image) } err = s.m.SaveChat(ourChat) @@ -167,7 +139,8 @@ func (s *MessengerSendImagesAlbumSuite) TestAlbumImageMessagesWithMentionSend() var album []*common.Message for i := 0; i < messageCount; i++ { - outgoingMessage := buildImageWithoutAlbumIDMessage(s, *ourChat) + outgoingMessage, err := buildImageWithoutAlbumIDMessage(*ourChat) + s.NoError(err) outgoingMessage.Mentioned = true outgoingMessage.Text = "hey @" + common.PubkeyToHex(&theirMessenger.identity.PublicKey) album = append(album, outgoingMessage) diff --git a/protocol/messenger_test.go b/protocol/messenger_test.go index 0ea7eb4e7..997d0bd35 100644 --- a/protocol/messenger_test.go +++ b/protocol/messenger_test.go @@ -5,7 +5,9 @@ import ( "encoding/hex" "encoding/json" "errors" + "io/ioutil" "math/big" + "os" "strings" "testing" "time" @@ -2319,6 +2321,39 @@ func (s *MessengerSuite) TestResendExpiredEmojis() { s.True(rawMessage.SendCount >= 2) } +func buildImageWithoutAlbumIDMessage(chat Chat) (*common.Message, error) { + file, err := os.Open("../_assets/tests/test.jpg") + if err != err { + return nil, err + } + defer file.Close() + + payload, err := ioutil.ReadAll(file) + if err != err { + return nil, err + } + + clock, timestamp := chat.NextClockAndTimestamp(&testTimeSource{}) + message := &common.Message{} + message.ChatId = chat.ID + message.Clock = clock + message.Timestamp = timestamp + message.WhisperTimestamp = clock + message.LocalChatID = chat.ID + message.MessageType = protobuf.MessageType_ONE_TO_ONE + message.ContentType = protobuf.ChatMessage_IMAGE + + image := protobuf.ImageMessage{ + Payload: payload, + Type: protobuf.ImageType_JPEG, + Width: 1200, + Height: 1000, + } + message.Payload = &protobuf.ChatMessage_Image{Image: &image} + + return message, nil +} + type testTimeSource struct{} func (t *testTimeSource) GetCurrentTime() uint64 {