status-im/status-go

View on GitHub
protocol/contact.go

Summary

Maintainability
A
3 hrs
Test Coverage
B
84%
package protocol

import (
    "crypto/ecdsa"
    "encoding/json"
    "fmt"

    accountJson "github.com/status-im/status-go/account/json"
    "github.com/status-im/status-go/api/multiformat"
    "github.com/status-im/status-go/eth-node/crypto"
    "github.com/status-im/status-go/eth-node/types"
    "github.com/status-im/status-go/images"
    "github.com/status-im/status-go/multiaccounts"
    "github.com/status-im/status-go/multiaccounts/accounts"
    multiaccountscommon "github.com/status-im/status-go/multiaccounts/common"
    "github.com/status-im/status-go/multiaccounts/settings"
    "github.com/status-im/status-go/protocol/common"
    "github.com/status-im/status-go/protocol/protobuf"
    "github.com/status-im/status-go/protocol/verification"
)

type ContactRequestState int

const (
    ContactRequestStateNone ContactRequestState = iota
    ContactRequestStateMutual
    ContactRequestStateSent
    // Received is a confusing state, we should use
    // sent for both, since they are now stored in different
    // states
    ContactRequestStateReceived
    ContactRequestStateDismissed
)

type MutualStateUpdateType int

const (
    MutualStateUpdateTypeSent MutualStateUpdateType = iota + 1
    MutualStateUpdateTypeAdded
    MutualStateUpdateTypeRemoved
)

// ContactDeviceInfo is a struct containing information about a particular device owned by a contact
type ContactDeviceInfo struct {
    // The installation id of the device
    InstallationID string `json:"id"`
    // Timestamp represents the last time we received this info
    Timestamp int64 `json:"timestamp"`
    // FCMToken is to be used for push notifications
    FCMToken string `json:"fcmToken"`
}

func (c *Contact) CanonicalImage(profilePicturesVisibility settings.ProfilePicturesVisibilityType) string {
    if profilePicturesVisibility == settings.ProfilePicturesVisibilityNone || (profilePicturesVisibility == settings.ProfilePicturesVisibilityContactsOnly && !c.added()) {
        return c.Identicon
    }

    if largeImage, ok := c.Images[images.LargeDimName]; ok {
        imageBase64, err := largeImage.GetDataURI()
        if err == nil {
            return imageBase64
        }
    }

    if thumbImage, ok := c.Images[images.SmallDimName]; ok {
        imageBase64, err := thumbImage.GetDataURI()
        if err == nil {
            return imageBase64
        }
    }

    return c.Identicon
}

type VerificationStatus int

const (
    VerificationStatusUNVERIFIED VerificationStatus = iota
    VerificationStatusVERIFYING
    VerificationStatusVERIFIED
)

// Contact has information about a "Contact"
type Contact struct {
    // ID of the contact. It's a hex-encoded public key (prefixed with 0x).
    ID string `json:"id"`
    // Ethereum address of the contact
    Address string `json:"address,omitempty"`
    // ENS name of contact
    EnsName string `json:"name,omitempty"`
    // EnsVerified whether we verified the name of the contact
    ENSVerified bool `json:"ensVerified"`
    // Generated username name of the contact
    Alias string `json:"alias,omitempty"`
    // Identicon generated from public key
    Identicon string `json:"identicon"`
    // LastUpdated is the last time we received an update from the contact
    // updates should be discarded if last updated is less than the one stored
    LastUpdated uint64 `json:"lastUpdated"`

    // LastUpdatedLocally is the last time we updated the contact locally
    LastUpdatedLocally uint64 `json:"lastUpdatedLocally"`

    LocalNickname string `json:"localNickname,omitempty"`

    // Display name of the contact
    DisplayName string `json:"displayName"`

    // Customization color of the contact
    CustomizationColor multiaccountscommon.CustomizationColor `json:"customizationColor,omitempty"`

    // Bio - description of the contact (tell us about yourself)
    Bio string `json:"bio"`

    Images map[string]images.IdentityImage `json:"images"`

    Blocked bool `json:"blocked"`

    // ContactRequestRemoteState is the state of the contact request
    // on the contact's end
    ContactRequestRemoteState ContactRequestState `json:"contactRequestRemoteState"`
    // ContactRequestRemoteClock is the clock for incoming contact requests
    ContactRequestRemoteClock uint64 `json:"contactRequestRemoteClock"`

    // ContactRequestLocalState is the state of the contact request
    // on our end
    ContactRequestLocalState ContactRequestState `json:"contactRequestLocalState"`
    // ContactRequestLocalClock is the clock for outgoing contact requests
    ContactRequestLocalClock uint64 `json:"contactRequestLocalClock"`

    IsSyncing bool
    Removed   bool

    VerificationStatus VerificationStatus       `json:"verificationStatus"`
    TrustStatus        verification.TrustStatus `json:"trustStatus"`
}

func (c Contact) IsVerified() bool {
    return c.VerificationStatus == VerificationStatusVERIFIED
}

func (c Contact) IsVerifying() bool {
    return c.VerificationStatus == VerificationStatusVERIFYING
}

func (c Contact) IsUnverified() bool {
    return c.VerificationStatus == VerificationStatusUNVERIFIED
}

func (c Contact) IsUntrustworthy() bool {
    return c.TrustStatus == verification.TrustStatusUNTRUSTWORTHY
}

func (c Contact) IsTrusted() bool {
    return c.TrustStatus == verification.TrustStatusTRUSTED
}

func (c Contact) PublicKey() (*ecdsa.PublicKey, error) {
    b, err := types.DecodeHex(c.ID)
    if err != nil {
        return nil, err
    }
    return crypto.UnmarshalPubkey(b)
}

func (c *Contact) Block(clock uint64) {
    c.Blocked = true
    c.DismissContactRequest(clock)
    c.Removed = true
}

func (c *Contact) BlockDesktop() {
    c.Blocked = true
}

func (c *Contact) Unblock(clock uint64) {
    c.Blocked = false
    // Reset the contact request flow
    c.RetractContactRequest(clock)
}

func (c *Contact) added() bool {
    return c.ContactRequestLocalState == ContactRequestStateSent
}

func (c *Contact) hasAddedUs() bool {
    return c.ContactRequestRemoteState == ContactRequestStateReceived
}

func (c *Contact) mutual() bool {
    return c.added() && c.hasAddedUs()
}

func (c *Contact) active() bool {
    return c.mutual() && !c.Blocked
}

func (c *Contact) dismissed() bool {
    return c.ContactRequestLocalState == ContactRequestStateDismissed
}

func (c *Contact) names() []string {
    var names []string

    if c.LocalNickname != "" {
        names = append(names, c.LocalNickname)
    }

    if c.ENSVerified && len(c.EnsName) != 0 {
        names = append(names, c.EnsName)
    }

    if c.DisplayName != "" {
        names = append(names, c.DisplayName)
    }

    return append(names, c.Alias)

}

func (c *Contact) PrimaryName() string {
    return c.names()[0]
}

func (c *Contact) SecondaryName() string {
    // Only shown if the user has a nickname
    if c.LocalNickname == "" {
        return ""
    }
    names := c.names()
    if len(names) > 1 {
        return names[1]
    }
    return ""
}

type ContactRequestProcessingResponse struct {
    processed                 bool
    newContactRequestReceived bool
    sendBackState             bool
}

func (c *Contact) ContactRequestSent(clock uint64) ContactRequestProcessingResponse {
    if clock <= c.ContactRequestLocalClock {
        return ContactRequestProcessingResponse{}
    }

    c.ContactRequestLocalClock = clock
    c.ContactRequestLocalState = ContactRequestStateSent

    c.Removed = false

    return ContactRequestProcessingResponse{processed: true}
}

func (c *Contact) AcceptContactRequest(clock uint64) ContactRequestProcessingResponse {
    // We treat accept the same as sent, that's because accepting a contact
    // request that does not exist is possible if the instruction is coming from
    // a different device, we'd rather assume that a contact requested existed
    // and didn't reach our device than being in an inconsistent state
    return c.ContactRequestSent(clock)
}

func (c *Contact) RetractContactRequest(clock uint64) ContactRequestProcessingResponse {
    if clock <= c.ContactRequestLocalClock {
        return ContactRequestProcessingResponse{}
    }

    // This is a symmetric action, we set both local & remote clock
    // since we want everything before this point discarded, regardless
    // the side it was sent from
    c.ContactRequestLocalClock = clock
    c.ContactRequestLocalState = ContactRequestStateNone
    c.ContactRequestRemoteState = ContactRequestStateNone
    c.ContactRequestRemoteClock = clock
    c.Removed = true

    return ContactRequestProcessingResponse{processed: true}
}

func (c *Contact) DismissContactRequest(clock uint64) ContactRequestProcessingResponse {
    if clock <= c.ContactRequestLocalClock {
        return ContactRequestProcessingResponse{}
    }

    c.ContactRequestLocalClock = clock
    c.ContactRequestLocalState = ContactRequestStateDismissed

    return ContactRequestProcessingResponse{processed: true}
}

// Remote actions

func (c *Contact) contactRequestRetracted(clock uint64, fromSyncing bool, r ContactRequestProcessingResponse) ContactRequestProcessingResponse {
    if clock <= c.ContactRequestRemoteClock {
        return r
    }

    // This is a symmetric action, we set both local & remote clock
    // since we want everything before this point discarded, regardless
    // the side it was sent from. The only exception is when the contact
    // request has been explicitly dismissed, in which case we don't
    // change state
    if c.ContactRequestLocalState != ContactRequestStateDismissed && !fromSyncing {
        c.ContactRequestLocalClock = clock
        c.ContactRequestLocalState = ContactRequestStateNone
    }
    c.ContactRequestRemoteClock = clock
    c.ContactRequestRemoteState = ContactRequestStateNone
    r.processed = true
    return r
}

func (c *Contact) ContactRequestRetracted(clock uint64, fromSyncing bool) ContactRequestProcessingResponse {
    return c.contactRequestRetracted(clock, fromSyncing, ContactRequestProcessingResponse{})
}

func (c *Contact) contactRequestReceived(clock uint64, r ContactRequestProcessingResponse) ContactRequestProcessingResponse {
    if clock <= c.ContactRequestRemoteClock {
        return r
    }
    r.processed = true
    c.ContactRequestRemoteClock = clock
    switch c.ContactRequestRemoteState {
    case ContactRequestStateNone:
        r.newContactRequestReceived = true
    }
    c.ContactRequestRemoteState = ContactRequestStateReceived

    return r
}

func (c *Contact) ContactRequestReceived(clock uint64) ContactRequestProcessingResponse {
    return c.contactRequestReceived(clock, ContactRequestProcessingResponse{})
}

func (c *Contact) ContactRequestAccepted(clock uint64) ContactRequestProcessingResponse {
    if clock <= c.ContactRequestRemoteClock {
        return ContactRequestProcessingResponse{}
    }
    // We treat received and accepted in the same way
    // since the intention is clear on the other side
    // and there's no difference
    return c.ContactRequestReceived(clock)
}

func buildContactFromPkString(pkString string) (*Contact, error) {
    publicKeyBytes, err := types.DecodeHex(pkString)
    if err != nil {
        return nil, err
    }

    publicKey, err := crypto.UnmarshalPubkey(publicKeyBytes)
    if err != nil {
        return nil, err
    }

    return buildContact(pkString, publicKey)
}

func BuildContactFromPublicKey(publicKey *ecdsa.PublicKey) (*Contact, error) {
    id := common.PubkeyToHex(publicKey)
    return buildContact(id, publicKey)
}

func getShortenedCompressedKey(publicKey string) string {
    if len(publicKey) > 9 {
        firstPart := publicKey[0:3]
        ellipsis := "..."
        publicKeySize := len(publicKey)
        lastPart := publicKey[publicKeySize-6 : publicKeySize]
        abbreviatedKey := fmt.Sprintf("%s%s%s", firstPart, ellipsis, lastPart)
        return abbreviatedKey
    }
    return ""
}

func buildContact(publicKeyString string, publicKey *ecdsa.PublicKey) (*Contact, error) {
    compressedKey, err := multiformat.SerializeLegacyKey(common.PubkeyToHex(publicKey))
    if err != nil {
        return nil, err
    }

    address := crypto.PubkeyToAddress(*publicKey)

    contact := &Contact{
        ID:                 publicKeyString,
        Alias:              getShortenedCompressedKey(compressedKey),
        Address:            types.EncodeHex(address[:]),
        CustomizationColor: multiaccountscommon.CustomizationColorBlue,
    }

    return contact, nil
}

func buildSelfContact(identity *ecdsa.PrivateKey, settings *accounts.Database, multiAccounts *multiaccounts.Database, account *multiaccounts.Account) (*Contact, error) {
    myPublicKeyString := types.EncodeHex(crypto.FromECDSAPub(&identity.PublicKey))

    c, err := buildContact(myPublicKeyString, &identity.PublicKey)
    if err != nil {
        return nil, fmt.Errorf("failed to build contact: %w", err)
    }

    if settings != nil {
        if s, err := settings.GetSettings(); err == nil {
            c.DisplayName = s.DisplayName
            c.Bio = s.Bio
            if s.PreferredName != nil {
                c.EnsName = *s.PreferredName
            }
        }
    }

    if multiAccounts != nil && account != nil {
        if identityImages, err := multiAccounts.GetIdentityImages(account.KeyUID); err != nil {
            imagesMap := make(map[string]images.IdentityImage)
            for _, img := range identityImages {
                imagesMap[img.Name] = *img
            }

            c.Images = imagesMap
        }
        if len(account.CustomizationColor) != 0 {
            c.CustomizationColor = account.CustomizationColor
        }
    }

    return c, nil
}

func contactIDFromPublicKey(key *ecdsa.PublicKey) string {
    return types.EncodeHex(crypto.FromECDSAPub(key))
}

func contactIDFromPublicKeyString(key string) (string, error) {
    pubKey, err := common.HexToPubkey(key)
    if err != nil {
        return "", err
    }

    return contactIDFromPublicKey(pubKey), nil
}

func (c *Contact) ProcessSyncContactRequestState(remoteState ContactRequestState, remoteClock uint64, localState ContactRequestState, localClock uint64) {
    // We process the two separately, first local state
    switch localState {
    case ContactRequestStateDismissed:
        c.DismissContactRequest(localClock)
    case ContactRequestStateNone:
        c.RetractContactRequest(localClock)
    case ContactRequestStateSent:
        c.ContactRequestSent(localClock)
    }

    // and later remote state
    switch remoteState {
    case ContactRequestStateReceived:
        c.ContactRequestReceived(remoteClock)
    case ContactRequestStateNone:
        c.ContactRequestRetracted(remoteClock, true)
    }
}

func (c *Contact) MarshalJSON() ([]byte, error) {
    type Alias Contact
    type ContactType struct {
        *Alias
        Added               bool                `json:"added"`
        ContactRequestState ContactRequestState `json:"contactRequestState"`
        HasAddedUs          bool                `json:"hasAddedUs"`
        Mutual              bool                `json:"mutual"`
        Active              bool                `json:"active"`
        PrimaryName         string              `json:"primaryName"`
        SecondaryName       string              `json:"secondaryName,omitempty"`
    }

    item := ContactType{
        Alias: (*Alias)(c),
    }

    item.Added = c.added()
    item.HasAddedUs = c.hasAddedUs()
    item.Mutual = c.mutual()
    item.Active = c.active()
    item.PrimaryName = c.PrimaryName()
    item.SecondaryName = c.SecondaryName()

    if c.mutual() {
        item.ContactRequestState = ContactRequestStateMutual
    } else if c.dismissed() {
        item.ContactRequestState = ContactRequestStateDismissed
    } else if c.added() {
        item.ContactRequestState = ContactRequestStateSent
    } else if c.hasAddedUs() {
        item.ContactRequestState = ContactRequestStateReceived
    }
    ext, err := accountJson.ExtendStructWithPubKeyData(item.ID, item)
    if err != nil {
        return nil, err
    }

    return json.Marshal(ext)
}

// ContactRequestPropagatedStateReceived handles the propagation of state from
// the other end.
func (c *Contact) ContactRequestPropagatedStateReceived(state *protobuf.ContactRequestPropagatedState) ContactRequestProcessingResponse {

    // It's inverted, as their local states is our remote state
    expectedLocalState := ContactRequestState(state.RemoteState)
    expectedLocalClock := state.RemoteClock

    remoteState := ContactRequestState(state.LocalState)
    remoteClock := state.LocalClock

    response := ContactRequestProcessingResponse{}

    // If we notice that the state is not consistent, and their clock is
    // outdated, we send back the state so they can catch up.
    if expectedLocalClock < c.ContactRequestLocalClock && expectedLocalState != c.ContactRequestLocalState {
        response.processed = true
        response.sendBackState = true
    }

    // If they expect our state to be more up-to-date, we only
    // trust it if the state is set to None, in this case we can trust
    // it, since a retraction can be initiated by both parties
    if expectedLocalClock > c.ContactRequestLocalClock && c.ContactRequestLocalState != ContactRequestStateDismissed && expectedLocalState == ContactRequestStateNone {
        response.processed = true
        c.ContactRequestLocalClock = expectedLocalClock
        c.ContactRequestLocalState = ContactRequestStateNone
        // We set the remote state, as this was an implicit retraction
        // potentially, for example this could happen if they
        // sent a retraction earier, but we never received it,
        // or one of our paired devices has retracted the contact request
        // but we never synced with them.
        c.ContactRequestRemoteState = ContactRequestStateNone
    }

    // We always trust this
    if remoteClock > c.ContactRequestRemoteClock {
        if remoteState == ContactRequestStateSent {
            response = c.contactRequestReceived(remoteClock, response)
        } else if remoteState == ContactRequestStateNone {
            response = c.contactRequestRetracted(remoteClock, false, response)
        }
    }

    return response
}

func (c *Contact) ContactRequestPropagatedState() *protobuf.ContactRequestPropagatedState {
    return &protobuf.ContactRequestPropagatedState{
        LocalClock:  c.ContactRequestLocalClock,
        LocalState:  uint64(c.ContactRequestLocalState),
        RemoteClock: c.ContactRequestRemoteClock,
        RemoteState: uint64(c.ContactRequestRemoteState),
    }
}