status-go/protocol/messenger_messages.go

521 lines
14 KiB
Go

package protocol
import (
"context"
"crypto/ecdsa"
"errors"
"go.uber.org/zap"
"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"
)
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 {
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 = request.ContentType
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,
ResendAutomatically: true,
}
_, 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_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,
ResendAutomatically: true,
}
_, 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,
ResendAutomatically: true,
}
_, 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
}
message.Text = 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)
}