status-im/status-go

View on GitHub
protocol/messenger_contacts.go

Summary

Maintainability
C
1 day
Test Coverage
D
69%
package protocol

import (
    "context"
    "crypto/ecdsa"
    "errors"
    "fmt"

    "github.com/golang/protobuf/proto"
    "go.uber.org/zap"

    "github.com/ethereum/go-ethereum/log"

    "github.com/status-im/status-go/deprecation"
    "github.com/status-im/status-go/eth-node/crypto"
    "github.com/status-im/status-go/eth-node/types"
    multiaccountscommon "github.com/status-im/status-go/multiaccounts/common"
    "github.com/status-im/status-go/protocol/common"
    "github.com/status-im/status-go/protocol/protobuf"
    "github.com/status-im/status-go/protocol/requests"
    "github.com/status-im/status-go/protocol/transport"
)

const outgoingMutualStateEventSentDefaultText = "You sent a contact request to @%s"
const outgoingMutualStateEventAcceptedDefaultText = "You accepted @%s's contact request"
const outgoingMutualStateEventRemovedDefaultText = "You removed @%s as a contact"
const incomingMutualStateEventSentDefaultText = "@%s sent you a contact request"
const incomingMutualStateEventAcceptedDefaultText = "@%s accepted your contact request"
const incomingMutualStateEventRemovedDefaultText = "@%s removed you as a contact"

var ErrGetLatestContactRequestForContactInvalidID = errors.New("get-latest-contact-request-for-contact: invalid id")

type SelfContactChangeEvent struct {
    DisplayNameChanged   bool
    PreferredNameChanged bool
    BioChanged           bool
    SocialLinksChanged   bool
    ImagesChanged        bool
}

func (m *Messenger) prepareMutualStateUpdateMessage(contactID string, updateType MutualStateUpdateType, clock uint64, timestamp uint64, outgoing bool) (*common.Message, error) {
    var text string
    var to string
    var from string
    var contentType protobuf.ChatMessage_ContentType
    if outgoing {
        to = contactID
        from = m.myHexIdentity()

        switch updateType {
        case MutualStateUpdateTypeSent:
            text = fmt.Sprintf(outgoingMutualStateEventSentDefaultText, contactID)
            contentType = protobuf.ChatMessage_SYSTEM_MESSAGE_MUTUAL_EVENT_SENT
        case MutualStateUpdateTypeAdded:
            text = fmt.Sprintf(outgoingMutualStateEventAcceptedDefaultText, contactID)
            contentType = protobuf.ChatMessage_SYSTEM_MESSAGE_MUTUAL_EVENT_ACCEPTED
        case MutualStateUpdateTypeRemoved:
            text = fmt.Sprintf(outgoingMutualStateEventRemovedDefaultText, contactID)
            contentType = protobuf.ChatMessage_SYSTEM_MESSAGE_MUTUAL_EVENT_REMOVED
        default:
            return nil, fmt.Errorf("unhandled outgoing MutualStateUpdateType = %d", updateType)
        }
    } else {
        to = m.myHexIdentity()
        from = contactID

        switch updateType {
        case MutualStateUpdateTypeSent:
            text = fmt.Sprintf(incomingMutualStateEventSentDefaultText, contactID)
            contentType = protobuf.ChatMessage_SYSTEM_MESSAGE_MUTUAL_EVENT_SENT
        case MutualStateUpdateTypeAdded:
            text = fmt.Sprintf(incomingMutualStateEventAcceptedDefaultText, contactID)
            contentType = protobuf.ChatMessage_SYSTEM_MESSAGE_MUTUAL_EVENT_ACCEPTED
        case MutualStateUpdateTypeRemoved:
            text = fmt.Sprintf(incomingMutualStateEventRemovedDefaultText, contactID)
            contentType = protobuf.ChatMessage_SYSTEM_MESSAGE_MUTUAL_EVENT_REMOVED
        default:
            return nil, fmt.Errorf("unhandled incoming MutualStateUpdateType = %d", updateType)
        }
    }

    message := &common.Message{
        ChatMessage: &protobuf.ChatMessage{
            ChatId:      contactID,
            Text:        text,
            MessageType: protobuf.MessageType_ONE_TO_ONE,
            ContentType: contentType,
            Clock:       clock,
            Timestamp:   timestamp,
        },
        From:             from,
        WhisperTimestamp: timestamp,
        LocalChatID:      contactID,
        Seen:             true,
        ID:               types.EncodeHex(crypto.Keccak256([]byte(fmt.Sprintf("%s%s%d%d", from, to, updateType, clock)))),
    }

    return message, nil
}

func (m *Messenger) acceptContactRequest(ctx context.Context, requestID string, fromSyncing bool) (*MessengerResponse, error) {
    contactRequest, err := m.persistence.MessageByID(requestID)
    if err != nil {
        m.logger.Error("could not find contact request message", zap.Error(err))
        return nil, err
    }

    m.logger.Info("acceptContactRequest")

    var ensName, nickname, displayName string
    customizationColor := multiaccountscommon.IDToColorFallbackToBlue(contactRequest.CustomizationColor)

    if contact, ok := m.allContacts.Load(contactRequest.From); ok {
        ensName = contact.EnsName
        nickname = contact.LocalNickname
        displayName = contact.DisplayName
        customizationColor = contact.CustomizationColor
    }

    response, err := m.addContact(ctx, contactRequest.From, ensName, nickname, displayName, customizationColor, contactRequest.ID, "", fromSyncing, false, false)
    if err != nil {
        return nil, err
    }

    // Force activate chat
    chat, ok := m.allChats.Load(contactRequest.From)
    if !ok {
        publicKey, err := common.HexToPubkey(contactRequest.From)
        if err != nil {
            return nil, err
        }

        chat = OneToOneFromPublicKey(publicKey, m.getTimesource())
    }

    chat.Active = true
    if err := m.saveChat(chat); err != nil {
        return nil, err
    }
    response.AddChat(chat)

    return response, nil
}

func (m *Messenger) AcceptContactRequest(ctx context.Context, request *requests.AcceptContactRequest) (*MessengerResponse, error) {
    err := request.Validate()
    if err != nil {
        return nil, err
    }

    response, err := m.acceptContactRequest(ctx, request.ID.String(), false)
    if err != nil {
        return nil, err
    }

    err = m.syncContactRequestDecision(ctx, request.ID.String(), "", true, m.dispatchMessage)
    if err != nil {
        return nil, err
    }

    return response, nil
}

func (m *Messenger) declineContactRequest(requestID, contactID string, fromSyncing bool) (*MessengerResponse, error) {
    m.logger.Info("declineContactRequest")

    contactRequest, err := m.persistence.MessageByID(requestID)
    if err == common.ErrRecordNotFound && fromSyncing {
        // original requestID(Message ID) is useless since we don't sync UserMessage in this case
        requestID = defaultContactRequestID(contactID)
        contactRequest, err = m.persistence.MessageByID(requestID)
    }
    if err != nil {
        return nil, err
    }

    response := &MessengerResponse{}
    var contact *Contact
    if contactRequest != nil {
        contact, err = m.BuildContact(&requests.BuildContact{PublicKey: contactRequest.From})
        if err != nil {
            return nil, err
        }
        contactRequest.ContactRequestState = common.ContactRequestStateDismissed
        err = m.persistence.SetContactRequestState(contactRequest.ID, contactRequest.ContactRequestState)
        if err != nil {
            return nil, err
        }
        response.AddMessage(contactRequest)
    }

    if !fromSyncing {
        _, clock, err := m.getOneToOneAndNextClock(contact)
        if err != nil {
            return nil, err
        }

        contact.DismissContactRequest(clock)
        err = m.persistence.SaveContact(contact, nil)
        if err != nil {
            return nil, err
        }

        response.AddContact(contact)
    }

    // update notification with the correct status
    notification, err := m.persistence.GetActivityCenterNotificationByID(types.FromHex(requestID))
    if err != nil {
        return nil, err
    }
    if notification != nil {
        notification.Name = contact.PrimaryName()
        notification.Message = contactRequest
        notification.Read = true
        notification.Dismissed = true
        notification.UpdatedAt = m.GetCurrentTimeInMillis()

        err = m.addActivityCenterNotification(response, notification, m.syncActivityCenterDismissedByIDs)
        if err != nil {
            m.logger.Error("failed to save notification", zap.Error(err))
            return nil, err
        }
    }
    return response, nil
}

func (m *Messenger) DeclineContactRequest(ctx context.Context, request *requests.DeclineContactRequest) (*MessengerResponse, error) {
    err := request.Validate()
    if err != nil {
        return nil, err
    }

    response, err := m.declineContactRequest(request.ID.String(), "", false)
    if err != nil {
        return nil, err
    }

    err = m.syncContactRequestDecision(ctx, request.ID.String(), "", false, m.dispatchMessage)
    if err != nil {
        return nil, err
    }

    return response, nil
}

func (m *Messenger) SendContactRequest(ctx context.Context, request *requests.SendContactRequest) (*MessengerResponse, error) {
    err := request.Validate()
    if err != nil {
        return nil, err
    }

    chatID, err := request.HexID()
    if err != nil {
        return nil, err
    }

    var ensName, nickname, displayName string
    customizationColor := multiaccountscommon.CustomizationColorBlue

    if contact, ok := m.allContacts.Load(chatID); ok {
        ensName = contact.EnsName
        nickname = contact.LocalNickname
        displayName = contact.DisplayName
        customizationColor = contact.CustomizationColor
    }

    return m.addContact(
        ctx,
        chatID,
        ensName,
        nickname,
        displayName,
        customizationColor,
        "",
        request.Message,
        false,
        false,
        true,
    )
}

func (m *Messenger) updateAcceptedContactRequest(response *MessengerResponse, contactRequestID, contactID string, fromSyncing bool) (*MessengerResponse, error) {
    m.logger.Debug("updateAcceptedContactRequest", zap.String("contactRequestID", contactRequestID), zap.String("contactID", contactID), zap.Bool("fromSyncing", fromSyncing))

    contactRequest, err := m.persistence.MessageByID(contactRequestID)
    if err == common.ErrRecordNotFound && fromSyncing {
        // original requestID(Message ID) is useless since we don't sync UserMessage in this case
        contactRequestID = defaultContactRequestID(contactID)
        contactRequest, err = m.persistence.MessageByID(contactRequestID)
    }
    if err != nil {
        m.logger.Error("contact request not found", zap.String("contactRequestID", contactRequestID), zap.Error(err))
        return nil, err
    }

    contactRequest.ContactRequestState = common.ContactRequestStateAccepted

    err = m.persistence.SetContactRequestState(contactRequest.ID, contactRequest.ContactRequestState)
    if err != nil {
        return nil, err
    }

    contact, ok := m.allContacts.Load(contactRequest.From)
    if !ok {
        m.logger.Error("failed to update contact request: contact not found", zap.String("contact id", contactRequest.From))
        return nil, errors.New("failed to update contact request: contact not found")
    }

    chat, ok := m.allChats.Load(contact.ID)
    if !ok {
        return nil, errors.New("no chat found for accepted contact request")
    }

    notification, err := m.persistence.GetActivityCenterNotificationByID(types.FromHex(contactRequest.ID))
    if err != nil {
        return nil, err
    }

    clock, _ := chat.NextClockAndTimestamp(m.transport)
    contact.AcceptContactRequest(clock)

    if !fromSyncing {
        acceptContactRequest := &protobuf.AcceptContactRequest{
            Id:    contactRequest.ID,
            Clock: clock,
        }
        encodedMessage, err := proto.Marshal(acceptContactRequest)
        if err != nil {
            return nil, err
        }
        _, err = m.dispatchMessage(context.Background(), common.RawMessage{
            LocalChatID: contactRequest.From,
            Payload:     encodedMessage,
            MessageType: protobuf.ApplicationMetadataMessage_ACCEPT_CONTACT_REQUEST,
            ResendType:  common.ResendTypeDataSync,
        })
        if err != nil {
            return nil, err
        }

        // Dispatch profile message to add a contact to the encrypted profile part
        err = m.DispatchProfileShowcase()
        if err != nil {
            return nil, err
        }
    }

    if response == nil {
        response = &MessengerResponse{}
    }

    if notification != nil {
        notification.Name = contact.PrimaryName()
        notification.Message = contactRequest
        notification.Read = true
        notification.Accepted = true
        notification.UpdatedAt = m.GetCurrentTimeInMillis()

        err = m.addActivityCenterNotification(response, notification, nil)
        if err != nil {
            m.logger.Error("failed to save notification", zap.Error(err))
            return nil, err
        }
    }

    response.AddMessage(contactRequest)
    response.AddContact(contact)

    // Add mutual state update message for incoming contact request
    clock, timestamp := chat.NextClockAndTimestamp(m.transport)
    updateMessage, err := m.prepareMutualStateUpdateMessage(contact.ID, MutualStateUpdateTypeAdded, clock, timestamp, true)
    if err != nil {
        return nil, err
    }

    err = m.prepareMessage(updateMessage, m.httpServer)
    if err != nil {
        return nil, err
    }
    err = m.persistence.SaveMessages([]*common.Message{updateMessage})
    if err != nil {
        return nil, err
    }
    response.AddMessage(updateMessage)
    response.AddChat(chat)

    return response, nil
}

func (m *Messenger) addContact(ctx context.Context,
    pubKey, ensName, nickname, displayName string,
    customizationColor multiaccountscommon.CustomizationColor,
    contactRequestID, contactRequestText string,
    fromSyncing, sendContactUpdate, createOutgoingContactRequestNotification bool) (*MessengerResponse, error) {
    contact, err := m.BuildContact(&requests.BuildContact{PublicKey: pubKey})
    if err != nil {
        return nil, err
    }

    response := &MessengerResponse{}

    chat, clock, err := m.getOneToOneAndNextClock(contact)
    if err != nil {
        return nil, err
    }

    if ensName != "" {
        err := m.ensVerifier.ENSVerified(pubKey, ensName, clock)
        if err != nil {
            return nil, err
        }
    }
    if err := m.addENSNameToContact(contact); err != nil {
        return nil, err
    }

    if len(nickname) != 0 {
        contact.LocalNickname = nickname
    }

    if len(displayName) != 0 {
        contact.DisplayName = displayName
    }

    contact.CustomizationColor = customizationColor

    contact.LastUpdatedLocally = clock
    contact.ContactRequestSent(clock)

    if !fromSyncing {
        // We sync the contact with the other devices
        err := m.syncContact(context.Background(), contact, m.dispatchMessage)
        if err != nil {
            return nil, err
        }
    }

    err = m.persistence.SaveContact(contact, nil)
    if err != nil {
        return nil, err
    }

    // TODO(samyoul) remove storing of an updated reference pointer?
    m.allContacts.Store(contact.ID, contact)

    // And we re-register for push notications
    err = m.reregisterForPushNotifications()
    if err != nil {
        return nil, err
    }

    // Reset last published time for ChatIdentity so new contact can receive data
    err = m.resetLastPublishedTimeForChatIdentity()
    if err != nil {
        return nil, err
    }

    // Profile chats are deprecated.
    // Code below can be removed after some reasonable time.

    //Create the corresponding chat
    var profileChat *Chat
    if !deprecation.ChatProfileDeprecated {
        profileChat = m.buildProfileChat(contact.ID)

        _, err = m.Join(profileChat)
        if err != nil {
            return nil, err
        }

        if err := m.saveChat(profileChat); err != nil {
            return nil, err
        }
    }

    publicKey, err := contact.PublicKey()
    if err != nil {
        return nil, err
    }

    // Fetch contact code
    _, err = m.scheduleSyncFiltersForContact(publicKey)
    if err != nil {
        return nil, err
    }

    if sendContactUpdate {
        // Get ENS name of a current user
        ensName, err = m.settings.ENSName()
        if err != nil {
            return nil, err
        }

        // Get display name of a current user
        displayName, err = m.settings.DisplayName()
        if err != nil {
            return nil, err
        }
        response, err = m.sendContactUpdate(context.Background(), pubKey, displayName, ensName, "", m.account.GetCustomizationColor(), m.dispatchMessage)
        if err != nil {
            return nil, err
        }
    }

    if len(contactRequestID) != 0 {
        updatedResponse, err := m.updateAcceptedContactRequest(response, contactRequestID, "", false)
        if err != nil {
            return nil, err
        }
        err = response.Merge(updatedResponse)
        if err != nil {
            return nil, err
        }
    }

    // Sends a standalone ChatIdentity message
    err = m.handleStandaloneChatIdentity(chat)
    if err != nil {
        return nil, err
    }

    // Profile chats are deprecated.
    // Code below can be removed after some reasonable time.

    // Add chat
    if !deprecation.ChatProfileDeprecated {
        response.AddChat(profileChat)

        _, err = m.transport.InitFilters([]transport.FiltersToInitialize{{ChatID: profileChat.ID}}, []*ecdsa.PublicKey{publicKey})
        if err != nil {
            return nil, err
        }
    }

    // Publish contact code
    err = m.publishContactCode()
    if err != nil {
        return nil, err
    }

    // Add mutual state update message for outgoing contact request
    if len(contactRequestID) == 0 {
        clock, timestamp := chat.NextClockAndTimestamp(m.transport)
        updateMessage, err := m.prepareMutualStateUpdateMessage(contact.ID, MutualStateUpdateTypeSent, clock, timestamp, true)
        if err != nil {
            return nil, err
        }

        err = m.prepareMessage(updateMessage, m.httpServer)
        if err != nil {
            return nil, err
        }
        err = m.persistence.SaveMessages([]*common.Message{updateMessage})
        if err != nil {
            return nil, err
        }
        response.AddMessage(updateMessage)
        err = chat.UpdateFromMessage(updateMessage, m.getTimesource())
        if err != nil {
            return nil, err
        }
        response.AddChat(chat)
    }

    // Add outgoing contact request notification
    if createOutgoingContactRequestNotification {
        clock, timestamp := chat.NextClockAndTimestamp(m.transport)
        contactRequest, err := m.generateContactRequest(clock, timestamp, contact, contactRequestText, true)
        if err != nil {
            return nil, err
        }

        // Send contact request as a plain chat message
        messageResponse, err := m.sendChatMessage(ctx, contactRequest)
        if err != nil {
            return nil, err
        }

        err = response.Merge(messageResponse)
        if err != nil {
            return nil, err
        }

        notification := m.generateOutgoingContactRequestNotification(contact, contactRequest)
        err = m.addActivityCenterNotification(response, notification, nil)
        if err != nil {
            return nil, err
        }
    }

    // Add contact
    response.AddContact(contact)
    return response, nil
}

func (m *Messenger) generateContactRequest(clock uint64, timestamp uint64, contact *Contact, text string, outgoing bool) (*common.Message, error) {
    if contact == nil {
        return nil, errors.New("contact cannot be nil")
    }

    contactRequest := common.NewMessage()
    contactRequest.ChatId = contact.ID
    contactRequest.WhisperTimestamp = timestamp
    contactRequest.Seen = true
    contactRequest.Text = text
    if outgoing {
        contactRequest.From = m.myHexIdentity()
        contactRequest.CustomizationColor = m.account.GetCustomizationColorID()
    } else {
        contactRequest.From = contact.ID
        contactRequest.CustomizationColor = multiaccountscommon.ColorToIDFallbackToBlue(contact.CustomizationColor)
    }
    contactRequest.LocalChatID = contact.ID
    contactRequest.ContentType = protobuf.ChatMessage_CONTACT_REQUEST
    contactRequest.Clock = clock
    if contact.mutual() {
        contactRequest.ContactRequestState = common.ContactRequestStateAccepted
    } else {
        contactRequest.ContactRequestState = common.ContactRequestStatePending
    }
    err := contactRequest.PrepareContent(common.PubkeyToHex(&m.identity.PublicKey))
    return contactRequest, err
}

func (m *Messenger) generateOutgoingContactRequestNotification(contact *Contact, contactRequest *common.Message) *ActivityCenterNotification {
    return &ActivityCenterNotification{
        ID:        types.FromHex(contactRequest.ID),
        Type:      ActivityCenterNotificationTypeContactRequest,
        Name:      contact.PrimaryName(),
        Author:    m.myHexIdentity(),
        Message:   contactRequest,
        Timestamp: m.getTimesource().GetCurrentTime(),
        ChatID:    contact.ID,
        Read: contactRequest.ContactRequestState == common.ContactRequestStateAccepted ||
            contactRequest.ContactRequestState == common.ContactRequestStateDismissed ||
            contactRequest.ContactRequestState == common.ContactRequestStatePending,
        Accepted:  contactRequest.ContactRequestState == common.ContactRequestStateAccepted,
        Dismissed: contactRequest.ContactRequestState == common.ContactRequestStateDismissed,
        UpdatedAt: m.GetCurrentTimeInMillis(),
    }
}

func (m *Messenger) AddContact(ctx context.Context, request *requests.AddContact) (*MessengerResponse, error) {
    err := request.Validate()
    if err != nil {
        return nil, err
    }

    id, err := request.HexID()
    if err != nil {
        return nil, err
    }

    return m.addContact(
        ctx,
        id,
        request.ENSName,
        request.Nickname,
        request.DisplayName,
        multiaccountscommon.CustomizationColor(request.CustomizationColor),
        "",
        defaultContactRequestText(),
        false,
        true,
        true,
    )
}

func (m *Messenger) resetLastPublishedTimeForChatIdentity() error {
    // Reset last published time for ChatIdentity so new contact can receive data
    contactCodeTopic := transport.ContactCodeTopic(&m.identity.PublicKey)
    m.logger.Debug("contact state changed ResetWhenChatIdentityLastPublished")
    return m.persistence.ResetWhenChatIdentityLastPublished(contactCodeTopic)
}

func (m *Messenger) removeContact(ctx context.Context, response *MessengerResponse, pubKey string, sync bool) error {
    contact, ok := m.allContacts.Load(pubKey)
    if !ok {
        return ErrContactNotFound
    }

    // System message for mutual state update
    chat, clock, err := m.getOneToOneAndNextClock(contact)
    if err != nil {
        return err
    }
    timestamp := m.getTimesource().GetCurrentTime()
    updateMessage, err := m.prepareMutualStateUpdateMessage(contact.ID, MutualStateUpdateTypeRemoved, clock, timestamp, true)
    if err != nil {
        return err
    }

    err = m.prepareMessage(updateMessage, m.httpServer)
    if err != nil {
        return err
    }
    err = m.persistence.SaveMessages([]*common.Message{updateMessage})
    if err != nil {
        return err
    }
    response.AddMessage(updateMessage)
    err = chat.UpdateFromMessage(updateMessage, m.getTimesource())
    if err != nil {
        return err
    }
    response.AddChat(chat)

    // Next we retract a contact request
    contact.RetractContactRequest(clock)
    contact.LastUpdatedLocally = m.getTimesource().GetCurrentTime()

    err = m.persistence.SaveContact(contact, nil)
    if err != nil {
        return err
    }

    if sync {
        err = m.syncContact(context.Background(), contact, m.dispatchMessage)
        if err != nil {
            return err
        }
    }

    // TODO(samyoul) remove storing of an updated reference pointer?
    m.allContacts.Store(contact.ID, contact)

    // And we re-register for push notications
    err = m.reregisterForPushNotifications()
    if err != nil {
        return err
    }

    // Dispatch profile message to remove a contact from the encrypted profile part
    err = m.DispatchProfileShowcase()
    if err != nil {
        return err
    }

    // Profile chats are deprecated.
    // Code below can be removed after some reasonable time.

    //Create the corresponding profile chat
    if !deprecation.ChatProfileDeprecated {
        profileChatID := buildProfileChatID(contact.ID)
        _, ok = m.allChats.Load(profileChatID)

        if ok {
            chatResponse, err := m.deactivateChat(profileChatID, 0, false, true)
            if err != nil {
                return err
            }
            err = response.Merge(chatResponse)
            if err != nil {
                return err
            }
        }
    }

    response.Contacts = []*Contact{contact}
    return nil
}

func (m *Messenger) RemoveContact(ctx context.Context, pubKey string) (*MessengerResponse, error) {
    response := new(MessengerResponse)

    err := m.removeContact(ctx, response, pubKey, true)
    if err != nil {
        return nil, err
    }

    return response, nil
}

func (m *Messenger) updateContactImagesURL(contact *Contact) error {
    if m.httpServer != nil {
        for k, v := range contact.Images {
            publicKey, err := contact.PublicKey()
            if err != nil {
                return err
            }
            v.LocalURL = m.httpServer.MakeContactImageURL(common.PubkeyToHex(publicKey), k)
            contact.Images[k] = v
        }
    }
    return nil
}

func (m *Messenger) Contacts() []*Contact {
    var contacts []*Contact
    m.allContacts.Range(func(contactID string, contact *Contact) (shouldContinue bool) {
        contacts = append(contacts, contact)
        return true
    })
    return contacts
}

func (m *Messenger) AddedContacts() []*Contact {
    var contacts []*Contact
    m.allContacts.Range(func(contactID string, contact *Contact) (shouldContinue bool) {
        if contact.added() {
            contacts = append(contacts, contact)
        }
        return true
    })
    return contacts
}

func (m *Messenger) MutualContacts() []*Contact {
    var contacts []*Contact
    m.allContacts.Range(func(contactID string, contact *Contact) (shouldContinue bool) {
        if contact.mutual() {
            contacts = append(contacts, contact)
        }
        return true
    })
    return contacts
}

func (m *Messenger) BlockedContacts() []*Contact {
    var contacts []*Contact
    m.allContacts.Range(func(contactID string, contact *Contact) (shouldContinue bool) {
        if contact.Blocked {
            contacts = append(contacts, contact)
        }
        return true
    })
    return contacts
}

// GetContactByID returns a Contact for given pubKey, if it's known.
// This function automatically checks if pubKey is self identity key and returns a Contact
// filled with self information.
// pubKey is assumed to include `0x` prefix
func (m *Messenger) GetContactByID(pubKey string) *Contact {
    if pubKey == m.IdentityPublicKeyString() {
        return m.selfContact
    }
    contact, _ := m.allContacts.Load(pubKey)
    return contact
}

func (m *Messenger) GetSelfContact() *Contact {
    return m.selfContact
}

func (m *Messenger) SetContactLocalNickname(request *requests.SetContactLocalNickname) (*MessengerResponse, error) {

    if err := request.Validate(); err != nil {
        return nil, err
    }

    pubKey := request.ID.String()
    nickname := request.Nickname

    contact, err := m.BuildContact(&requests.BuildContact{PublicKey: pubKey})
    if err != nil {
        return nil, err
    }

    if err := m.addENSNameToContact(contact); err != nil {
        return nil, err
    }

    clock := m.getTimesource().GetCurrentTime()
    contact.LocalNickname = nickname
    contact.LastUpdatedLocally = clock

    err = m.persistence.SaveContact(contact, nil)
    if err != nil {
        return nil, err
    }

    m.allContacts.Store(contact.ID, contact)

    response := &MessengerResponse{}
    response.Contacts = []*Contact{contact}

    err = m.syncContact(context.Background(), contact, m.dispatchMessage)
    if err != nil {
        return nil, err
    }

    return response, nil
}

func (m *Messenger) blockContact(ctx context.Context, response *MessengerResponse, contactID string, isDesktopFunc bool, fromSyncing bool) error {
    contact, err := m.BuildContact(&requests.BuildContact{PublicKey: contactID})
    if err != nil {
        return err
    }

    response.AddContact(contact)

    _, clock, err := m.getOneToOneAndNextClock(contact)
    if err != nil {
        return err
    }

    contactWasAdded := contact.added()
    contact.Block(clock)

    contact.LastUpdatedLocally = m.getTimesource().GetCurrentTime()

    chats, err := m.persistence.BlockContact(contact, isDesktopFunc)
    if err != nil {
        return err
    }

    response.AddChats(chats)

    m.allContacts.Store(contact.ID, contact)
    for _, chat := range chats {
        m.allChats.Store(chat.ID, chat)
    }

    if !isDesktopFunc {
        m.allChats.Delete(contact.ID)
        m.allChats.Delete(buildProfileChatID(contact.ID))
    }

    if !fromSyncing {
        if contactWasAdded {
            err = m.sendRetractContactRequest(contact)
            if err != nil {
                return err
            }
        }

        err = m.syncContact(context.Background(), contact, m.dispatchMessage)
        if err != nil {
            return err
        }

        // We remove anything that's related to this contact request
        updatedAt := m.GetCurrentTimeInMillis()
        notifications, err := m.persistence.DeleteChatContactRequestActivityCenterNotifications(contact.ID, updatedAt)
        if err != nil {
            return err
        }
        err = m.syncActivityCenterDeleted(ctx, notifications, updatedAt)
        if err != nil {
            m.logger.Error("BlockContact, error syncing activity center notifications as deleted", zap.Error(err))
            return err
        }
    }

    // re-register for push notifications
    err = m.reregisterForPushNotifications()
    if err != nil {
        return err
    }

    return nil
}

func (m *Messenger) BlockContact(ctx context.Context, contactID string, fromSyncing bool) (*MessengerResponse, error) {
    response := &MessengerResponse{}

    err := m.blockContact(ctx, response, contactID, false, fromSyncing)
    if err != nil {
        return nil, err
    }

    response, err = m.DeclineAllPendingGroupInvitesFromUser(ctx, response, contactID)
    if err != nil {
        return nil, err
    }

    // AC notifications are synced separately
    // NOTE: Should we still do the local part (persistence.dismiss...) and only skip the syncing?
    //         This would make the solution more reliable even in case AC notification sync is not recevied.
    //         This should be considered separately, I'm not sure if that's safe.
    //         https://github.com/status-im/status-go/issues/3720
    if !fromSyncing {
        updatedAt := m.GetCurrentTimeInMillis()
        _, err = m.DismissAllActivityCenterNotificationsFromUser(ctx, contactID, updatedAt)
        if err != nil {
            return nil, err
        }
    }

    return response, nil
}

// The same function as the one above.
// Should be removed with https://github.com/status-im/status-desktop/issues/8805
func (m *Messenger) BlockContactDesktop(ctx context.Context, contactID string) (*MessengerResponse, error) {
    response := &MessengerResponse{}

    err := m.blockContact(ctx, response, contactID, true, false)
    if err != nil {
        return nil, err
    }

    response, err = m.DeclineAllPendingGroupInvitesFromUser(ctx, response, contactID)
    if err != nil {
        return nil, err
    }

    notifications, err := m.DismissAllActivityCenterNotificationsFromUser(ctx, contactID, m.GetCurrentTimeInMillis())
    if err != nil {
        return nil, err
    }
    response.AddActivityCenterNotifications(notifications)
    return response, nil
}

func (m *Messenger) UnblockContact(contactID string) (*MessengerResponse, error) {
    response := &MessengerResponse{}
    contact, ok := m.allContacts.Load(contactID)
    if !ok || !contact.Blocked {
        return response, nil
    }

    _, clock, err := m.getOneToOneAndNextClock(contact)
    if err != nil {
        return nil, err
    }

    contact.Unblock(clock)

    contact.LastUpdatedLocally = m.getTimesource().GetCurrentTime()

    err = m.persistence.SaveContact(contact, nil)
    if err != nil {
        return nil, err
    }

    m.allContacts.Store(contact.ID, contact)

    response.AddContact(contact)

    err = m.syncContact(context.Background(), contact, m.dispatchMessage)
    if err != nil {
        return nil, err
    }

    // re-register for push notifications
    err = m.reregisterForPushNotifications()
    if err != nil {
        return nil, err
    }

    return response, nil
}

// Send contact updates to all contacts added by us
func (m *Messenger) SendContactUpdates(ctx context.Context, ensName, profileImage string, customizationColor multiaccountscommon.CustomizationColor) (err error) {
    myID := contactIDFromPublicKey(&m.identity.PublicKey)

    displayName, err := m.settings.DisplayName()
    if err != nil {
        return err
    }

    if len(customizationColor) == 0 && m.account != nil {
        customizationColor = m.account.GetCustomizationColor()
    }

    if _, err = m.sendContactUpdate(ctx, myID, displayName, ensName, profileImage, customizationColor, m.dispatchMessage); err != nil {
        return err
    }

    // TODO: This should not be sending paired messages, as we do it above
    m.allContacts.Range(func(contactID string, contact *Contact) (shouldContinue bool) {
        if contact.added() {
            if _, err = m.sendContactUpdate(ctx, contact.ID, displayName, ensName, profileImage, customizationColor, m.dispatchMessage); err != nil {
                return false
            }
        }
        return true
    })
    return err
}

// 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, customizationColor multiaccountscommon.CustomizationColor) (*MessengerResponse, error) {
    displayName, err := m.settings.DisplayName()
    if err != nil {
        return nil, err
    }

    return m.sendContactUpdate(ctx, chatID, displayName, ensName, profileImage, customizationColor, m.dispatchMessage)
}

func (m *Messenger) sendContactUpdate(ctx context.Context,
    chatID, displayName, ensName, profileImage string,
    customizationColor multiaccountscommon.CustomizationColor,
    rawMessageHandler RawMessageHandler) (*MessengerResponse, error) {
    var response MessengerResponse

    contact, ok := m.allContacts.Load(chatID)
    if !ok || !contact.added() {
        return nil, nil
    }

    chat, clock, err := m.getOneToOneAndNextClock(contact)
    if err != nil {
        return nil, err
    }

    contactUpdate := &protobuf.ContactUpdate{
        Clock:                         clock,
        DisplayName:                   displayName,
        EnsName:                       ensName,
        ProfileImage:                  profileImage,
        ContactRequestClock:           contact.ContactRequestLocalClock,
        ContactRequestPropagatedState: contact.ContactRequestPropagatedState(),
        PublicKey:                     contact.ID,
        CustomizationColor:            multiaccountscommon.ColorToIDFallbackToBlue(customizationColor),
    }

    encodedMessage, err := proto.Marshal(contactUpdate)
    if err != nil {
        return nil, err
    }

    rawMessage := common.RawMessage{
        LocalChatID: chatID,
        Payload:     encodedMessage,
        MessageType: protobuf.ApplicationMetadataMessage_CONTACT_UPDATE,
        ResendType:  common.ResendTypeDataSync,
    }

    _, err = rawMessageHandler(ctx, rawMessage)
    if err != nil {
        return nil, err
    }

    response.Contacts = []*Contact{contact}
    response.AddChat(chat)

    chat.LastClockValue = clock
    err = m.saveChat(chat)
    if err != nil {
        return nil, err
    }
    return &response, nil
}

func (m *Messenger) addENSNameToContact(contact *Contact) error {

    // Check if there's already a verified record
    ensRecord, err := m.ensVerifier.GetVerifiedRecord(contact.ID)
    if err != nil {
        return err
    }
    if ensRecord == nil {
        return nil
    }

    contact.EnsName = ensRecord.Name
    contact.ENSVerified = true

    return nil
}

func (m *Messenger) RetractContactRequest(request *requests.RetractContactRequest) (*MessengerResponse, error) {
    err := request.Validate()
    if err != nil {
        return nil, err
    }
    contact, ok := m.allContacts.Load(request.ID.String())
    if !ok {
        return nil, errors.New("contact not found")
    }
    response := &MessengerResponse{}
    err = m.removeContact(context.Background(), response, contact.ID, true)
    if err != nil {
        return nil, err
    }

    err = m.sendRetractContactRequest(contact)
    if err != nil {
        return nil, err
    }

    return response, err
}

// Send message to remote account to remove our contact from their end.
func (m *Messenger) sendRetractContactRequest(contact *Contact) error {
    _, clock, err := m.getOneToOneAndNextClock(contact)
    if err != nil {
        return err
    }
    retractContactRequest := &protobuf.RetractContactRequest{
        Clock: clock,
    }

    encodedMessage, err := proto.Marshal(retractContactRequest)
    if err != nil {
        return err
    }

    _, err = m.dispatchMessage(context.Background(), common.RawMessage{
        LocalChatID: contact.ID,
        Payload:     encodedMessage,
        MessageType: protobuf.ApplicationMetadataMessage_RETRACT_CONTACT_REQUEST,
        ResendType:  common.ResendTypeDataSync,
    })
    if err != nil {
        return err
    }

    return err
}

func (m *Messenger) GetLatestContactRequestForContact(contactID string) (*MessengerResponse, error) {
    if len(contactID) == 0 {
        return nil, ErrGetLatestContactRequestForContactInvalidID
    }

    contactRequestID, err := m.persistence.LatestPendingContactRequestIDForContact(contactID)
    if err != nil {
        return nil, err
    }

    contactRequest, err := m.persistence.MessageByID(contactRequestID)
    if err != nil {
        m.logger.Error("contact request not found", zap.String("contactRequestID", contactRequestID), zap.Error(err))
        return nil, err
    }

    response := &MessengerResponse{}
    response.AddMessage(contactRequest)

    return response, nil
}

func (m *Messenger) AcceptLatestContactRequestForContact(ctx context.Context, request *requests.AcceptLatestContactRequestForContact) (*MessengerResponse, error) {
    if err := request.Validate(); err != nil {
        return nil, err
    }

    contactRequestID, err := m.persistence.LatestPendingContactRequestIDForContact(request.ID.String())
    if err != nil {
        return nil, err
    }

    return m.AcceptContactRequest(ctx, &requests.AcceptContactRequest{ID: types.Hex2Bytes(contactRequestID)})
}

func (m *Messenger) DismissLatestContactRequestForContact(ctx context.Context, request *requests.DismissLatestContactRequestForContact) (*MessengerResponse, error) {
    if err := request.Validate(); err != nil {
        return nil, err
    }

    contactRequestID, err := m.persistence.LatestPendingContactRequestIDForContact(request.ID.String())
    if err != nil {
        return nil, err
    }

    return m.DeclineContactRequest(ctx, &requests.DeclineContactRequest{ID: types.Hex2Bytes(contactRequestID)})
}

func (m *Messenger) PendingContactRequests(cursor string, limit int) ([]*common.Message, string, error) {
    return m.persistence.PendingContactRequests(cursor, limit)
}

func defaultContactRequestID(contactID string) string {
    return "0x" + types.Bytes2Hex(append(types.Hex2Bytes(contactID), 0x20))
}

func defaultContactRequestText() string {
    return "Please add me to your contacts"
}

func (m *Messenger) BuildContact(request *requests.BuildContact) (*Contact, error) {
    contact, ok := m.allContacts.Load(request.PublicKey)
    if !ok {
        var err error
        contact, err = buildContactFromPkString(request.PublicKey)
        if err != nil {
            return nil, err
        }

        if request.ENSName != "" {
            contact.ENSVerified = true
            contact.EnsName = request.ENSName
        }

        if len(contact.CustomizationColor) == 0 {
            contact.CustomizationColor = multiaccountscommon.CustomizationColorBlue
        }
    }

    // Schedule sync filter to fetch information about the contact
    publicKey, err := contact.PublicKey()
    if err != nil {
        return nil, err
    }

    _, err = m.scheduleSyncFiltersForContact(publicKey)
    if err != nil {
        return nil, err
    }

    return contact, nil
}

func (m *Messenger) scheduleSyncFiltersForContact(publicKey *ecdsa.PublicKey) (*transport.Filter, error) {
    filter, err := m.transport.JoinPrivate(publicKey)
    if err != nil {
        return nil, err
    }
    _, err = m.scheduleSyncFilters([]*transport.Filter{filter})
    if err != nil {
        return filter, err
    }
    return filter, nil
}

func (m *Messenger) FetchContact(contactID string, waitForResponse bool) (*Contact, error) {
    options := []StoreNodeRequestOption{
        WithWaitForResponseOption(waitForResponse),
    }
    contact, _, err := m.storeNodeRequestsManager.FetchContact(contactID, options)
    return contact, err
}

func (m *Messenger) SubscribeToSelfContactChanges() chan *SelfContactChangeEvent {
    s := make(chan *SelfContactChangeEvent, 10)
    m.selfContactSubscriptions = append(m.selfContactSubscriptions, s)
    return s
}

func (m *Messenger) publishSelfContactSubscriptions(event *SelfContactChangeEvent) {
    for _, s := range m.selfContactSubscriptions {
        select {
        case s <- event:
        default:
            log.Warn("self contact subscription channel full, dropping message")
        }
    }
}