diff --git a/VERSION b/VERSION index 4a46fb5b7..e6227be88 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.68.0 +0.68.2 diff --git a/protocol/chat.go b/protocol/chat.go index f674e8b98..bcba0c5dc 100644 --- a/protocol/chat.go +++ b/protocol/chat.go @@ -312,10 +312,14 @@ func CreatePublicChat(name string, timesource common.TimeSource) Chat { } } -func CreateProfileChat(name string, profile string, timesource common.TimeSource) Chat { +func buildProfileChatID(publicKeyString string) string { + return "@" + publicKeyString +} + +func CreateProfileChat(id string, profile string, timesource common.TimeSource) Chat { return Chat{ - ID: name, - Name: name, + ID: id, + Name: id, Active: true, Timestamp: int64(timesource.GetCurrentTime()), Color: chatColors[rand.Intn(len(chatColors))], // nolint: gosec diff --git a/protocol/contact.go b/protocol/contact.go index 33689cc10..cd961cfb9 100644 --- a/protocol/contact.go +++ b/protocol/contact.go @@ -2,7 +2,6 @@ package protocol import ( "crypto/ecdsa" - "encoding/hex" "github.com/status-im/status-go/eth-node/crypto" "github.com/status-im/status-go/eth-node/types" @@ -83,6 +82,18 @@ func (c Contact) IsBlocked() bool { return existsInStringSlice(c.SystemTags, contactBlocked) } +func (c *Contact) Remove() { + var newSystemTags []string + // Remove the newSystemTags system-tag, so that the contact is + // not considered "added" anymore + for _, tag := range newSystemTags { + if tag != contactAdded { + newSystemTags = append(newSystemTags, tag) + } + } + c.SystemTags = newSystemTags +} + func (c *Contact) ResetENSVerification(clock uint64, name string) { c.ENSVerifiedAt = 0 c.ENSVerified = false @@ -101,16 +112,33 @@ func existsInStringSlice(set []string, find string) bool { return false } -func buildContact(publicKey *ecdsa.PublicKey) (*Contact, error) { - id := "0x" + hex.EncodeToString(crypto.FromECDSAPub(publicKey)) +func buildContactFromPkString(pkString string) (*Contact, error) { + publicKeyBytes, err := types.DecodeHex(pkString) + if err != nil { + return nil, err + } - identicon, err := identicon.GenerateBase64(id) + publicKey, err := crypto.UnmarshalPubkey(publicKeyBytes) + if err != nil { + return nil, err + } + + return buildContact(pkString, publicKey) +} + +func buildContactFromPublicKey(publicKey *ecdsa.PublicKey) (*Contact, error) { + id := types.EncodeHex(crypto.FromECDSAPub(publicKey)) + return buildContact(id, publicKey) +} + +func buildContact(publicKeyString string, publicKey *ecdsa.PublicKey) (*Contact, error) { + identicon, err := identicon.GenerateBase64(publicKeyString) if err != nil { return nil, err } contact := &Contact{ - ID: id, + ID: publicKeyString, Alias: alias.GenerateFromPublicKey(publicKey), Identicon: identicon, } diff --git a/protocol/errors.go b/protocol/errors.go index 95b2af557..14d0c23d7 100644 --- a/protocol/errors.go +++ b/protocol/errors.go @@ -5,7 +5,8 @@ import ( ) var ( - ErrChatIDEmpty = errors.New("chat ID is empty") - ErrChatNotFound = errors.New("can't find chat") - ErrNotImplemented = errors.New("not implemented") + ErrChatIDEmpty = errors.New("chat ID is empty") + ErrChatNotFound = errors.New("can't find chat") + ErrNotImplemented = errors.New("not implemented") + ErrContactNotFound = errors.New("contact not found") ) diff --git a/protocol/message_handler.go b/protocol/message_handler.go index 6630d909a..eb5d27be3 100644 --- a/protocol/message_handler.go +++ b/protocol/message_handler.go @@ -221,15 +221,8 @@ func (m *MessageHandler) HandleSyncInstallationContact(state *ReceivedMessageSta contact, ok := state.AllContacts[message.Id] if !ok { - publicKeyBytes, err := hex.DecodeString(message.Id[2:]) - if err != nil { - return err - } - publicKey, err := crypto.UnmarshalPubkey(publicKeyBytes) - if err != nil { - return err - } - contact, err = buildContact(publicKey) + var err error + contact, err = buildContactFromPkString(message.Id) if err != nil { return err } diff --git a/protocol/message_persistence.go b/protocol/message_persistence.go index 31b86271c..6cf591063 100644 --- a/protocol/message_persistence.go +++ b/protocol/message_persistence.go @@ -818,8 +818,27 @@ func (db sqlitePersistence) HideMessage(id string) error { } func (db sqlitePersistence) DeleteMessagesByChatID(id string) error { - _, err := db.db.Exec(`DELETE FROM user_messages WHERE local_chat_id = ?`, id) - return err + return db.deleteMessagesByChatID(id, nil) +} + +func (db sqlitePersistence) deleteMessagesByChatID(id string, tx *sql.Tx) (err error) { + if tx == nil { + tx, err = db.db.BeginTx(context.Background(), &sql.TxOptions{}) + if err != nil { + return err + } + defer func() { + if err == nil { + err = tx.Commit() + return + } + // don't shadow original error + _ = tx.Rollback() + }() + } + + _, err = tx.Exec(`DELETE FROM user_messages WHERE local_chat_id = ?`, id) + return } func (db sqlitePersistence) MarkAllRead(chatID string) error { @@ -1136,3 +1155,79 @@ func (db sqlitePersistence) InvitationByID(id string) (*GroupChatInvitation, err return nil, err } } + +// ClearHistory deletes all the messages for a chat and updates it's values +func (db sqlitePersistence) ClearHistory(chat *Chat, currentClockValue uint64) (err error) { + var tx *sql.Tx + + tx, err = db.db.BeginTx(context.Background(), &sql.TxOptions{}) + if err != nil { + return + } + + defer func() { + if err == nil { + err = tx.Commit() + return + } + // don't shadow original error + _ = tx.Rollback() + }() + err = db.clearHistory(chat, currentClockValue, tx) + + return +} + +// Deactivate chat sets a chat as inactive and clear its history +func (db sqlitePersistence) DeactivateChat(chat *Chat, currentClockValue uint64) (err error) { + var tx *sql.Tx + + tx, err = db.db.BeginTx(context.Background(), &sql.TxOptions{}) + if err != nil { + return + } + + defer func() { + if err == nil { + err = tx.Commit() + return + } + // don't shadow original error + _ = tx.Rollback() + }() + err = db.deactivateChat(chat, currentClockValue, tx) + + return +} + +func (db sqlitePersistence) deactivateChat(chat *Chat, currentClockValue uint64, tx *sql.Tx) error { + chat.Active = false + err := db.saveChat(tx, *chat) + if err != nil { + return err + } + + return db.clearHistory(chat, currentClockValue, tx) +} + +func (db sqlitePersistence) clearHistory(chat *Chat, currentClockValue uint64, tx *sql.Tx) error { + // Set deleted at clock value if it's not a public chat so that + // old messages will be discarded + if !chat.Public() && !chat.ProfileUpdates() && !chat.Timeline() { + if chat.LastMessage != nil && chat.LastMessage.Clock != 0 { + chat.DeletedAtClockValue = chat.LastMessage.Clock + } + chat.DeletedAtClockValue = currentClockValue + } + + chat.LastMessage = nil + chat.UnviewedMessagesCount = 0 + + err := db.deleteMessagesByChatID(chat.ID, tx) + if err != nil { + return err + } + + err = db.saveChat(tx, *chat) + return err +} diff --git a/protocol/messenger.go b/protocol/messenger.go index 305bff06c..62a195ab1 100644 --- a/protocol/messenger.go +++ b/protocol/messenger.go @@ -2082,59 +2082,42 @@ func (m *Messenger) DeleteChat(chatID string) error { return nil } -func (m *Messenger) isNewContact(contact *Contact) bool { - previousContact, ok := m.allContacts[contact.ID] - return contact.IsAdded() && (!ok || !previousContact.IsAdded()) +func (m *Messenger) DeactivateChat(chatID string) (*MessengerResponse, error) { + m.mutex.Lock() + defer m.mutex.Unlock() + return m.deactivateChat(chatID) } -func (m *Messenger) hasNicknameChanged(contact *Contact) bool { - previousContact, ok := m.allContacts[contact.ID] +func (m *Messenger) deactivateChat(chatID string) (*MessengerResponse, error) { + var response MessengerResponse + chat, ok := m.allChats[chatID] if !ok { - return false + return nil, ErrChatNotFound } - return contact.LocalNickname != previousContact.LocalNickname -} -func (m *Messenger) removedContact(contact *Contact) bool { - previousContact, ok := m.allContacts[contact.ID] - if !ok { - return false - } - return previousContact.IsAdded() && !contact.IsAdded() -} + clock, _ := chat.NextClockAndTimestamp(m.getTimesource()) + + err := m.persistence.DeactivateChat(chat, clock) -func (m *Messenger) saveContact(contact *Contact) error { - name, identicon, err := generateAliasAndIdenticon(contact.ID) if err != nil { - return err + return nil, err } - contact.Identicon = identicon - contact.Alias = name - - if m.isNewContact(contact) || m.hasNicknameChanged(contact) { - err := m.syncContact(context.Background(), contact) + // We re-register as our options have changed and we don't want to + // receive PN from mentions in this chat anymore + if chat.Public() { + err := m.reregisterForPushNotifications() if err != nil { - return err + return nil, err } } - // We check if it should re-register with the push notification server - shouldReregisterForPushNotifications := (m.isNewContact(contact) || m.removedContact(contact)) + m.allChats[chatID] = chat - err = m.persistence.SaveContact(contact, nil) - if err != nil { - return err - } + response.Chats = []*Chat{chat} + // TODO: Remove filters - m.allContacts[contact.ID] = contact - - // Reregister only when data has changed - if shouldReregisterForPushNotifications { - return m.reregisterForPushNotifications() - } - - return nil + return &response, nil } func (m *Messenger) reregisterForPushNotifications() error { @@ -2146,55 +2129,6 @@ func (m *Messenger) reregisterForPushNotifications() error { return m.pushNotificationClient.Reregister(m.pushNotificationOptions()) } -func (m *Messenger) SaveContact(contact *Contact) error { - m.mutex.Lock() - defer m.mutex.Unlock() - return m.saveContact(contact) -} - -func (m *Messenger) BlockContact(contact *Contact) ([]*Chat, error) { - m.mutex.Lock() - defer m.mutex.Unlock() - chats, err := m.persistence.BlockContact(contact) - if err != nil { - return nil, err - } - - m.allContacts[contact.ID] = contact - for _, chat := range chats { - m.allChats[chat.ID] = chat - } - delete(m.allChats, contact.ID) - - // re-register for push notifications - err = m.reregisterForPushNotifications() - if err != nil { - return nil, err - } - - return chats, nil -} - -func (m *Messenger) Contacts() []*Contact { - m.mutex.Lock() - defer m.mutex.Unlock() - var contacts []*Contact - for _, contact := range m.allContacts { - if contact.HasCustomFields() { - contacts = append(contacts, contact) - } - } - return contacts -} - -// GetContactByID assumes pubKey includes 0x prefix -func (m *Messenger) GetContactByID(pubKey string) *Contact { - m.mutex.Lock() - defer m.mutex.Unlock() - - return m.allContacts[pubKey] -} - // pull a message from the database and send it again func (m *Messenger) reSendRawMessage(ctx context.Context, messageID string) error { message, err := m.persistence.RawMessageByID(messageID) @@ -2524,106 +2458,6 @@ func (m *Messenger) sendChatMessage(ctx context.Context, message *common.Message return &response, m.saveChat(chat) } -// Send contact updates to all contacts added by us -func (m *Messenger) SendContactUpdates(ctx context.Context, ensName, profileImage string) error { - m.mutex.Lock() - defer m.mutex.Unlock() - - myID := contactIDFromPublicKey(&m.identity.PublicKey) - - if _, err := m.sendContactUpdate(ctx, myID, ensName, profileImage); err != nil { - return err - } - - // TODO: This should not be sending paired messages, as we do it above - for _, contact := range m.allContacts { - if contact.IsAdded() { - if _, err := m.sendContactUpdate(ctx, contact.ID, ensName, profileImage); err != nil { - return err - } - } - } - return nil -} - -// NOTE: this endpoint does not add the contact, the reason being is that currently -// that's left as a responsibility to the client, which will call both `SendContactUpdate` -// and `SaveContact` with the correct system tag. -// Ideally we have a single endpoint that does both, but probably best to bring `ENS` name -// on the messenger first. - -// SendContactUpdate sends a contact update to a user and adds the user to contacts -func (m *Messenger) SendContactUpdate(ctx context.Context, chatID, ensName, profileImage string) (*MessengerResponse, error) { - m.mutex.Lock() - defer m.mutex.Unlock() - return m.sendContactUpdate(ctx, chatID, ensName, profileImage) -} - -func (m *Messenger) sendContactUpdate(ctx context.Context, chatID, ensName, profileImage string) (*MessengerResponse, error) { - var response MessengerResponse - - contact, ok := m.allContacts[chatID] - if !ok { - pubkeyBytes, err := types.DecodeHex(chatID) - if err != nil { - return nil, err - } - - publicKey, err := crypto.UnmarshalPubkey(pubkeyBytes) - if err != nil { - return nil, err - } - - contact, err = buildContact(publicKey) - if err != nil { - return nil, err - } - } - - chat, ok := m.allChats[chatID] - if !ok { - publicKey, err := contact.PublicKey() - if err != nil { - return nil, err - } - chat = OneToOneFromPublicKey(publicKey, m.getTimesource()) - // We don't want to show the chat to the user - chat.Active = false - } - - m.allChats[chat.ID] = chat - clock, _ := chat.NextClockAndTimestamp(m.getTimesource()) - - contactUpdate := &protobuf.ContactUpdate{ - Clock: clock, - EnsName: ensName, - ProfileImage: profileImage} - encodedMessage, err := proto.Marshal(contactUpdate) - if err != nil { - return nil, err - } - - _, err = m.dispatchMessage(ctx, common.RawMessage{ - LocalChatID: chatID, - Payload: encodedMessage, - MessageType: protobuf.ApplicationMetadataMessage_CONTACT_UPDATE, - ResendAutomatically: true, - }) - if err != nil { - return nil, err - } - - response.Contacts = []*Contact{contact} - response.Chats = []*Chat{chat} - - chat.LastClockValue = clock - err = m.saveChat(chat) - if err != nil { - return nil, err - } - return &response, m.saveContact(contact) -} - // SyncDevices sends all public chats and contacts to paired devices // TODO remove use of photoPath in contacts func (m *Messenger) SyncDevices(ctx context.Context, ensName, photoPath string) error { @@ -2914,7 +2748,7 @@ func (m *Messenger) handleRetrievedMessages(chatWithMessages map[transport.Filte if c, ok := messageState.AllContacts[senderID]; ok { contact = c } else { - c, err := buildContact(publicKey) + c, err := buildContact(senderID, publicKey) if err != nil { logger.Info("failed to build contact", zap.Error(err)) continue @@ -3376,6 +3210,32 @@ func (m *Messenger) DeleteMessagesByChatID(id string) error { return m.persistence.DeleteMessagesByChatID(id) } +func (m *Messenger) ClearHistory(id string) (*MessengerResponse, error) { + m.mutex.Lock() + defer m.mutex.Unlock() + return m.clearHistory(id) + +} + +func (m *Messenger) clearHistory(id string) (*MessengerResponse, error) { + chat, ok := m.allChats[id] + if !ok { + return nil, ErrChatNotFound + } + + clock, _ := chat.NextClockAndTimestamp(m.transport) + + err := m.persistence.ClearHistory(chat, clock) + if err != nil { + return nil, err + } + + m.allChats[id] = chat + + response := &MessengerResponse{Chats: []*Chat{chat}} + return response, nil +} + // MarkMessagesSeen marks messages with `ids` as seen in the chat `chatID`. // It returns the number of affected messages or error. If there is an error, // the number of affected messages is always zero. diff --git a/protocol/messenger_contact_update_test.go b/protocol/messenger_contact_update_test.go index 2a08e7233..722c7af66 100644 --- a/protocol/messenger_contact_update_test.go +++ b/protocol/messenger_contact_update_test.go @@ -119,3 +119,35 @@ func (s *MessengerContactUpdateSuite) TestReceiveContactUpdate() { s.Require().NotEmpty(receivedContact.LastUpdated) s.Require().NoError(theirMessenger.Shutdown()) } + +func (s *MessengerContactUpdateSuite) TestAddContact() { + contactID := types.EncodeHex(crypto.FromECDSAPub(&s.m.identity.PublicKey)) + + theirMessenger := s.newMessenger(s.shh) + s.Require().NoError(theirMessenger.Start()) + + response, err := theirMessenger.AddContact(context.Background(), contactID) + s.Require().NoError(err) + s.Require().NotNil(response) + + s.Require().Len(response.Contacts, 1) + contact := response.Contacts[0] + + // It adds the profile chat and the one to one chat + s.Require().Len(response.Chats, 2) + + // It should add the contact + s.Require().True(contact.IsAdded()) + + // Wait for the message to reach its destination + response, err = WaitOnMessengerResponse( + s.m, + func(r *MessengerResponse) bool { return len(r.Contacts) > 0 }, + "contact request not received", + ) + s.Require().NoError(err) + + receivedContact := response.Contacts[0] + s.Require().True(receivedContact.HasBeenAdded()) + s.Require().NotEmpty(receivedContact.LastUpdated) +} diff --git a/protocol/messenger_contacts.go b/protocol/messenger_contacts.go new file mode 100644 index 000000000..5a4b29927 --- /dev/null +++ b/protocol/messenger_contacts.go @@ -0,0 +1,323 @@ +package protocol + +import ( + "context" + "crypto/ecdsa" + + "github.com/golang/protobuf/proto" + + "github.com/status-im/status-go/protocol/common" + "github.com/status-im/status-go/protocol/protobuf" +) + +func (m *Messenger) SaveContact(contact *Contact) error { + m.mutex.Lock() + defer m.mutex.Unlock() + return m.saveContact(contact) +} + +func (m *Messenger) AddContact(ctx context.Context, pubKey string) (*MessengerResponse, error) { + m.mutex.Lock() + defer m.mutex.Unlock() + contact, ok := m.allContacts[pubKey] + if !ok { + var err error + contact, err = buildContactFromPkString(pubKey) + if err != nil { + return nil, err + } + } + + if !contact.IsAdded() { + contact.SystemTags = append(contact.SystemTags, contactAdded) + } + + // We sync the contact with the other devices + err := m.syncContact(context.Background(), contact) + if err != nil { + return nil, err + } + + err = m.persistence.SaveContact(contact, nil) + if err != nil { + return nil, err + } + + m.allContacts[contact.ID] = contact + + // And we re-register for push notications + err = m.reregisterForPushNotifications() + if err != nil { + return nil, err + } + + // Create the corresponding profile chat + profileChatID := buildProfileChatID(contact.ID) + profileChat, ok := m.allChats[profileChatID] + + if !ok { + builtChat := CreateProfileChat(profileChatID, contact.ID, m.getTimesource()) + profileChat = &builtChat + } + + // TODO: return filters in messenger response + err = m.Join(*profileChat) + if err != nil { + return nil, err + } + + // Finally we send a contact update so they are notified we added them + // TODO: ens and picture are both blank for now + response, err := m.sendContactUpdate(context.Background(), pubKey, "", "") + if err != nil { + return nil, err + } + + response.Chats = append(response.Chats, profileChat) + + publicKey, err := contact.PublicKey() + if err != nil { + return nil, err + } + + // TODO: Add filters to response + _, err = m.transport.InitFilters([]string{profileChat.ID}, []*ecdsa.PublicKey{publicKey}) + if err != nil { + return nil, err + } + + return response, nil +} + +func (m *Messenger) RemoveContact(ctx context.Context, pubKey string) (*MessengerResponse, error) { + m.mutex.Lock() + defer m.mutex.Unlock() + var response *MessengerResponse + + contact, ok := m.allContacts[pubKey] + if !ok { + return nil, ErrContactNotFound + } + + contact.Remove() + + err := m.persistence.SaveContact(contact, nil) + if err != nil { + return nil, err + } + + m.allContacts[contact.ID] = contact + + // And we re-register for push notications + err = m.reregisterForPushNotifications() + if err != nil { + return nil, err + } + + // Create the corresponding profile chat + profileChatID := buildProfileChatID(contact.ID) + _, ok = m.allChats[profileChatID] + + if ok { + chatResponse, err := m.deactivateChat(profileChatID) + if err != nil { + return nil, err + } + err = response.Merge(chatResponse) + if err != nil { + return nil, err + } + } + + response.Contacts = []*Contact{contact} + return response, nil +} + +func (m *Messenger) Contacts() []*Contact { + m.mutex.Lock() + defer m.mutex.Unlock() + var contacts []*Contact + for _, contact := range m.allContacts { + if contact.HasCustomFields() { + contacts = append(contacts, contact) + } + } + return contacts +} + +// GetContactByID assumes pubKey includes 0x prefix +func (m *Messenger) GetContactByID(pubKey string) *Contact { + m.mutex.Lock() + defer m.mutex.Unlock() + + return m.allContacts[pubKey] +} + +func (m *Messenger) BlockContact(contact *Contact) ([]*Chat, error) { + m.mutex.Lock() + defer m.mutex.Unlock() + chats, err := m.persistence.BlockContact(contact) + if err != nil { + return nil, err + } + + m.allContacts[contact.ID] = contact + for _, chat := range chats { + m.allChats[chat.ID] = chat + } + delete(m.allChats, contact.ID) + + // re-register for push notifications + err = m.reregisterForPushNotifications() + if err != nil { + return nil, err + } + + return chats, nil +} + +func (m *Messenger) saveContact(contact *Contact) error { + name, identicon, err := generateAliasAndIdenticon(contact.ID) + if err != nil { + return err + } + + contact.Identicon = identicon + contact.Alias = name + + if m.isNewContact(contact) || m.hasNicknameChanged(contact) { + err := m.syncContact(context.Background(), contact) + if err != nil { + return err + } + } + + // We check if it should re-register with the push notification server + shouldReregisterForPushNotifications := (m.isNewContact(contact) || m.removedContact(contact)) + + err = m.persistence.SaveContact(contact, nil) + if err != nil { + return err + } + + m.allContacts[contact.ID] = contact + + // Reregister only when data has changed + if shouldReregisterForPushNotifications { + return m.reregisterForPushNotifications() + } + + return nil +} + +// Send contact updates to all contacts added by us +func (m *Messenger) SendContactUpdates(ctx context.Context, ensName, profileImage string) error { + m.mutex.Lock() + defer m.mutex.Unlock() + + myID := contactIDFromPublicKey(&m.identity.PublicKey) + + if _, err := m.sendContactUpdate(ctx, myID, ensName, profileImage); err != nil { + return err + } + + // TODO: This should not be sending paired messages, as we do it above + for _, contact := range m.allContacts { + if contact.IsAdded() { + if _, err := m.sendContactUpdate(ctx, contact.ID, ensName, profileImage); err != nil { + return err + } + } + } + return nil +} + +// NOTE: this endpoint does not add the contact, the reason being is that currently +// that's left as a responsibility to the client, which will call both `SendContactUpdate` +// and `SaveContact` with the correct system tag. +// Ideally we have a single endpoint that does both, but probably best to bring `ENS` name +// on the messenger first. + +// SendContactUpdate sends a contact update to a user and adds the user to contacts +func (m *Messenger) SendContactUpdate(ctx context.Context, chatID, ensName, profileImage string) (*MessengerResponse, error) { + m.mutex.Lock() + defer m.mutex.Unlock() + return m.sendContactUpdate(ctx, chatID, ensName, profileImage) +} + +func (m *Messenger) sendContactUpdate(ctx context.Context, chatID, ensName, profileImage string) (*MessengerResponse, error) { + var response MessengerResponse + + contact, ok := m.allContacts[chatID] + if !ok { + var err error + contact, err = buildContactFromPkString(chatID) + if err != nil { + return nil, err + } + } + + chat, ok := m.allChats[chatID] + if !ok { + publicKey, err := contact.PublicKey() + if err != nil { + return nil, err + } + chat = OneToOneFromPublicKey(publicKey, m.getTimesource()) + // We don't want to show the chat to the user + chat.Active = false + } + + m.allChats[chat.ID] = chat + clock, _ := chat.NextClockAndTimestamp(m.getTimesource()) + + contactUpdate := &protobuf.ContactUpdate{ + Clock: clock, + EnsName: ensName, + ProfileImage: profileImage} + encodedMessage, err := proto.Marshal(contactUpdate) + if err != nil { + return nil, err + } + + _, err = m.dispatchMessage(ctx, common.RawMessage{ + LocalChatID: chatID, + Payload: encodedMessage, + MessageType: protobuf.ApplicationMetadataMessage_CONTACT_UPDATE, + ResendAutomatically: true, + }) + if err != nil { + return nil, err + } + + response.Contacts = []*Contact{contact} + response.Chats = []*Chat{chat} + + chat.LastClockValue = clock + err = m.saveChat(chat) + if err != nil { + return nil, err + } + return &response, m.saveContact(contact) +} + +func (m *Messenger) isNewContact(contact *Contact) bool { + previousContact, ok := m.allContacts[contact.ID] + return contact.IsAdded() && (!ok || !previousContact.IsAdded()) +} + +func (m *Messenger) hasNicknameChanged(contact *Contact) bool { + previousContact, ok := m.allContacts[contact.ID] + if !ok { + return false + } + return contact.LocalNickname != previousContact.LocalNickname +} + +func (m *Messenger) removedContact(contact *Contact) bool { + previousContact, ok := m.allContacts[contact.ID] + if !ok { + return false + } + return previousContact.IsAdded() && !contact.IsAdded() +} diff --git a/protocol/messenger_installations_test.go b/protocol/messenger_installations_test.go index 75c13862d..66d399f31 100644 --- a/protocol/messenger_installations_test.go +++ b/protocol/messenger_installations_test.go @@ -99,7 +99,7 @@ func (s *MessengerInstallationSuite) TestReceiveInstallation() { contactKey, err := crypto.GenerateKey() s.Require().NoError(err) - contact, err := buildContact(&contactKey.PublicKey) + contact, err := buildContactFromPublicKey(&contactKey.PublicKey) s.Require().NoError(err) contact.SystemTags = append(contact.SystemTags, contactAdded) err = s.m.SaveContact(contact) @@ -141,7 +141,7 @@ func (s *MessengerInstallationSuite) TestSyncInstallation() { contactKey, err := crypto.GenerateKey() s.Require().NoError(err) - contact, err := buildContact(&contactKey.PublicKey) + contact, err := buildContactFromPublicKey(&contactKey.PublicKey) s.Require().NoError(err) contact.SystemTags = append(contact.SystemTags, contactAdded) contact.LocalNickname = "Test Nickname" diff --git a/protocol/persistence_test.go b/protocol/persistence_test.go index 47abd19f0..065c2e06c 100644 --- a/protocol/persistence_test.go +++ b/protocol/persistence_test.go @@ -776,3 +776,135 @@ func TestHideMessage(t *testing.T) { require.True(t, actualHidden) require.True(t, actualSeen) } + +func TestDeactivatePublicChat(t *testing.T) { + db, err := openTestDB() + require.NoError(t, err) + p := sqlitePersistence{db: db} + publicChatID := "public-chat-id" + var currentClockValue uint64 = 10 + + timesource := &testTimeSource{} + lastMessage := common.Message{ + ID: "0x01", + LocalChatID: publicChatID, + ChatMessage: protobuf.ChatMessage{Text: "some-text"}, + From: "me", + } + lastMessage.Clock = 20 + + require.NoError(t, p.SaveMessages([]*common.Message{&lastMessage})) + + publicChat := CreatePublicChat(publicChatID, timesource) + publicChat.LastMessage = &lastMessage + publicChat.UnviewedMessagesCount = 1 + + err = p.DeactivateChat(&publicChat, currentClockValue) + + // It does not set deleted at for a public chat + require.NoError(t, err) + require.Equal(t, uint64(0), publicChat.DeletedAtClockValue) + + // It sets the lastMessage to nil + require.Nil(t, publicChat.LastMessage) + + // It sets unviewed messages count + require.Equal(t, uint(0), publicChat.UnviewedMessagesCount) + + // It sets active as false + require.False(t, publicChat.Active) + + // It deletes messages + messages, _, err := p.MessageByChatID(publicChatID, "", 10) + require.NoError(t, err) + require.Len(t, messages, 0) + + // Reload chat to make sure it has been save + dbChat, err := p.Chat(publicChatID) + + require.NoError(t, err) + require.NotNil(t, dbChat) + + // Same checks on the chat pulled from the db + // It does not set deleted at for a public chat + require.NoError(t, err) + require.Equal(t, uint64(0), dbChat.DeletedAtClockValue) + + // It sets the lastMessage to nil + require.Nil(t, dbChat.LastMessage) + + // It sets unviewed messages count + require.Equal(t, uint(0), dbChat.UnviewedMessagesCount) + + // It sets active as false + require.False(t, dbChat.Active) +} + +func TestDeactivateOneToOneChat(t *testing.T) { + key, err := crypto.GenerateKey() + require.NoError(t, err) + + pkString := types.EncodeHex(crypto.FromECDSAPub(&key.PublicKey)) + + db, err := openTestDB() + require.NoError(t, err) + p := sqlitePersistence{db: db} + var currentClockValue uint64 = 10 + + timesource := &testTimeSource{} + + chat := CreateOneToOneChat(pkString, &key.PublicKey, timesource) + + lastMessage := common.Message{ + ID: "0x01", + LocalChatID: chat.ID, + ChatMessage: protobuf.ChatMessage{Text: "some-text"}, + From: "me", + } + lastMessage.Clock = 20 + + require.NoError(t, p.SaveMessages([]*common.Message{&lastMessage})) + + chat.LastMessage = &lastMessage + chat.UnviewedMessagesCount = 1 + + err = p.DeactivateChat(&chat, currentClockValue) + + // It does set deleted at for a public chat + require.NoError(t, err) + require.NotEqual(t, uint64(0), chat.DeletedAtClockValue) + + // It sets the lastMessage to nil + require.Nil(t, chat.LastMessage) + + // It sets unviewed messages count + require.Equal(t, uint(0), chat.UnviewedMessagesCount) + + // It sets active as false + require.False(t, chat.Active) + + // It deletes messages + messages, _, err := p.MessageByChatID(chat.ID, "", 10) + require.NoError(t, err) + require.Len(t, messages, 0) + + // Reload chat to make sure it has been save + dbChat, err := p.Chat(chat.ID) + + require.NoError(t, err) + require.NotNil(t, dbChat) + + // Same checks on the chat pulled from the db + // It does set deleted at for a public chat + require.NoError(t, err) + require.NotEqual(t, uint64(0), dbChat.DeletedAtClockValue) + + // It sets the lastMessage to nil + require.Nil(t, dbChat.LastMessage) + + // It sets unviewed messages count + require.Equal(t, uint(0), dbChat.UnviewedMessagesCount) + + // It sets active as false + require.False(t, dbChat.Active) +} diff --git a/services/ext/api.go b/services/ext/api.go index 0347df96a..dde220cf4 100644 --- a/services/ext/api.go +++ b/services/ext/api.go @@ -415,6 +415,22 @@ func (api *PublicAPI) MarkAllRead(chatID string) error { return api.service.messenger.MarkAllRead(chatID) } +func (api *PublicAPI) AddContact(ctx context.Context, pubKey string) (*protocol.MessengerResponse, error) { + return api.service.messenger.AddContact(ctx, pubKey) +} + +func (api *PublicAPI) RemoveContact(ctx context.Context, pubKey string) (*protocol.MessengerResponse, error) { + return api.service.messenger.RemoveContact(ctx, pubKey) +} + +func (api *PublicAPI) ClearHistory(chatID string) (*protocol.MessengerResponse, error) { + return api.service.messenger.ClearHistory(chatID) +} + +func (api *PublicAPI) DeactivateChat(chatID string) (*protocol.MessengerResponse, error) { + return api.service.messenger.DeactivateChat(chatID) +} + func (api *PublicAPI) UpdateMessageOutgoingStatus(id, newOutgoingStatus string) error { return api.service.messenger.UpdateMessageOutgoingStatus(id, newOutgoingStatus) }