diff --git a/protocol/identity/profile_showcase.go b/protocol/identity/profile_showcase.go index 932fb20ec..195686acc 100644 --- a/protocol/identity/profile_showcase.go +++ b/protocol/identity/profile_showcase.go @@ -86,8 +86,10 @@ type ProfileShowcasePreferences struct { // Profile showcase for a contact type ProfileShowcaseCommunity struct { - CommunityID string `json:"communityId"` - Order int `json:"order"` + CommunityID string `json:"communityId"` + Order int `json:"order"` + MembershipStatus ProfileShowcaseMembershipStatus `json:"membershipStatus"` + Grant []byte `json:"grant,omitempty"` } type ProfileShowcaseAccount struct { diff --git a/protocol/messenger_handler.go b/protocol/messenger_handler.go index b759d20a1..f72339347 100644 --- a/protocol/messenger_handler.go +++ b/protocol/messenger_handler.go @@ -3057,6 +3057,7 @@ func (m *Messenger) HandleChatIdentity(state *ReceivedMessageState, ci *protobuf if err != nil { return err } + state.Response.AddUpdatedProfileShowcaseContactID(contact.ID) } } diff --git a/protocol/messenger_profile_showcase.go b/protocol/messenger_profile_showcase.go index da1761542..9c88cd934 100644 --- a/protocol/messenger_profile_showcase.go +++ b/protocol/messenger_profile_showcase.go @@ -1,6 +1,7 @@ package protocol import ( + "bytes" "context" "crypto/ecdsa" crand "crypto/rand" @@ -15,6 +16,7 @@ import ( eth_common "github.com/ethereum/go-ethereum/common" + "github.com/status-im/status-go/eth-node/crypto" "github.com/status-im/status-go/eth-node/types" "github.com/status-im/status-go/multiaccounts/accounts" "github.com/status-im/status-go/protocol/common" @@ -107,6 +109,60 @@ func (m *Messenger) validateCollectiblesOwnership(accounts []*identity.ProfileSh return nil } +func (m *Messenger) validateCommunityMembershipEntry( + entry *identity.ProfileShowcaseCommunity, + community *communities.Community, + contactPubKey *ecdsa.PublicKey) (identity.ProfileShowcaseMembershipStatus, error) { + if community == nil { + return identity.ProfileShowcaseMembershipStatusUnproven, nil + } + + if community.Encrypted() { + grant, err := community.VerifyGrantSignature(entry.Grant) + if err != nil { + m.logger.Warn("failed to verify grant signature ", zap.Error(err)) + return identity.ProfileShowcaseMembershipStatusNotAMember, nil + } + + if grant != nil && bytes.Equal(grant.MemberId, crypto.CompressPubkey(contactPubKey)) { + return identity.ProfileShowcaseMembershipStatusProvenMember, nil + } + // Show as not a member if membership can't be proven + return identity.ProfileShowcaseMembershipStatusNotAMember, nil + } + + if community.HasMember(contactPubKey) { + return identity.ProfileShowcaseMembershipStatusProvenMember, nil + } + + return identity.ProfileShowcaseMembershipStatusNotAMember, nil +} + +func (m *Messenger) validateCommunitiesMembership(communities []*identity.ProfileShowcaseCommunity, contactPubKey *ecdsa.PublicKey) ([]*identity.ProfileShowcaseCommunity, error) { + validatedCommunities := []*identity.ProfileShowcaseCommunity{} + + for _, communityEntry := range communities { + community, err := m.FetchCommunity(&FetchCommunityRequest{ + CommunityKey: communityEntry.CommunityID, + Shard: nil, + TryDatabase: true, + WaitForResponse: true, + }) + if err != nil { + m.logger.Warn("failed to fetch community for profile entry ", zap.Error(err)) + continue + } + + communityEntry.MembershipStatus, err = m.validateCommunityMembershipEntry(communityEntry, community, contactPubKey) + if err != nil { + m.logger.Warn("failed to verify grant signature ", zap.Error(err)) + } + validatedCommunities = append(validatedCommunities, communityEntry) + } + + return validatedCommunities, nil +} + func (m *Messenger) toProfileShowcaseCommunityProto(preferences []*identity.ProfileShowcaseCommunityPreference, visibility identity.ProfileShowcaseVisibility) []*protobuf.ProfileShowcaseCommunity { entries := []*protobuf.ProfileShowcaseCommunity{} for _, preference := range preferences { @@ -230,11 +286,13 @@ func (m *Messenger) toProfileShowcaseSocialLinksProto(preferences []*identity.Pr } func (m *Messenger) fromProfileShowcaseCommunityProto(senderPubKey *ecdsa.PublicKey, messages []*protobuf.ProfileShowcaseCommunity) []*identity.ProfileShowcaseCommunity { + // NOTE: no requests to the network are allowed to be made here, called in the receiver thread entries := []*identity.ProfileShowcaseCommunity{} for _, message := range messages { entry := &identity.ProfileShowcaseCommunity{ CommunityID: message.CommunityId, Order: int(message.Order), + Grant: message.Grant, } entries = append(entries, entry) @@ -243,6 +301,7 @@ func (m *Messenger) fromProfileShowcaseCommunityProto(senderPubKey *ecdsa.Public } func (m *Messenger) fromProfileShowcaseAccountProto(messages []*protobuf.ProfileShowcaseAccount) []*identity.ProfileShowcaseAccount { + // NOTE: no requests to the network are allowed to be made here, called in the receiver thread entries := []*identity.ProfileShowcaseAccount{} for _, entry := range messages { entries = append(entries, &identity.ProfileShowcaseAccount{ @@ -257,6 +316,7 @@ func (m *Messenger) fromProfileShowcaseAccountProto(messages []*protobuf.Profile } func (m *Messenger) fromProfileShowcaseCollectibleProto(messages []*protobuf.ProfileShowcaseCollectible) []*identity.ProfileShowcaseCollectible { + // NOTE: no requests to the network are allowed to be made here, called in the receiver thread entries := []*identity.ProfileShowcaseCollectible{} for _, message := range messages { entry := &identity.ProfileShowcaseCollectible{ @@ -271,6 +331,7 @@ func (m *Messenger) fromProfileShowcaseCollectibleProto(messages []*protobuf.Pro } func (m *Messenger) fromProfileShowcaseVerifiedTokenProto(messages []*protobuf.ProfileShowcaseVerifiedToken) []*identity.ProfileShowcaseVerifiedToken { + // NOTE: no requests to the network are allowed to be made here, called in the receiver thread entries := []*identity.ProfileShowcaseVerifiedToken{} for _, entry := range messages { entries = append(entries, &identity.ProfileShowcaseVerifiedToken{ @@ -282,6 +343,7 @@ func (m *Messenger) fromProfileShowcaseVerifiedTokenProto(messages []*protobuf.P } func (m *Messenger) fromProfileShowcaseUnverifiedTokenProto(messages []*protobuf.ProfileShowcaseUnverifiedToken) []*identity.ProfileShowcaseUnverifiedToken { + // NOTE: no requests to the network are allowed to be made here, called in the receiver thread entries := []*identity.ProfileShowcaseUnverifiedToken{} for _, entry := range messages { entries = append(entries, &identity.ProfileShowcaseUnverifiedToken{ @@ -294,6 +356,7 @@ func (m *Messenger) fromProfileShowcaseUnverifiedTokenProto(messages []*protobuf } func (m *Messenger) fromProfileShowcaseSocialLinkProto(messages []*protobuf.ProfileShowcaseSocialLink) []*identity.ProfileShowcaseSocialLink { + // NOTE: no requests to the network are allowed to be made here, called in the receiver thread entries := []*identity.ProfileShowcaseSocialLink{} for _, entry := range messages { entries = append(entries, &identity.ProfileShowcaseSocialLink{ @@ -349,8 +412,29 @@ func (m *Messenger) GetProfileShowcasePreferences() (*identity.ProfileShowcasePr return m.persistence.GetProfileShowcasePreferences() } -func (m *Messenger) GetProfileShowcaseForContact(contactID string) (*identity.ProfileShowcase, error) { - return m.persistence.GetProfileShowcaseForContact(contactID) +func (m *Messenger) GetProfileShowcaseForContact(contactID string, validate bool) (*identity.ProfileShowcase, error) { + profileShowcase, err := m.persistence.GetProfileShowcaseForContact(contactID) + if err != nil { + return nil, err + } + + if !validate { + return profileShowcase, nil + } + + contactPubKey, err := common.HexToPubkey(contactID) + if err != nil { + return nil, err + } + + profileShowcase.Communities, err = m.validateCommunitiesMembership(profileShowcase.Communities, contactPubKey) + if err != nil { + return nil, err + } + + // TODO: validate collectibles & assets ownership, https://github.com/status-im/status-desktop/issues/14129 + + return profileShowcase, nil } func (m *Messenger) GetProfileShowcaseAccountsByAddress(address string) ([]*identity.ProfileShowcaseAccount, error) { @@ -600,7 +684,6 @@ func (m *Messenger) BuildProfileShowcaseFromIdentity(state *ReceivedMessageState return err } - state.Response.AddProfileShowcase(newShowcase) return nil } diff --git a/protocol/messenger_profile_showcase_test.go b/protocol/messenger_profile_showcase_test.go index 76c86b5f8..e11fa46bb 100644 --- a/protocol/messenger_profile_showcase_test.go +++ b/protocol/messenger_profile_showcase_test.go @@ -471,17 +471,23 @@ func (s *TestMessengerProfileShowcase) TestShareShowcasePreferences() { contactID := types.EncodeHex(crypto.FromECDSAPub(&s.m.identity.PublicKey)) // Get summarised profile data for mutual contact - resp, err := WaitOnMessengerResponse( + _, err = WaitOnMessengerResponse( mutualContact, func(r *MessengerResponse) bool { - return len(r.updatedProfileShowcases) > 0 && r.updatedProfileShowcases[contactID] != nil + return r.updatedProfileShowcaseContactIDs[contactID] == true }, "no messages", ) s.Require().NoError(err) - s.Require().Len(resp.updatedProfileShowcases, 1) - profileShowcase := resp.updatedProfileShowcases[contactID] + profileShowcase, err := mutualContact.GetProfileShowcaseForContact(contactID, false) + s.Require().NoError(err) + + s.Require().Len(profileShowcase.Communities, 2) + s.Require().Equal(profileShowcase.Communities[0].CommunityID, request.Communities[0].CommunityID) + s.Require().Equal(profileShowcase.Communities[0].Order, request.Communities[0].Order) + s.Require().Equal(profileShowcase.Communities[1].CommunityID, request.Communities[1].CommunityID) + s.Require().Equal(profileShowcase.Communities[1].Order, request.Communities[1].Order) s.Require().Len(profileShowcase.Accounts, 2) s.Require().Equal(profileShowcase.Accounts[0].Address, request.Accounts[0].Address) @@ -516,18 +522,16 @@ func (s *TestMessengerProfileShowcase) TestShareShowcasePreferences() { s.Require().Equal(profileShowcase.SocialLinks[1].Order, request.SocialLinks[2].Order) // Get summarised profile data for verified contact - resp, err = WaitOnMessengerResponse( + _, err = WaitOnMessengerResponse( verifiedContact, func(r *MessengerResponse) bool { - return len(r.updatedProfileShowcases) > 0 && r.updatedProfileShowcases[contactID] != nil + return r.updatedProfileShowcaseContactIDs[contactID] == true }, "no messages", ) s.Require().NoError(err) - s.Require().Len(resp.updatedProfileShowcases, 1) - // Here let's try synchronous - profileShowcase, err = verifiedContact.GetProfileShowcaseForContact(contactID) + profileShowcase, err = verifiedContact.GetProfileShowcaseForContact(contactID, false) s.Require().NoError(err) s.Require().Len(profileShowcase.Accounts, 2) @@ -613,22 +617,24 @@ func (s *TestMessengerProfileShowcase) TestProfileShowcaseProofOfMembershipUnenc s.Require().NoError(err) contactID := types.EncodeHex(crypto.FromECDSAPub(&alice.identity.PublicKey)) - resp, err := WaitOnMessengerResponse( + _, err = WaitOnMessengerResponse( bob, func(r *MessengerResponse) bool { - return len(r.updatedProfileShowcases) > 0 && r.updatedProfileShowcases[contactID] != nil + return r.updatedProfileShowcaseContactIDs[contactID] == true }, "no messages", ) s.Require().NoError(err) - s.Require().Len(resp.updatedProfileShowcases, 1) - profileShowcase := resp.updatedProfileShowcases[contactID] + profileShowcase, err := bob.GetProfileShowcaseForContact(contactID, true) + s.Require().NoError(err) // Verify community's data s.Require().Len(profileShowcase.Communities, 2) s.Require().Equal(profileShowcase.Communities[0].CommunityID, aliceCommunity.IDString()) + s.Require().Equal(profileShowcase.Communities[0].MembershipStatus, identity.ProfileShowcaseMembershipStatusProvenMember) s.Require().Equal(profileShowcase.Communities[1].CommunityID, bobCommunity.IDString()) + s.Require().Equal(profileShowcase.Communities[1].MembershipStatus, identity.ProfileShowcaseMembershipStatusNotAMember) } func (s *TestMessengerProfileShowcase) TestProfileShowcaseProofOfMembershipEncryptedCommunity() { @@ -679,20 +685,22 @@ func (s *TestMessengerProfileShowcase) TestProfileShowcaseProofOfMembershipEncry s.Require().NoError(err) contactID := types.EncodeHex(crypto.FromECDSAPub(&alice.identity.PublicKey)) - resp, err := WaitOnMessengerResponse( + _, err = WaitOnMessengerResponse( bob, func(r *MessengerResponse) bool { - return len(r.updatedProfileShowcases) > 0 && r.updatedProfileShowcases[contactID] != nil + return r.updatedProfileShowcaseContactIDs[contactID] == true }, "no messages", ) s.Require().NoError(err) - s.Require().Len(resp.updatedProfileShowcases, 1) - profileShowcase := resp.updatedProfileShowcases[contactID] + profileShowcase, err := bob.GetProfileShowcaseForContact(contactID, true) + s.Require().NoError(err) // Verify community's data s.Require().Len(profileShowcase.Communities, 2) s.Require().Equal(profileShowcase.Communities[0].CommunityID, aliceCommunity.IDString()) + s.Require().Equal(profileShowcase.Communities[0].MembershipStatus, identity.ProfileShowcaseMembershipStatusProvenMember) s.Require().Equal(profileShowcase.Communities[1].CommunityID, bobCommunity.IDString()) + s.Require().Equal(profileShowcase.Communities[1].MembershipStatus, identity.ProfileShowcaseMembershipStatusNotAMember) } diff --git a/protocol/messenger_response.go b/protocol/messenger_response.go index c63d19767..6daafedbb 100644 --- a/protocol/messenger_response.go +++ b/protocol/messenger_response.go @@ -69,32 +69,32 @@ type MessengerResponse struct { // notifications a list of notifications derived from messenger events // that are useful to notify the user about - notifications map[string]*localnotifications.Notification - requestsToJoinCommunity map[string]*communities.RequestToJoin - chats map[string]*Chat - removedChats map[string]bool - removedMessages map[string]*RemovedMessage - deletedMessages map[string]string - communities map[string]*communities.Community - communitiesSettings map[string]*communities.CommunitySettings - activityCenterNotifications map[string]*ActivityCenterNotification - activityCenterState *ActivityCenterState - messages map[string]*common.Message - pinMessages map[string]*common.PinMessage - discordMessages map[string]*protobuf.DiscordMessage - discordMessageAttachments map[string]*protobuf.DiscordMessageAttachment - discordMessageAuthors map[string]*protobuf.DiscordMessageAuthor - currentStatus *UserStatus - statusUpdates map[string]UserStatus - clearedHistories map[string]*ClearedHistory - verificationRequests map[string]*verification.Request - trustStatus map[string]verification.TrustStatus - emojiReactions map[string]*EmojiReaction - savedAddresses map[string]*wallet.SavedAddress - SocialLinksInfo *identity.SocialLinksInfo - ensUsernameDetails []*ensservice.UsernameDetail - updatedProfileShowcases map[string]*identity.ProfileShowcase - seenAndUnseenMessages map[string]*SeenUnseenMessages + notifications map[string]*localnotifications.Notification + requestsToJoinCommunity map[string]*communities.RequestToJoin + chats map[string]*Chat + removedChats map[string]bool + removedMessages map[string]*RemovedMessage + deletedMessages map[string]string + communities map[string]*communities.Community + communitiesSettings map[string]*communities.CommunitySettings + activityCenterNotifications map[string]*ActivityCenterNotification + activityCenterState *ActivityCenterState + messages map[string]*common.Message + pinMessages map[string]*common.PinMessage + discordMessages map[string]*protobuf.DiscordMessage + discordMessageAttachments map[string]*protobuf.DiscordMessageAttachment + discordMessageAuthors map[string]*protobuf.DiscordMessageAuthor + currentStatus *UserStatus + statusUpdates map[string]UserStatus + clearedHistories map[string]*ClearedHistory + verificationRequests map[string]*verification.Request + trustStatus map[string]verification.TrustStatus + emojiReactions map[string]*EmojiReaction + savedAddresses map[string]*wallet.SavedAddress + SocialLinksInfo *identity.SocialLinksInfo + ensUsernameDetails []*ensservice.UsernameDetail + updatedProfileShowcaseContactIDs map[string]bool + seenAndUnseenMessages map[string]*SeenUnseenMessages } func (r *MessengerResponse) MarshalJSON() ([]byte, error) { @@ -119,31 +119,31 @@ func (r *MessengerResponse) MarshalJSON() ([]byte, error) { TrustStatus map[string]verification.TrustStatus `json:"trustStatus,omitempty"` // Notifications a list of notifications derived from messenger events // that are useful to notify the user about - Notifications []*localnotifications.Notification `json:"notifications"` - Communities []*communities.Community `json:"communities,omitempty"` - CommunitiesSettings []*communities.CommunitySettings `json:"communitiesSettings,omitempty"` - ActivityCenterNotifications []*ActivityCenterNotification `json:"activityCenterNotifications,omitempty"` - ActivityCenterState *ActivityCenterState `json:"activityCenterState,omitempty"` - CurrentStatus *UserStatus `json:"currentStatus,omitempty"` - StatusUpdates []UserStatus `json:"statusUpdates,omitempty"` - Settings []*settings.SyncSettingField `json:"settings,omitempty"` - IdentityImages []images.IdentityImage `json:"identityImages,omitempty"` - CustomizationColor string `json:"customizationColor,omitempty"` - WatchOnlyAccounts []*accounts.Account `json:"watchOnlyAccounts,omitempty"` - Keypairs []*accounts.Keypair `json:"keypairs,omitempty"` - AccountsPositions []*accounts.Account `json:"accountsPositions,omitempty"` - TokenPreferences []walletsettings.TokenPreferences `json:"tokenPreferences,omitempty"` - CollectiblePreferences []walletsettings.CollectiblePreferences `json:"collectiblePreferences,omitempty"` - DiscordCategories []*discord.Category `json:"discordCategories,omitempty"` - DiscordChannels []*discord.Channel `json:"discordChannels,omitempty"` - DiscordOldestMessageTimestamp int `json:"discordOldestMessageTimestamp"` - DiscordMessages []*protobuf.DiscordMessage `json:"discordMessages,omitempty"` - DiscordMessageAttachments []*protobuf.DiscordMessageAttachment `json:"discordMessageAtachments,omitempty"` - SavedAddresses []*wallet.SavedAddress `json:"savedAddresses,omitempty"` - SocialLinksInfo *identity.SocialLinksInfo `json:"socialLinksInfo,omitempty"` - EnsUsernameDetails []*ensservice.UsernameDetail `json:"ensUsernameDetails,omitempty"` - UpdatedProfileShowcases []*identity.ProfileShowcase `json:"updatedProfileShowcases,omitempty"` - SeenAndUnseenMessages []*SeenUnseenMessages `json:"seenAndUnseenMessages,omitempty"` + Notifications []*localnotifications.Notification `json:"notifications"` + Communities []*communities.Community `json:"communities,omitempty"` + CommunitiesSettings []*communities.CommunitySettings `json:"communitiesSettings,omitempty"` + ActivityCenterNotifications []*ActivityCenterNotification `json:"activityCenterNotifications,omitempty"` + ActivityCenterState *ActivityCenterState `json:"activityCenterState,omitempty"` + CurrentStatus *UserStatus `json:"currentStatus,omitempty"` + StatusUpdates []UserStatus `json:"statusUpdates,omitempty"` + Settings []*settings.SyncSettingField `json:"settings,omitempty"` + IdentityImages []images.IdentityImage `json:"identityImages,omitempty"` + CustomizationColor string `json:"customizationColor,omitempty"` + WatchOnlyAccounts []*accounts.Account `json:"watchOnlyAccounts,omitempty"` + Keypairs []*accounts.Keypair `json:"keypairs,omitempty"` + AccountsPositions []*accounts.Account `json:"accountsPositions,omitempty"` + TokenPreferences []walletsettings.TokenPreferences `json:"tokenPreferences,omitempty"` + CollectiblePreferences []walletsettings.CollectiblePreferences `json:"collectiblePreferences,omitempty"` + DiscordCategories []*discord.Category `json:"discordCategories,omitempty"` + DiscordChannels []*discord.Channel `json:"discordChannels,omitempty"` + DiscordOldestMessageTimestamp int `json:"discordOldestMessageTimestamp"` + DiscordMessages []*protobuf.DiscordMessage `json:"discordMessages,omitempty"` + DiscordMessageAttachments []*protobuf.DiscordMessageAttachment `json:"discordMessageAtachments,omitempty"` + SavedAddresses []*wallet.SavedAddress `json:"savedAddresses,omitempty"` + SocialLinksInfo *identity.SocialLinksInfo `json:"socialLinksInfo,omitempty"` + EnsUsernameDetails []*ensservice.UsernameDetail `json:"ensUsernameDetails,omitempty"` + UpdatedProfileShowcaseContactIDs []string `json:"updatedProfileShowcaseContactIDs,omitempty"` + SeenAndUnseenMessages []*SeenUnseenMessages `json:"seenAndUnseenMessages,omitempty"` }{ Contacts: r.Contacts, Installations: r.Installations, @@ -163,29 +163,29 @@ func (r *MessengerResponse) MarshalJSON() ([]byte, error) { TokenPreferences: r.TokenPreferences, CollectiblePreferences: r.CollectiblePreferences, - Messages: r.Messages(), - VerificationRequests: r.VerificationRequests(), - SavedAddresses: r.SavedAddresses(), - Notifications: r.Notifications(), - Chats: r.Chats(), - Communities: r.Communities(), - CommunitiesSettings: r.CommunitiesSettings(), - RemovedChats: r.RemovedChats(), - RemovedMessages: r.RemovedMessages(), - DeletedMessages: r.DeletedMessagesInChats(), - ClearedHistories: r.ClearedHistories(), - ActivityCenterNotifications: r.ActivityCenterNotifications(), - ActivityCenterState: r.ActivityCenterState(), - PinMessages: r.PinMessages(), - EmojiReactions: r.EmojiReactions(), - StatusUpdates: r.StatusUpdates(), - DiscordCategories: r.DiscordCategories, - DiscordChannels: r.DiscordChannels, - DiscordOldestMessageTimestamp: r.DiscordOldestMessageTimestamp, - SocialLinksInfo: r.SocialLinksInfo, - EnsUsernameDetails: r.EnsUsernameDetails(), - UpdatedProfileShowcases: r.GetUpdatedProfileShowcases(), - SeenAndUnseenMessages: r.GetSeenAndUnseenMessages(), + Messages: r.Messages(), + VerificationRequests: r.VerificationRequests(), + SavedAddresses: r.SavedAddresses(), + Notifications: r.Notifications(), + Chats: r.Chats(), + Communities: r.Communities(), + CommunitiesSettings: r.CommunitiesSettings(), + RemovedChats: r.RemovedChats(), + RemovedMessages: r.RemovedMessages(), + DeletedMessages: r.DeletedMessagesInChats(), + ClearedHistories: r.ClearedHistories(), + ActivityCenterNotifications: r.ActivityCenterNotifications(), + ActivityCenterState: r.ActivityCenterState(), + PinMessages: r.PinMessages(), + EmojiReactions: r.EmojiReactions(), + StatusUpdates: r.StatusUpdates(), + DiscordCategories: r.DiscordCategories, + DiscordChannels: r.DiscordChannels, + DiscordOldestMessageTimestamp: r.DiscordOldestMessageTimestamp, + SocialLinksInfo: r.SocialLinksInfo, + EnsUsernameDetails: r.EnsUsernameDetails(), + UpdatedProfileShowcaseContactIDs: r.GetUpdatedProfileShowcaseContactIDs(), + SeenAndUnseenMessages: r.GetSeenAndUnseenMessages(), } responseItem.TrustStatus = r.TrustStatus() @@ -328,7 +328,7 @@ func (r *MessengerResponse) IsEmpty() bool { len(r.verificationRequests)+ len(r.requestsToJoinCommunity)+ len(r.savedAddresses)+ - len(r.updatedProfileShowcases)+ + len(r.updatedProfileShowcaseContactIDs)+ len(r.seenAndUnseenMessages)+ len(r.ensUsernameDetails) == 0 && r.currentStatus == nil && @@ -368,7 +368,7 @@ func (r *MessengerResponse) Merge(response *MessengerResponse) error { r.AddEnsUsernameDetails(response.EnsUsernameDetails()) r.AddRequestsToJoinCommunity(response.RequestsToJoinCommunity()) r.AddBookmarks(response.GetBookmarks()) - r.AddProfileShowcases(response.GetUpdatedProfileShowcases()) + r.AddSeveralUpdatedProfileShowcaseContactIDs(response.GetUpdatedProfileShowcaseContactIDs()) r.AddSeveralSeenAndUnseenMessages(response.GetSeenAndUnseenMessages()) r.CommunityChanges = append(r.CommunityChanges, response.CommunityChanges...) r.BackupHandled = response.BackupHandled @@ -870,26 +870,30 @@ func (r *MessengerResponse) HasDiscordChannel(id string) bool { return false } -func (r *MessengerResponse) AddProfileShowcases(showcases []*identity.ProfileShowcase) { - for _, showcase := range showcases { - r.AddProfileShowcase(showcase) +func (r *MessengerResponse) AddSeveralUpdatedProfileShowcaseContactIDs(contactIDs []string) { + for _, contactID := range contactIDs { + r.AddUpdatedProfileShowcaseContactID(contactID) } } -func (r *MessengerResponse) AddProfileShowcase(showcase *identity.ProfileShowcase) { - if r.updatedProfileShowcases == nil { - r.updatedProfileShowcases = make(map[string]*identity.ProfileShowcase) +func (r *MessengerResponse) AddUpdatedProfileShowcaseContactID(contactID string) { + if r.updatedProfileShowcaseContactIDs == nil { + r.updatedProfileShowcaseContactIDs = make(map[string]bool) } - r.updatedProfileShowcases[showcase.ContactID] = showcase + if _, exists := r.updatedProfileShowcaseContactIDs[contactID]; exists { + return + } + + r.updatedProfileShowcaseContactIDs[contactID] = true } -func (r *MessengerResponse) GetUpdatedProfileShowcases() []*identity.ProfileShowcase { - var showcases []*identity.ProfileShowcase - for _, showcase := range r.updatedProfileShowcases { - showcases = append(showcases, showcase) +func (r *MessengerResponse) GetUpdatedProfileShowcaseContactIDs() []string { + var contactIDs []string + for contactID := range r.updatedProfileShowcaseContactIDs { + contactIDs = append(contactIDs, contactID) } - return showcases + return contactIDs } func (r *MessengerResponse) AddSeveralSeenAndUnseenMessages(messages []*SeenUnseenMessages) { diff --git a/protocol/messenger_testing_utils.go b/protocol/messenger_testing_utils.go index dca4ca37a..b79775160 100644 --- a/protocol/messenger_testing_utils.go +++ b/protocol/messenger_testing_utils.go @@ -417,7 +417,16 @@ func RandomBytes(length int) []byte { func DummyProfileShowcasePreferences(withCollectibles bool) *identity.ProfileShowcasePreferences { preferences := &identity.ProfileShowcasePreferences{ - Communities: []*identity.ProfileShowcaseCommunityPreference{}, // empty to avoid fetching + Communities: []*identity.ProfileShowcaseCommunityPreference{ + { + CommunityID: "0x254254546768764565565", + ShowcaseVisibility: identity.ProfileShowcaseVisibilityEveryone, + }, + { + CommunityID: "0x865241434343432412343", + ShowcaseVisibility: identity.ProfileShowcaseVisibilityContacts, + }, + }, Accounts: []*identity.ProfileShowcaseAccountPreference{ { Address: "0x0000000000000000000000000033433445133423", diff --git a/protocol/migrations/migrations.go b/protocol/migrations/migrations.go index a725b7225..5995bfb96 100644 --- a/protocol/migrations/migrations.go +++ b/protocol/migrations/migrations.go @@ -130,6 +130,7 @@ // 1709805967_simplify_profile_showcase_preferences.up.sql (701B) // 1709828431_add_community_description_cache.up.sql (730B) // 1710331283_add_bio_to_contacts.up.sql (42B) +// 1711389881_add_profile_showcase_community_grant.up.sql (86B) // README.md (554B) // doc.go (870B) @@ -2799,6 +2800,26 @@ func _1710331283_add_bio_to_contactsUpSql() (*asset, error) { return a, nil } +var __1711389881_add_profile_showcase_community_grantUpSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x04\xc0\x41\x0e\x02\x21\x0c\x05\xd0\xbd\xa7\xf8\xf7\x70\x55\x04\x57\x75\x26\x31\xb0\x26\xa4\x41\x25\x71\xa8\xa1\x35\x5e\xdf\x47\x9c\xd3\x1d\x99\x02\x27\x7c\x96\x3e\xc6\xbb\x57\x7b\xe9\x4f\x9a\xf5\x2a\x7a\x1c\xdf\x39\x7c\x74\xab\xa2\xd3\x9b\xb8\x81\x62\xc4\x65\xe7\x72\xdb\xf0\x5c\x6d\x3a\x02\xef\x01\x31\x5d\xa9\x70\xc6\x56\x98\xcf\xa7\x7f\x00\x00\x00\xff\xff\x50\x71\xc8\xa0\x56\x00\x00\x00") + +func _1711389881_add_profile_showcase_community_grantUpSqlBytes() ([]byte, error) { + return bindataRead( + __1711389881_add_profile_showcase_community_grantUpSql, + "1711389881_add_profile_showcase_community_grant.up.sql", + ) +} + +func _1711389881_add_profile_showcase_community_grantUpSql() (*asset, error) { + bytes, err := _1711389881_add_profile_showcase_community_grantUpSqlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "1711389881_add_profile_showcase_community_grant.up.sql", size: 86, mode: os.FileMode(0644), modTime: time.Unix(1700000000, 0)} + a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x45, 0x39, 0x37, 0xf3, 0x6a, 0xb, 0xb8, 0x38, 0x8, 0xcb, 0x62, 0xe1, 0x5b, 0xf2, 0xc, 0x6c, 0xf2, 0xa3, 0x5c, 0xeb, 0x22, 0x4f, 0x6c, 0xe1, 0x56, 0xd1, 0xd1, 0x3, 0xf9, 0x6d, 0x5e, 0x69}} + return a, nil +} + var _readmeMd = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x54\x91\xc1\xce\xd3\x30\x10\x84\xef\x7e\x8a\x91\x7a\x01\xa9\x2a\x8f\xc0\x0d\x71\x82\x03\x48\x1c\xc9\x36\x9e\x36\x96\x1c\x6f\xf0\xae\x93\xe6\xed\x91\xa3\xc2\xdf\xff\x66\xed\xd8\x33\xdf\x78\x4f\xa7\x13\xbe\xea\x06\x57\x6c\x35\x39\x31\xa7\x7b\x15\x4f\x5a\xec\x73\x08\xbf\x08\x2d\x79\x7f\x4a\x43\x5b\x86\x17\xfd\x8c\x21\xea\x56\x5e\x47\x90\x4a\x14\x75\x48\xde\x64\x37\x2c\x6a\x96\xae\x99\x48\x05\xf6\x27\x77\x13\xad\x08\xae\x8a\x51\xe7\x25\xf3\xf1\xa9\x9f\xf9\x58\x58\x2c\xad\xbc\xe0\x8b\x56\xf0\x21\x5d\xeb\x4c\x95\xb3\xae\x84\x60\xd4\xdc\xe6\x82\x5d\x1b\x36\x6d\x39\x62\x92\xf5\xb8\x11\xdb\x92\xd3\x28\xce\xe0\x13\xe1\x72\xcd\x3c\x63\xd4\x65\x87\xae\xac\xe8\xc3\x28\x2e\x67\x44\x66\x3a\x21\x25\xa2\x72\xac\x14\x67\xbc\x84\x9f\x53\x32\x8c\x52\x70\x25\x56\xd6\xfd\x8d\x05\x37\xad\x30\x9d\x9f\xa6\x86\x0f\xcd\x58\x7f\xcf\x34\x93\x3b\xed\x90\x9f\xa4\x1f\xcf\x30\x85\x4d\x07\x58\xaf\x7f\x25\xc4\x9d\xf3\x72\x64\x84\xd0\x7f\xf9\x9b\x3a\x2d\x84\xef\x85\x48\x66\x8d\xd8\x88\x9b\x8c\x8c\x98\x5b\xf6\x74\x14\x4e\x33\x0d\xc9\xe0\x93\x38\xda\x12\xc5\x69\xbd\xe4\xf0\x2e\x7a\x78\x07\x1c\xfe\x13\x9f\x91\x29\x31\x95\x7b\x7f\x62\x59\x37\xb4\xe5\x5e\x25\xfe\x33\xee\xd5\x53\x71\xd6\xda\x3a\xd8\xcb\xde\x2e\xf8\xa1\x90\x55\x53\x0c\xc7\xaa\x0d\xe9\x76\x14\x29\x1c\x7b\x68\xdd\x2f\xe1\x6f\x00\x00\x00\xff\xff\x3c\x0a\xc2\xfe\x2a\x02\x00\x00") func readmeMdBytes() ([]byte, error) { @@ -3060,8 +3081,9 @@ var _bindata = map[string]func() (*asset, error){ "1709805967_simplify_profile_showcase_preferences.up.sql": _1709805967_simplify_profile_showcase_preferencesUpSql, "1709828431_add_community_description_cache.up.sql": _1709828431_add_community_description_cacheUpSql, "1710331283_add_bio_to_contacts.up.sql": _1710331283_add_bio_to_contactsUpSql, - "README.md": readmeMd, - "doc.go": docGo, + "1711389881_add_profile_showcase_community_grant.up.sql": _1711389881_add_profile_showcase_community_grantUpSql, + "README.md": readmeMd, + "doc.go": docGo, } // AssetDebug is true if the assets were built with the debug flag enabled. @@ -3240,8 +3262,9 @@ var _bintree = &bintree{nil, map[string]*bintree{ "1709805967_simplify_profile_showcase_preferences.up.sql": {_1709805967_simplify_profile_showcase_preferencesUpSql, map[string]*bintree{}}, "1709828431_add_community_description_cache.up.sql": {_1709828431_add_community_description_cacheUpSql, map[string]*bintree{}}, "1710331283_add_bio_to_contacts.up.sql": {_1710331283_add_bio_to_contactsUpSql, map[string]*bintree{}}, - "README.md": {readmeMd, map[string]*bintree{}}, - "doc.go": {docGo, map[string]*bintree{}}, + "1711389881_add_profile_showcase_community_grant.up.sql": {_1711389881_add_profile_showcase_community_grantUpSql, map[string]*bintree{}}, + "README.md": {readmeMd, map[string]*bintree{}}, + "doc.go": {docGo, map[string]*bintree{}}, }} // RestoreAsset restores an asset under the given directory. diff --git a/protocol/migrations/sqlite/1711389881_add_profile_showcase_community_grant.up.sql b/protocol/migrations/sqlite/1711389881_add_profile_showcase_community_grant.up.sql new file mode 100644 index 000000000..2822eb8f2 --- /dev/null +++ b/protocol/migrations/sqlite/1711389881_add_profile_showcase_community_grant.up.sql @@ -0,0 +1 @@ +ALTER TABLE profile_showcase_communities_contacts ADD COLUMN grant BLOB DEFAULT NULL; diff --git a/protocol/persistence_profile_showcase.go b/protocol/persistence_profile_showcase.go index 3afbe5e9f..0cd5fc1d4 100644 --- a/protocol/persistence_profile_showcase.go +++ b/protocol/persistence_profile_showcase.go @@ -40,10 +40,9 @@ const selectProfileShowcaseSocialLinkPreferenceQuery = "SELECT url, text, visibi const clearProfileShowcaseSocialLinkPreferencesQuery = "DELETE FROM profile_showcase_social_links_preferences" // #nosec G101 // Profile showcase for a contact - -const upsertContactProfileShowcaseCommunityQuery = "INSERT OR REPLACE INTO profile_showcase_communities_contacts(contact_id, community_id, sort_order) VALUES (?, ?, ?)" // #nosec G101 -const selectContactProfileShowcaseCommunityQuery = "SELECT community_id, sort_order FROM profile_showcase_communities_contacts WHERE contact_id = ?" // #nosec G101 -const removeContactProfileShowcaseCommunityQuery = "DELETE FROM profile_showcase_communities_contacts WHERE contact_id = ?" // #nosec G101 +const upsertContactProfileShowcaseCommunityQuery = "INSERT OR REPLACE INTO profile_showcase_communities_contacts(contact_id, community_id, sort_order, grant) VALUES (?, ?, ?, ?)" // #nosec G101 +const selectContactProfileShowcaseCommunityQuery = "SELECT community_id, sort_order, grant FROM profile_showcase_communities_contacts WHERE contact_id = ?" // #nosec G101 +const removeContactProfileShowcaseCommunityQuery = "DELETE FROM profile_showcase_communities_contacts WHERE contact_id = ?" // #nosec G101 const upsertContactProfileShowcaseAccountQuery = "INSERT OR REPLACE INTO profile_showcase_accounts_contacts(contact_id, address, name, color_id, emoji, sort_order) VALUES (?, ?, ?, ?, ?, ?)" // #nosec G101 const selectContactProfileShowcaseAccountQuery = "SELECT * FROM profile_showcase_accounts_contacts WHERE contact_id = ?" // #nosec G101 @@ -391,6 +390,7 @@ func (db sqlitePersistence) saveProfileShowcaseCommunityContact(tx *sql.Tx, cont contactID, community.CommunityID, community.Order, + community.Grant, ) return err @@ -405,9 +405,11 @@ func (db sqlitePersistence) getProfileShowcaseCommunitiesContact(tx *sql.Tx, con communities := []*identity.ProfileShowcaseCommunity{} for rows.Next() { - community := &identity.ProfileShowcaseCommunity{} + community := &identity.ProfileShowcaseCommunity{ + MembershipStatus: identity.ProfileShowcaseMembershipStatusUnproven, + } - err := rows.Scan(&community.CommunityID, &community.Order) + err := rows.Scan(&community.CommunityID, &community.Order, &community.Grant) if err != nil { return nil, err } diff --git a/services/ext/api.go b/services/ext/api.go index 01232e7c8..3fb7a6aaa 100644 --- a/services/ext/api.go +++ b/services/ext/api.go @@ -1697,8 +1697,8 @@ func (api *PublicAPI) GetProfileShowcasePreferences() (*identity.ProfileShowcase } // Get profile showcase for a contact -func (api *PublicAPI) GetProfileShowcaseForContact(contactID string) (*identity.ProfileShowcase, error) { - return api.service.messenger.GetProfileShowcaseForContact(contactID) +func (api *PublicAPI) GetProfileShowcaseForContact(contactID string, validate bool) (*identity.ProfileShowcase, error) { + return api.service.messenger.GetProfileShowcaseForContact(contactID, validate) } // Get profile showcase accounts by address