status-go/protocol/messenger_contacts.go

1343 lines
37 KiB
Go

package protocol
import (
"context"
"crypto/ecdsa"
"errors"
"fmt"
"github.com/golang/protobuf/proto"
"go.uber.org/zap"
"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"
"github.com/status-im/status-go/logutils"
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(m.ctx, 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:
logutils.ZapLogger().Warn("self contact subscription channel full, dropping message")
}
}
}