status-im/status-go

View on GitHub
protocol/messenger_backup.go

Summary

Maintainability
A
0 mins
Test Coverage
B
80%
package protocol

import (
    "context"
    "time"

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

    gocommon "github.com/status-im/status-go/common"
    "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/communities"
    "github.com/status-im/status-go/protocol/protobuf"
)

const (
    BackupContactsPerBatch = 20
)

// backupTickerInterval is how often we should check for backups
var backupTickerInterval = 120 * time.Second

// backupIntervalSeconds is the amount of seconds we should allow between
// backups
var backupIntervalSeconds uint64 = 28800

type CommunitySet struct {
    Joined  []*communities.Community
    Deleted []*communities.Community
}

func (m *Messenger) backupEnabled() (bool, error) {
    return m.settings.BackupEnabled()
}

func (m *Messenger) lastBackup() (uint64, error) {
    return m.settings.LastBackup()
}

func (m *Messenger) startBackupLoop() {
    ticker := time.NewTicker(backupTickerInterval)
    go func() {
        defer gocommon.LogOnPanic()
        for {
            select {
            case <-ticker.C:
                if !m.Online() {
                    continue
                }

                enabled, err := m.backupEnabled()
                if err != nil {
                    m.logger.Error("failed to fetch backup enabled")
                    continue
                }
                if !enabled {
                    m.logger.Debug("backup not enabled, skipping")
                    continue
                }

                lastBackup, err := m.lastBackup()
                if err != nil {
                    m.logger.Error("failed to fetch last backup time")
                    continue
                }

                now := time.Now().Unix()
                if uint64(now) <= backupIntervalSeconds+lastBackup {
                    m.logger.Debug("not backing up")
                    continue
                }
                m.logger.Debug("backing up data")

                ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
                defer cancel()
                _, err = m.BackupData(ctx)
                if err != nil {
                    m.logger.Error("failed to backup data", zap.Error(err))
                }
            case <-m.quit:
                ticker.Stop()
                return
            }
        }
    }()
}

func (m *Messenger) BackupData(ctx context.Context) (uint64, error) {
    clock, chat := m.getLastClockWithRelatedChat()
    contactsToBackup := m.backupContacts(ctx)
    communitiesToBackup, err := m.backupCommunities(ctx, clock)
    if err != nil {
        return 0, err
    }
    chatsToBackup := m.backupChats(ctx, clock)
    if err != nil {
        return 0, err
    }
    profileToBackup, err := m.backupProfile(ctx, clock)
    if err != nil {
        return 0, err
    }
    _, settings, errors := m.prepareSyncSettingsMessages(clock, true)
    if len(errors) != 0 {
        // return just the first error, the others have been logged
        return 0, errors[0]
    }

    keypairsToBackup, err := m.backupKeypairs()
    if err != nil {
        return 0, err
    }

    woAccountsToBackup, err := m.backupWatchOnlyAccounts()
    if err != nil {
        return 0, err
    }

    backupDetailsOnly := func() *protobuf.Backup {
        return &protobuf.Backup{
            Clock: clock,
            ChatsDetails: &protobuf.FetchingBackedUpDataDetails{
                DataNumber:  uint32(0),
                TotalNumber: uint32(len(chatsToBackup)),
            },
            ContactsDetails: &protobuf.FetchingBackedUpDataDetails{
                DataNumber:  uint32(0),
                TotalNumber: uint32(len(contactsToBackup)),
            },
            CommunitiesDetails: &protobuf.FetchingBackedUpDataDetails{
                DataNumber:  uint32(0),
                TotalNumber: uint32(len(communitiesToBackup)),
            },
            ProfileDetails: &protobuf.FetchingBackedUpDataDetails{
                DataNumber:  uint32(0),
                TotalNumber: uint32(len(profileToBackup)),
            },
            SettingsDetails: &protobuf.FetchingBackedUpDataDetails{
                DataNumber:  uint32(0),
                TotalNumber: uint32(len(settings)),
            },
            KeypairDetails: &protobuf.FetchingBackedUpDataDetails{
                DataNumber:  uint32(0),
                TotalNumber: uint32(len(keypairsToBackup)),
            },
            WatchOnlyAccountDetails: &protobuf.FetchingBackedUpDataDetails{
                DataNumber:  uint32(0),
                TotalNumber: uint32(len(woAccountsToBackup)),
            },
        }
    }

    // Update contacts messages encode and dispatch
    for i, d := range contactsToBackup {
        pb := backupDetailsOnly()
        pb.ContactsDetails.DataNumber = uint32(i + 1)
        pb.Contacts = d.Contacts
        err = m.encodeAndDispatchBackupMessage(ctx, pb, chat.ID)
        if err != nil {
            return 0, err
        }
    }

    // Update communities messages encode and dispatch
    for i, d := range communitiesToBackup {
        pb := backupDetailsOnly()
        pb.CommunitiesDetails.DataNumber = uint32(i + 1)
        pb.Communities = d.Communities
        err = m.encodeAndDispatchBackupMessage(ctx, pb, chat.ID)
        if err != nil {
            return 0, err
        }
    }

    // Update profile messages encode and dispatch
    for i, d := range profileToBackup {
        pb := backupDetailsOnly()
        pb.ProfileDetails.DataNumber = uint32(i + 1)
        pb.Profile = d.Profile
        err = m.encodeAndDispatchBackupMessage(ctx, pb, chat.ID)
        if err != nil {
            return 0, err
        }
    }

    // Update chats encode and dispatch
    for i, d := range chatsToBackup {
        pb := backupDetailsOnly()
        pb.ChatsDetails.DataNumber = uint32(i + 1)
        pb.Chats = d.Chats
        err = m.encodeAndDispatchBackupMessage(ctx, pb, chat.ID)
        if err != nil {
            return 0, err
        }
    }

    // Update settings messages encode and dispatch
    for i, d := range settings {
        pb := backupDetailsOnly()
        pb.SettingsDetails.DataNumber = uint32(i + 1)
        pb.Setting = d
        err = m.encodeAndDispatchBackupMessage(ctx, pb, chat.ID)
        if err != nil {
            return 0, err
        }
    }

    // Update keypairs messages encode and dispatch
    for i, d := range keypairsToBackup {
        pb := backupDetailsOnly()
        pb.KeypairDetails.DataNumber = uint32(i + 1)
        pb.Keypair = d.Keypair
        err = m.encodeAndDispatchBackupMessage(ctx, pb, chat.ID)
        if err != nil {
            return 0, err
        }
    }

    // Update watch only messages encode and dispatch
    for i, d := range woAccountsToBackup {
        pb := backupDetailsOnly()
        pb.WatchOnlyAccountDetails.DataNumber = uint32(i + 1)
        pb.WatchOnlyAccount = d.WatchOnlyAccount
        err = m.encodeAndDispatchBackupMessage(ctx, pb, chat.ID)
        if err != nil {
            return 0, err
        }
    }

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

    clockInSeconds := clock / 1000
    err = m.settings.SetLastBackup(clockInSeconds)
    if err != nil {
        return 0, err
    }
    if m.config.messengerSignalsHandler != nil {
        m.config.messengerSignalsHandler.BackupPerformed(clockInSeconds)
    }

    return clockInSeconds, nil
}

func (m *Messenger) encodeAndDispatchBackupMessage(ctx context.Context, message *protobuf.Backup, chatID string) error {
    encodedMessage, err := proto.Marshal(message)
    if err != nil {
        return err
    }

    _, err = m.dispatchMessage(ctx, common.RawMessage{
        LocalChatID:         chatID,
        Payload:             encodedMessage,
        SkipEncryptionLayer: true,
        SendOnPersonalTopic: true,
        MessageType:         protobuf.ApplicationMetadataMessage_BACKUP,
    })

    return err
}

func (m *Messenger) backupContacts(ctx context.Context) []*protobuf.Backup {
    var contacts []*protobuf.SyncInstallationContactV2
    m.allContacts.Range(func(contactID string, contact *Contact) (shouldContinue bool) {
        syncContact := m.buildSyncContactMessage(contact)
        if syncContact != nil {
            contacts = append(contacts, syncContact)
        }
        return true
    })

    var backupMessages []*protobuf.Backup
    for i := 0; i < len(contacts); i += BackupContactsPerBatch {
        j := i + BackupContactsPerBatch
        if j > len(contacts) {
            j = len(contacts)
        }

        contactsToAdd := contacts[i:j]

        backupMessage := &protobuf.Backup{
            Contacts: contactsToAdd,
        }
        backupMessages = append(backupMessages, backupMessage)
    }

    return backupMessages
}

func (m *Messenger) retrieveAllCommunities() (*CommunitySet, error) {
    joinedCs, err := m.communitiesManager.JoinedAndPendingCommunitiesWithRequests()
    if err != nil {
        return nil, err
    }

    deletedCs, err := m.communitiesManager.DeletedCommunities()
    if err != nil {
        return nil, err
    }

    return &CommunitySet{
        Joined:  joinedCs,
        Deleted: deletedCs,
    }, nil
}

func (m *Messenger) backupCommunities(ctx context.Context, clock uint64) ([]*protobuf.Backup, error) {
    communitySet, err := m.retrieveAllCommunities()
    if err != nil {
        return nil, err
    }

    var backupMessages []*protobuf.Backup
    combinedCs := append(communitySet.Joined, communitySet.Deleted...)

    for _, c := range combinedCs {
        _, beingImported := m.importingCommunities[c.IDString()]
        if !beingImported {
            backupMessage, err := m.backupCommunity(c, clock)
            if err != nil {
                return nil, err
            }

            backupMessages = append(backupMessages, backupMessage)
        }
    }

    return backupMessages, nil
}

func (m *Messenger) backupCommunity(community *communities.Community, clock uint64) (*protobuf.Backup, error) {
    communityId := community.ID()
    settings, err := m.communitiesManager.GetCommunitySettingsByID(communityId)
    if err != nil {
        return nil, err
    }

    syncControlNode, err := m.communitiesManager.GetSyncControlNode(communityId)
    if err != nil {
        return nil, err
    }

    syncMessage, err := community.ToSyncInstallationCommunityProtobuf(clock, settings, syncControlNode)
    if err != nil {
        return nil, err
    }

    err = m.propagateSyncInstallationCommunityWithHRKeys(syncMessage, community)
    if err != nil {
        return nil, err
    }

    return &protobuf.Backup{
        Communities: []*protobuf.SyncInstallationCommunity{syncMessage},
    }, nil
}

func (m *Messenger) backupChats(ctx context.Context, clock uint64) []*protobuf.Backup {
    var oneToOneAndGroupChats []*protobuf.SyncChat
    m.allChats.Range(func(chatID string, chat *Chat) bool {
        if !chat.OneToOne() && !chat.PrivateGroupChat() {
            return true
        }
        syncChat := protobuf.SyncChat{
            Clock:    clock,
            Id:       chatID,
            ChatType: uint32(chat.ChatType),
            Active:   chat.Active,
        }
        chatMuteTill, _ := time.Parse(time.RFC3339, chat.MuteTill.Format(time.RFC3339))
        if chat.Muted && chatMuteTill.Equal(time.Time{}) {
            // Only set Muted if it is "permanently" muted
            syncChat.Muted = true
        }
        if chat.PrivateGroupChat() {
            syncChat.Name = chat.Name // The Name is only useful in the case of a group chat

            syncChat.MembershipUpdateEvents = make([]*protobuf.MembershipUpdateEvents, len(chat.MembershipUpdates))
            for i, membershipUpdate := range chat.MembershipUpdates {
                syncChat.MembershipUpdateEvents[i] = &protobuf.MembershipUpdateEvents{
                    Clock:      membershipUpdate.ClockValue,
                    Type:       uint32(membershipUpdate.Type),
                    Members:    membershipUpdate.Members,
                    Name:       membershipUpdate.Name,
                    Signature:  membershipUpdate.Signature,
                    ChatId:     membershipUpdate.ChatID,
                    From:       membershipUpdate.From,
                    RawPayload: membershipUpdate.RawPayload,
                    Color:      membershipUpdate.Color,
                }
            }
        }
        oneToOneAndGroupChats = append(oneToOneAndGroupChats, &syncChat)
        return true
    })

    var backupMessages []*protobuf.Backup
    backupMessage := &protobuf.Backup{
        Chats: oneToOneAndGroupChats,
    }
    backupMessages = append(backupMessages, backupMessage)
    return backupMessages
}

func (m *Messenger) buildSyncContactMessage(contact *Contact) *protobuf.SyncInstallationContactV2 {
    var ensName string
    if contact.ENSVerified {
        ensName = contact.EnsName
    }

    var customizationColor uint32
    if len(contact.CustomizationColor) != 0 {
        customizationColor = multiaccountscommon.ColorToIDFallbackToBlue(contact.CustomizationColor)
    }

    oneToOneChat, ok := m.allChats.Load(contact.ID)
    muted := false
    if ok {
        muted = oneToOneChat.Muted
    }

    return &protobuf.SyncInstallationContactV2{
        LastUpdatedLocally:        contact.LastUpdatedLocally,
        LastUpdated:               contact.LastUpdated,
        Id:                        contact.ID,
        DisplayName:               contact.DisplayName,
        EnsName:                   ensName,
        CustomizationColor:        customizationColor,
        LocalNickname:             contact.LocalNickname,
        Added:                     contact.added(),
        Blocked:                   contact.Blocked,
        Muted:                     muted,
        HasAddedUs:                contact.hasAddedUs(),
        Removed:                   contact.Removed,
        ContactRequestLocalState:  int64(contact.ContactRequestLocalState),
        ContactRequestRemoteState: int64(contact.ContactRequestRemoteState),
        ContactRequestRemoteClock: int64(contact.ContactRequestRemoteClock),
        ContactRequestLocalClock:  int64(contact.ContactRequestLocalClock),
        VerificationStatus:        int64(contact.VerificationStatus),
        TrustStatus:               int64(contact.TrustStatus),
    }
}

func (m *Messenger) backupProfile(ctx context.Context, clock uint64) ([]*protobuf.Backup, error) {
    displayName, err := m.settings.DisplayName()
    if err != nil {
        return nil, err
    }

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

    if m.account == nil {
        return nil, nil
    }

    keyUID := m.account.KeyUID
    images, err := m.multiAccounts.GetIdentityImages(keyUID)
    if err != nil {
        return nil, err
    }

    pictureProtos := make([]*protobuf.SyncProfilePicture, len(images))
    for i, image := range images {
        p := &protobuf.SyncProfilePicture{}
        p.Name = image.Name
        p.Payload = image.Payload
        p.Width = uint32(image.Width)
        p.Height = uint32(image.Height)
        p.FileSize = uint32(image.FileSize)
        p.ResizeTarget = uint32(image.ResizeTarget)
        if image.Clock == 0 {
            p.Clock = clock
        } else {
            p.Clock = image.Clock
        }
        pictureProtos[i] = p
    }

    ensUsernameDetails, err := m.getEnsUsernameDetails()
    if err != nil {
        return nil, err
    }
    ensUsernameDetailProtos := make([]*protobuf.SyncEnsUsernameDetail, len(ensUsernameDetails))
    for i, ensUsernameDetail := range ensUsernameDetails {
        ensUsernameDetailProtos[i] = &protobuf.SyncEnsUsernameDetail{
            Username: ensUsernameDetail.Username,
            Clock:    ensUsernameDetail.Clock,
            Removed:  ensUsernameDetail.Removed,
            ChainId:  ensUsernameDetail.ChainID,
        }
    }

    profileShowcasePreferences, err := m.GetProfileShowcasePreferences()
    if err != nil {
        return nil, err
    }

    backupMessage := &protobuf.Backup{
        Profile: &protobuf.BackedUpProfile{
            KeyUid:                     keyUID,
            DisplayName:                displayName,
            Pictures:                   pictureProtos,
            DisplayNameClock:           displayNameClock,
            EnsUsernameDetails:         ensUsernameDetailProtos,
            ProfileShowcasePreferences: ToProfileShowcasePreferencesProto(profileShowcasePreferences),
        },
    }

    backupMessages := []*protobuf.Backup{backupMessage}

    return backupMessages, nil
}

func (m *Messenger) backupKeypairs() ([]*protobuf.Backup, error) {
    keypairs, err := m.settings.GetAllKeypairs()
    if err != nil {
        return nil, err
    }

    var backupMessages []*protobuf.Backup
    for _, kp := range keypairs {

        kp.SyncedFrom = accounts.SyncedFromBackup
        keypair, err := m.prepareSyncKeypairMessage(kp)
        if err != nil {
            return nil, err
        }

        backupMessage := &protobuf.Backup{
            Keypair: keypair,
        }

        backupMessages = append(backupMessages, backupMessage)
    }

    return backupMessages, nil
}

func (m *Messenger) backupWatchOnlyAccounts() ([]*protobuf.Backup, error) {
    accounts, err := m.settings.GetAllWatchOnlyAccounts()
    if err != nil {
        return nil, err
    }

    var backupMessages []*protobuf.Backup
    for _, acc := range accounts {

        backupMessage := &protobuf.Backup{}
        backupMessage.WatchOnlyAccount = m.prepareSyncAccountMessage(acc)

        backupMessages = append(backupMessages, backupMessage)
    }

    return backupMessages, nil
}