Add ClearHistory & DeactivateChat methods
This commit is contained in:
parent
922e785512
commit
b331b61807
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -8,4 +8,5 @@ var (
|
|||
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")
|
||||
)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -818,9 +818,28 @@ 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 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 {
|
||||
tx, err := db.db.BeginTx(context.Background(), &sql.TxOptions{})
|
||||
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 contact.LocalNickname != previousContact.LocalNickname
|
||||
return nil, ErrChatNotFound
|
||||
}
|
||||
|
||||
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.
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue