diff --git a/protocol/common/message_sender.go b/protocol/common/message_sender.go index 592f559ec..f018a5cad 100644 --- a/protocol/common/message_sender.go +++ b/protocol/common/message_sender.go @@ -285,7 +285,7 @@ func (s *MessageSender) sendCommunity( // Check if it's a key exchange message. In this case we send it // to all the recipients if rawMessage.CommunityKeyExMsgType != KeyExMsgNone { - keyExMessageSpecs, err := s.protocol.GetKeyExMessageSpecs(rawMessage.CommunityID, s.identity, rawMessage.Recipients, rawMessage.CommunityKeyExMsgType == KeyExMsgRekey) + keyExMessageSpecs, err := s.protocol.GetKeyExMessageSpecs(rawMessage.HashRatchetGroupID, s.identity, rawMessage.Recipients, rawMessage.CommunityKeyExMsgType == KeyExMsgRekey) if err != nil { return nil, err } @@ -307,7 +307,7 @@ func (s *MessageSender) sendCommunity( // If it's a chat message, we send it on the community chat topic if ShouldCommunityMessageBeEncrypted(rawMessage.MessageType) { - messageSpec, err := s.protocol.BuildHashRatchetMessage(rawMessage.CommunityID, wrappedMessage) + messageSpec, err := s.protocol.BuildHashRatchetMessage(rawMessage.HashRatchetGroupID, wrappedMessage) if err != nil { return nil, err } diff --git a/protocol/common/raw_message.go b/protocol/common/raw_message.go index fe5fd7fad..4fcf05d22 100644 --- a/protocol/common/raw_message.go +++ b/protocol/common/raw_message.go @@ -35,4 +35,5 @@ type RawMessage struct { CommunityKeyExMsgType CommKeyExMsgType Ephemeral bool BeforeDispatch func(*RawMessage) error + HashRatchetGroupID []byte } diff --git a/protocol/communities/community.go b/protocol/communities/community.go index 0ead9ad84..236eb7d0b 100644 --- a/protocol/communities/community.go +++ b/protocol/communities/community.go @@ -681,6 +681,20 @@ func (o *Community) GetMember(pk *ecdsa.PublicKey) *protobuf.CommunityMember { return o.getMember(pk) } +func (o *Community) getChatMember(pk *ecdsa.PublicKey, chatID string) *protobuf.CommunityMember { + if !o.hasMember(pk) { + return nil + } + + chat, ok := o.config.CommunityDescription.Chats[chatID] + if !ok { + return nil + } + + key := common.PubkeyToHex(pk) + return chat.Members[key] +} + func (o *Community) hasMember(pk *ecdsa.PublicKey) bool { member := o.getMember(pk) @@ -736,18 +750,7 @@ func (o *Community) IsMemberInChat(pk *ecdsa.PublicKey, chatID string) bool { o.mutex.Lock() defer o.mutex.Unlock() - if !o.hasMember(pk) { - return false - } - - chat, ok := o.config.CommunityDescription.Chats[chatID] - if !ok { - return false - } - - key := common.PubkeyToHex(pk) - _, ok = chat.Members[key] - return ok + return o.getChatMember(pk, chatID) != nil } func (o *Community) RemoveUserFromChat(pk *ecdsa.PublicKey, chatID string) (*protobuf.CommunityDescription, error) { @@ -905,17 +908,27 @@ func (o *Community) AddRoleToMember(pk *ecdsa.PublicKey, role protobuf.Community } updated := false - member := o.getMember(pk) - if member != nil { + addRole := func(member *protobuf.CommunityMember) { roles := make(map[protobuf.CommunityMember_Roles]bool) roles[role] = true if !o.hasMemberPermission(member, roles) { member.Roles = append(member.Roles, role) - o.config.CommunityDescription.Members[common.PubkeyToHex(pk)] = member updated = true } } + member := o.getMember(pk) + if member != nil { + addRole(member) + } + + for channelID := range o.chats() { + chatMember := o.getChatMember(pk, channelID) + if chatMember != nil { + addRole(member) + } + } + if updated { o.increaseClock() } @@ -931,8 +944,7 @@ func (o *Community) RemoveRoleFromMember(pk *ecdsa.PublicKey, role protobuf.Comm } updated := false - member := o.getMember(pk) - if member != nil { + removeRole := func(member *protobuf.CommunityMember) { roles := make(map[protobuf.CommunityMember_Roles]bool) roles[role] = true if o.hasMemberPermission(member, roles) { @@ -943,11 +955,22 @@ func (o *Community) RemoveRoleFromMember(pk *ecdsa.PublicKey, role protobuf.Comm } } member.Roles = newRoles - o.config.CommunityDescription.Members[common.PubkeyToHex(pk)] = member updated = true } } + member := o.getMember(pk) + if member != nil { + removeRole(member) + } + + for channelID := range o.chats() { + chatMember := o.getChatMember(pk, channelID) + if chatMember != nil { + removeRole(member) + } + } + if updated { o.increaseClock() } @@ -1328,16 +1351,20 @@ func (o *Community) ToBytes() ([]byte, error) { } func (o *Community) Chats() map[string]*protobuf.CommunityChat { - response := make(map[string]*protobuf.CommunityChat) - // Why are we checking here for nil, it should be the responsibility of the caller if o == nil { - return response + return make(map[string]*protobuf.CommunityChat) } o.mutex.Lock() defer o.mutex.Unlock() + return o.chats() +} + +func (o *Community) chats() map[string]*protobuf.CommunityChat { + response := make(map[string]*protobuf.CommunityChat) + if o.config != nil && o.config.CommunityDescription != nil { for k, v := range o.config.CommunityDescription.Chats { response[k] = v @@ -1391,7 +1418,21 @@ func (o *Community) TokenPermissions() map[string]*protobuf.CommunityTokenPermis } func (o *Community) HasTokenPermissions() bool { - return len(o.config.CommunityDescription.TokenPermissions) > 0 + return o.config.CommunityDescription.TokenPermissions != nil && len(o.config.CommunityDescription.TokenPermissions) > 0 +} + +func (o *Community) ChannelHasTokenPermissions(chatID string) bool { + if !o.HasTokenPermissions() { + return false + } + + for _, tokenPermission := range o.TokenPermissions() { + if includes(tokenPermission.ChatIds, chatID) { + return true + } + } + + return false } func TokenPermissionsByType(permissions map[string]*protobuf.CommunityTokenPermission, permissionType protobuf.CommunityTokenPermission_Type) []*protobuf.CommunityTokenPermission { @@ -2064,6 +2105,8 @@ func (o *Community) createChat(chatID string, chat *protobuf.CommunityChat) erro } } + chat.Members = o.config.CommunityDescription.Members + o.config.CommunityDescription.Chats[chatID] = chat return nil diff --git a/protocol/communities/community_encryption_key_action.go b/protocol/communities/community_encryption_key_action.go index 2472a4ca7..1796d01f5 100644 --- a/protocol/communities/community_encryption_key_action.go +++ b/protocol/communities/community_encryption_key_action.go @@ -63,7 +63,9 @@ func evaluateCommunityLevelEncryptionKeyAction(origin, modified *Community, chan func evaluateChannelLevelEncryptionKeyActions(origin, modified *Community, changes *CommunityChanges) *map[string]EncryptionKeyAction { result := make(map[string]EncryptionKeyAction) - for chatID := range modified.config.CommunityDescription.Chats { + for channelID := range modified.config.CommunityDescription.Chats { + chatID := modified.IDString() + channelID + originChannelViewOnlyPermissions := origin.ChannelTokenPermissionsByType(chatID, protobuf.CommunityTokenPermission_CAN_VIEW_CHANNEL) originChannelViewAndPostPermissions := origin.ChannelTokenPermissionsByType(chatID, protobuf.CommunityTokenPermission_CAN_VIEW_AND_POST_CHANNEL) originChannelPermissions := append(originChannelViewOnlyPermissions, originChannelViewAndPostPermissions...) @@ -75,13 +77,13 @@ func evaluateChannelLevelEncryptionKeyActions(origin, modified *Community, chang membersAdded := make(map[string]*protobuf.CommunityMember) membersRemoved := make(map[string]*protobuf.CommunityMember) - chatChanges, ok := changes.ChatsModified[chatID] + chatChanges, ok := changes.ChatsModified[channelID] if ok { membersAdded = chatChanges.MembersAdded membersRemoved = chatChanges.MembersRemoved } - result[chatID] = *evaluateEncryptionKeyAction(originChannelPermissions, modifiedChannelPermissions, modified.config.CommunityDescription.Members, membersAdded, membersRemoved) + result[channelID] = *evaluateEncryptionKeyAction(originChannelPermissions, modifiedChannelPermissions, modified.config.CommunityDescription.Chats[channelID].Members, membersAdded, membersRemoved) } return &result diff --git a/protocol/communities/community_encryption_key_action_test.go b/protocol/communities/community_encryption_key_action_test.go index bfde84261..9d2053286 100644 --- a/protocol/communities/community_encryption_key_action_test.go +++ b/protocol/communities/community_encryption_key_action_test.go @@ -8,6 +8,7 @@ import ( "github.com/stretchr/testify/suite" "github.com/status-im/status-go/eth-node/crypto" + "github.com/status-im/status-go/eth-node/types" "github.com/status-im/status-go/protocol/common" "github.com/status-im/status-go/protocol/protobuf" ) @@ -622,7 +623,8 @@ func (s *CommunityEncryptionKeyActionSuite) TestCommunityLevelKeyActions_Permiss } func (s *CommunityEncryptionKeyActionSuite) TestChannelLevelKeyActions() { - chatID := "0x1234" + channelID := "1234" + chatID := types.EncodeHex(crypto.CompressPubkey(&s.identity.PublicKey)) + channelID testCases := []struct { name string originPermissions []*protobuf.CommunityTokenPermission @@ -736,7 +738,7 @@ func (s *CommunityEncryptionKeyActionSuite) TestChannelLevelKeyActions() { for _, tc := range testCases { s.Run(tc.name, func() { origin := createTestCommunity(s.identity) - _, err := origin.CreateChat(chatID, &protobuf.CommunityChat{ + _, err := origin.CreateChat(channelID, &protobuf.CommunityChat{ Members: map[string]*protobuf.CommunityMember{}, Permissions: &protobuf.CommunityPermissions{Access: protobuf.CommunityPermissions_NO_MEMBERSHIP}, Identity: &protobuf.ChatIdentity{}, @@ -752,7 +754,7 @@ func (s *CommunityEncryptionKeyActionSuite) TestChannelLevelKeyActions() { for _, member := range tc.originMembers { _, err := origin.AddMember(member, []protobuf.CommunityMember_Roles{}) s.Require().NoError(err) - _, err = origin.AddMemberToChat(chatID, member, []protobuf.CommunityMember_Roles{}) + _, err = origin.AddMemberToChat(channelID, member, []protobuf.CommunityMember_Roles{}) s.Require().NoError(err) } @@ -763,12 +765,12 @@ func (s *CommunityEncryptionKeyActionSuite) TestChannelLevelKeyActions() { for _, member := range tc.modifiedMembers { _, err := modified.AddMember(member, []protobuf.CommunityMember_Roles{}) s.Require().NoError(err) - _, err = modified.AddMemberToChat(chatID, member, []protobuf.CommunityMember_Roles{}) + _, err = modified.AddMemberToChat(channelID, member, []protobuf.CommunityMember_Roles{}) s.Require().NoError(err) } actions := EvaluateCommunityEncryptionKeyActions(origin, modified) - channelAction, ok := actions.ChannelKeysActions[chatID] + channelAction, ok := actions.ChannelKeysActions[channelID] s.Require().True(ok) s.Require().Equal(tc.expectedAction.ActionType, channelAction.ActionType) s.Require().Len(tc.expectedAction.Members, len(channelAction.Members)) @@ -783,8 +785,10 @@ func (s *CommunityEncryptionKeyActionSuite) TestChannelLevelKeyActions() { func (s *CommunityEncryptionKeyActionSuite) TestNilOrigin() { newCommunity := createTestCommunity(s.identity) - chatID := "0x1234" - _, err := newCommunity.CreateChat(chatID, &protobuf.CommunityChat{ + channelID := "0x1234" + chatID := types.EncodeHex(crypto.CompressPubkey(&s.identity.PublicKey)) + channelID + + _, err := newCommunity.CreateChat(channelID, &protobuf.CommunityChat{ Members: map[string]*protobuf.CommunityMember{}, Permissions: &protobuf.CommunityPermissions{Access: protobuf.CommunityPermissions_NO_MEMBERSHIP}, Identity: &protobuf.ChatIdentity{}, @@ -813,6 +817,6 @@ func (s *CommunityEncryptionKeyActionSuite) TestNilOrigin() { actions := EvaluateCommunityEncryptionKeyActions(nil, newCommunity) s.Require().Equal(actions.CommunityKeyAction.ActionType, EncryptionKeyAdd) s.Require().Len(actions.ChannelKeysActions, 1) - s.Require().NotNil(actions.ChannelKeysActions[chatID]) - s.Require().Equal(actions.ChannelKeysActions[chatID].ActionType, EncryptionKeyAdd) + s.Require().NotNil(actions.ChannelKeysActions[channelID]) + s.Require().Equal(actions.ChannelKeysActions[channelID].ActionType, EncryptionKeyAdd) } diff --git a/protocol/communities/manager.go b/protocol/communities/manager.go index 56c3c3f4d..70e3ef53a 100644 --- a/protocol/communities/manager.go +++ b/protocol/communities/manager.go @@ -62,29 +62,29 @@ var ( ) type Manager struct { - persistence *Persistence - encryptor *encryption.Protocol - ensSubscription chan []*ens.VerificationRecord - subscriptions []chan *Subscription - ensVerifier *ens.Verifier - identity *ecdsa.PrivateKey - accountsManager account.Manager - tokenManager TokenManager - collectiblesManager CollectiblesManager - logger *zap.Logger - stdoutLogger *zap.Logger - transport *transport.Transport - quit chan struct{} - torrentConfig *params.TorrentConfig - torrentClient *torrent.Client - walletConfig *params.WalletConfig - historyArchiveTasksWaitGroup sync.WaitGroup - historyArchiveTasks sync.Map // stores `chan struct{}` - periodicMemberPermissionsTasks sync.Map // stores `chan struct{}` - torrentTasks map[string]metainfo.Hash - historyArchiveDownloadTasks map[string]*HistoryArchiveDownloadTask - stopped bool - RekeyInterval time.Duration + persistence *Persistence + encryptor *encryption.Protocol + ensSubscription chan []*ens.VerificationRecord + subscriptions []chan *Subscription + ensVerifier *ens.Verifier + identity *ecdsa.PrivateKey + accountsManager account.Manager + tokenManager TokenManager + collectiblesManager CollectiblesManager + logger *zap.Logger + stdoutLogger *zap.Logger + transport *transport.Transport + quit chan struct{} + torrentConfig *params.TorrentConfig + torrentClient *torrent.Client + walletConfig *params.WalletConfig + historyArchiveTasksWaitGroup sync.WaitGroup + historyArchiveTasks sync.Map // stores `chan struct{}` + periodicMembersReevaluationTasks sync.Map // stores `chan struct{}` + torrentTasks map[string]metainfo.Hash + historyArchiveDownloadTasks map[string]*HistoryArchiveDownloadTask + stopped bool + RekeyInterval time.Duration } type HistoryArchiveDownloadTask struct { @@ -615,16 +615,12 @@ func (m *Manager) EditCommunityTokenPermission(request *requests.EditCommunityTo return community, changes, nil } -func (m *Manager) CheckMemberPermissions(community *Community, removeAdmins bool) error { +func (m *Manager) ReevaluateMembers(community *Community, removeAdmins bool) error { becomeMemberPermissions := community.TokenPermissionsByType(protobuf.CommunityTokenPermission_BECOME_MEMBER) becomeAdminPermissions := community.TokenPermissionsByType(protobuf.CommunityTokenPermission_BECOME_ADMIN) - adminPermissions := len(becomeAdminPermissions) > 0 - memberPermissions := len(becomeMemberPermissions) > 0 - - if !adminPermissions && !memberPermissions && !removeAdmins { - return nil - } + hasMemberPermissions := len(becomeMemberPermissions) > 0 + hasAdminPermissions := len(becomeAdminPermissions) > 0 for memberKey, member := range community.Members() { memberPubKey, err := common.HexToPubkey(memberKey) @@ -641,7 +637,7 @@ func (m *Manager) CheckMemberPermissions(community *Community, removeAdmins bool // Check if user was not treated as an admin without wallet in open community // or user treated as a member without wallet in closed community - if (!memberHasWallet && isAdmin) || (memberPermissions && !memberHasWallet) { + if !memberHasWallet && (hasMemberPermissions || isAdmin) { _, err = community.RemoveUserFromOrg(memberPubKey) if err != nil { return err @@ -653,7 +649,7 @@ func (m *Manager) CheckMemberPermissions(community *Community, removeAdmins bool // Check if user is still an admin or can become an admin and do update of member role removeAdminRole := false - if adminPermissions { + if hasAdminPermissions { permissionResponse, err := m.checkPermissionToJoin(becomeAdminPermissions, accountsAndChainIDs, true) if err != nil { return err @@ -678,21 +674,67 @@ func (m *Manager) CheckMemberPermissions(community *Community, removeAdmins bool isAdmin = false } - // Skip further validation if user has admin permissions or we do not have member permissions - if isAdmin || !memberPermissions { + if isAdmin { + // Make sure admin is added to every channel + for channelID := range community.Chats() { + if !community.IsMemberInChat(memberPubKey, channelID) { + _, err = community.AddMemberToChat(channelID, memberPubKey, []protobuf.CommunityMember_Roles{protobuf.CommunityMember_ROLE_ADMIN}) + if err != nil { + return err + } + } + } + // Skip further validation if user has admin permissions continue } - permissionResponse, err := m.checkPermissionToJoin(becomeMemberPermissions, accountsAndChainIDs, true) - if err != nil { - return err - } - - if !permissionResponse.Satisfied { - _, err = community.RemoveUserFromOrg(memberPubKey) + if hasMemberPermissions { + permissionResponse, err := m.checkPermissionToJoin(becomeMemberPermissions, accountsAndChainIDs, true) if err != nil { return err } + + if !permissionResponse.Satisfied { + _, err = community.RemoveUserFromOrg(memberPubKey) + if err != nil { + return err + } + // Skip channels validation if user has been removed + continue + } + } + + // Validate channel permissions + for channelID := range community.Chats() { + chatID := community.IDString() + channelID + + viewOnlyPermissions := community.ChannelTokenPermissionsByType(chatID, protobuf.CommunityTokenPermission_CAN_VIEW_CHANNEL) + viewAndPostPermissions := community.ChannelTokenPermissionsByType(chatID, protobuf.CommunityTokenPermission_CAN_VIEW_AND_POST_CHANNEL) + + if len(viewOnlyPermissions) == 0 && len(viewAndPostPermissions) == 0 { + continue + } + + response, err := m.checkChannelPermissions(viewOnlyPermissions, viewAndPostPermissions, accountsAndChainIDs, true) + if err != nil { + return err + } + + isMemberAlreadyInChannel := community.IsMemberInChat(memberPubKey, channelID) + + if response.ViewOnlyPermissions.Satisfied || response.ViewAndPostPermissions.Satisfied { + if !isMemberAlreadyInChannel { + _, err := community.AddMemberToChat(channelID, memberPubKey, []protobuf.CommunityMember_Roles{}) + if err != nil { + return err + } + } + } else if isMemberAlreadyInChannel { + _, err := community.RemoveUserFromChat(memberPubKey, channelID) + if err != nil { + return err + } + } } } @@ -704,21 +746,13 @@ func (m *Manager) CheckMemberPermissions(community *Community, removeAdmins bool return nil } -func (m *Manager) CheckIfStopCheckingPermissionsPeriodically(community *Community) { - if cancel, exists := m.periodicMemberPermissionsTasks.Load(community.IDString()); exists && - len(community.TokenPermissions()) == 0 { - close(cancel.(chan struct{})) // Need to cast to the chan - } -} - -func (m *Manager) CheckMemberPermissionsPeriodically(communityID types.HexBytes) { - - if _, exists := m.periodicMemberPermissionsTasks.Load(communityID.String()); exists { +func (m *Manager) ReevaluateMembersPeriodically(communityID types.HexBytes) { + if _, exists := m.periodicMembersReevaluationTasks.Load(communityID.String()); exists { return } cancel := make(chan struct{}) - m.periodicMemberPermissionsTasks.Store(communityID.String(), cancel) + m.periodicMembersReevaluationTasks.Store(communityID.String(), cancel) ticker := time.NewTicker(memberPermissionsCheckInterval) defer ticker.Stop() @@ -729,15 +763,15 @@ func (m *Manager) CheckMemberPermissionsPeriodically(communityID types.HexBytes) community, err := m.GetByID(communityID) if err != nil { m.logger.Debug("can't validate member permissions, community was not found", zap.Error(err)) - m.periodicMemberPermissionsTasks.Delete(communityID.String()) + m.periodicMembersReevaluationTasks.Delete(communityID.String()) } - err = m.CheckMemberPermissions(community, true) + err = m.ReevaluateMembers(community, true) if err != nil { m.logger.Debug("failed to check member permissions", zap.Error(err)) } case <-cancel: - m.periodicMemberPermissionsTasks.Delete(communityID.String()) + m.periodicMembersReevaluationTasks.Delete(communityID.String()) return } } @@ -1525,6 +1559,32 @@ func (m *Manager) accountsSatisfyPermissionsToJoin(community *Community, account return true, false, nil } +func (m *Manager) accountsSatisfyPermissionsToJoinChannels(community *Community, accounts []*protobuf.RevealedAccount) (map[string]*protobuf.CommunityChat, error) { + result := make(map[string]*protobuf.CommunityChat) + + accountsAndChainIDs := revealedAccountsToAccountsAndChainIDsCombination(accounts) + + for channelID, channel := range community.config.CommunityDescription.Chats { + channelViewOnlyPermissions := community.ChannelTokenPermissionsByType(community.IDString()+channelID, protobuf.CommunityTokenPermission_CAN_VIEW_CHANNEL) + channelViewAndPostPermissions := community.ChannelTokenPermissionsByType(community.IDString()+channelID, protobuf.CommunityTokenPermission_CAN_VIEW_AND_POST_CHANNEL) + channelPermissions := append(channelViewOnlyPermissions, channelViewAndPostPermissions...) + + if len(channelPermissions) > 0 { + permissionResponse, err := m.checkPermissions(channelPermissions, accountsAndChainIDs, true) + if err != nil { + return nil, err + } + if permissionResponse.Satisfied { + result[channelID] = channel + } + } else { + result[channelID] = channel + } + } + + return result, nil +} + func (m *Manager) AcceptRequestToJoin(request *requests.AcceptRequestToJoinCommunity) (*Community, error) { dbRequest, err := m.persistence.GetRequestToJoin(request.ID) if err != nil { @@ -1565,6 +1625,18 @@ func (m *Manager) AcceptRequestToJoin(request *requests.AcceptRequestToJoinCommu return nil, err } + channels, err := m.accountsSatisfyPermissionsToJoinChannels(community, revealedAccounts) + if err != nil { + return nil, err + } + + for channelID := range channels { + _, err = community.AddMemberToChat(channelID, pk, memberRoles) + if err != nil { + return nil, err + } + } + if err := m.markRequestToJoin(pk, community); err != nil { return nil, err } @@ -2798,8 +2870,17 @@ func (m *Manager) IsEncrypted(communityID string) (bool, error) { } return community.Encrypted(), nil - } + +func (m *Manager) IsChannelEncrypted(communityID string, chatID string) (bool, error) { + community, err := m.GetByIDString(communityID) + if err != nil { + return false, err + } + + return community.ChannelHasTokenPermissions(chatID), nil +} + func (m *Manager) ShouldHandleSyncCommunity(community *protobuf.SyncCommunity) (bool, error) { return m.persistence.ShouldHandleSyncCommunity(community) } diff --git a/protocol/communities_key_distributor.go b/protocol/communities_key_distributor.go index 34e58413a..689c71b39 100644 --- a/protocol/communities_key_distributor.go +++ b/protocol/communities_key_distributor.go @@ -30,9 +30,9 @@ func (ckd *CommunitiesKeyDistributorImpl) Distribute(community *communities.Comm return err } - for chatID := range keyActions.ChannelKeysActions { - keyAction := keyActions.ChannelKeysActions[chatID] - err := ckd.distributeKey(community.ID(), []byte(chatID), &keyAction) + for channelID := range keyActions.ChannelKeysActions { + keyAction := keyActions.ChannelKeysActions[channelID] + err := ckd.distributeKey(community.ID(), []byte(community.IDString()+channelID), &keyAction) if err != nil { return err } @@ -110,6 +110,7 @@ func (ckd *CommunitiesKeyDistributorImpl) sendKeyExchangeMessage(communityID, ha CommunityKeyExMsgType: msgType, Recipients: pubkeys, MessageType: protobuf.ApplicationMetadataMessage_CHAT_MESSAGE, + HashRatchetGroupID: hashRatchetGroupID, } _, err := ckd.sender.SendCommunityMessage(context.Background(), rawMessage) diff --git a/protocol/communities_messenger_token_permissions_test.go b/protocol/communities_messenger_token_permissions_test.go index 89b6b0a16..ec54a498d 100644 --- a/protocol/communities_messenger_token_permissions_test.go +++ b/protocol/communities_messenger_token_permissions_test.go @@ -767,7 +767,157 @@ func (s *MessengerCommunitiesTokenPermissionsSuite) TestBecomeMemberPermissions( s.Require().NoError(err) // send message to channel - msg = s.sendChatMessage(s.owner, chat.ID, "hello on encrypted community") + msg = s.sendChatMessage(s.owner, chat.ID, "hello on encrypted community 2") + + // bob can read the message + response, err = WaitOnMessengerResponse( + s.bob, + func(r *MessengerResponse) bool { + for _, message := range r.messages { + if message.Text == msg.Text { + return true + } + } + return false + }, + "no messages", + ) + s.Require().NoError(err) + s.Require().Len(response.Messages(), 1) + s.Require().Equal(msg.Text, response.Messages()[0].Text) +} + +func (s *MessengerCommunitiesTokenPermissionsSuite) TestViewChannelPermissions() { + community, chat := s.createCommunity() + + // bob joins the community + s.advertiseCommunityTo(community, s.bob) + s.joinCommunity(community, s.bob, bobPassword, []string{}) + + // send message to the channel + msg := s.sendChatMessage(s.owner, chat.ID, "hello on open community") + + // bob can read the message + response, err := WaitOnMessengerResponse( + s.bob, + func(r *MessengerResponse) bool { + for _, message := range r.messages { + if message.Text == msg.Text { + return true + } + } + return false + }, + "no messages", + ) + s.Require().NoError(err) + s.Require().Len(response.Messages(), 1) + s.Require().Equal(msg.Text, response.Messages()[0].Text) + + // setup view channel permission + channelPermissionRequest := requests.CreateCommunityTokenPermission{ + CommunityID: community.ID(), + Type: protobuf.CommunityTokenPermission_CAN_VIEW_CHANNEL, + TokenCriteria: []*protobuf.TokenCriteria{ + &protobuf.TokenCriteria{ + Type: protobuf.CommunityTokenType_ERC20, + ContractAddresses: map[uint64]string{testChainID1: "0x123"}, + Symbol: "TEST", + Amount: "100", + Decimals: uint64(18), + }, + }, + ChatIds: []string{chat.ID}, + } + + waitOnBobToBeKickedFromChannel := s.waitOnCommunitiesEvent(s.owner, func(sub *communities.Subscription) bool { + for channelID, channel := range sub.Community.Chats() { + if channelID == chat.CommunityChatID() && len(channel.Members) == 1 { + return true + } + } + return false + }) + waitOnChannelToBeRekeyedOnceBobIsKicked := s.waitOnKeyDistribution(func(sub *CommunityAndKeyActions) bool { + for channelID, action := range sub.keyActions.ChannelKeysActions { + if channelID == chat.CommunityChatID() && action.ActionType == communities.EncryptionKeyRekey { + return true + } + } + return false + }) + + response, err = s.owner.CreateCommunityTokenPermission(&channelPermissionRequest) + s.Require().NoError(err) + s.Require().Len(response.Communities(), 1) + s.Require().True(s.owner.communitiesManager.IsChannelEncrypted(community.IDString(), chat.ID)) + + err = <-waitOnBobToBeKickedFromChannel + s.Require().NoError(err) + + err = <-waitOnChannelToBeRekeyedOnceBobIsKicked + s.Require().NoError(err) + + // send message to the channel + msg = s.sendChatMessage(s.owner, chat.ID, "hello on closed channel") + + // bob can't read the message + _, err = WaitOnMessengerResponse( + s.bob, + func(r *MessengerResponse) bool { + for _, message := range r.messages { + if message.Text == msg.Text { + return true + } + } + return false + }, + "no messages", + ) + s.Require().Error(err) + s.Require().ErrorContains(err, "no messages") + + // make bob satisfy channel criteria + s.makeAddressSatisfyTheCriteria(testChainID1, bobAddress, channelPermissionRequest.TokenCriteria[0]) + + waitOnChannelKeyToBeDistributedToBob := s.waitOnKeyDistribution(func(sub *CommunityAndKeyActions) bool { + for channelID, action := range sub.keyActions.ChannelKeysActions { + if channelID == chat.CommunityChatID() && action.ActionType == communities.EncryptionKeySendToMembers { + for memberPubKey := range action.Members { + if memberPubKey == common.PubkeyToHex(&s.bob.identity.PublicKey) { + return true + } + + } + } + } + return false + }) + + // force owner to reevaluate channel members + // in production it will happen automatically, by periodic check + community, err = s.owner.communitiesManager.GetByID(community.ID()) + s.Require().NoError(err) + err = s.owner.communitiesManager.ReevaluateMembers(community, true) + s.Require().NoError(err) + + err = <-waitOnChannelKeyToBeDistributedToBob + s.Require().NoError(err) + + // ensure key is delivered to bob before message is sent + // FIXME: this step shouldn't be necessary as we store hash ratchet messages + // for later, to decrypt them when the key arrives. + // for some reason, without it, the test is flaky + _, _ = WaitOnMessengerResponse( + s.bob, + func(r *MessengerResponse) bool { + return false + }, + "", + ) + + // send message to the channel + msg = s.sendChatMessage(s.owner, chat.ID, "hello on closed channel 2") // bob can read the message response, err = WaitOnMessengerResponse( diff --git a/protocol/messenger.go b/protocol/messenger.go index 9ddb1c018..e355bd707 100644 --- a/protocol/messenger.go +++ b/protocol/messenger.go @@ -822,7 +822,7 @@ func (m *Messenger) Start() (*MessengerResponse, error) { for _, c := range adminCommunities { if c.Joined() && c.HasTokenPermissions() { - go m.communitiesManager.CheckMemberPermissionsPeriodically(c.ID()) + go m.communitiesManager.ReevaluateMembersPeriodically(c.ID()) } } } @@ -2063,21 +2063,36 @@ func (m *Messenger) dispatchMessage(ctx context.Context, rawMessage common.RawMe } logger.Debug("sending community chat message", zap.String("chatName", chat.Name)) - isEncrypted, err := m.communitiesManager.IsEncrypted(chat.CommunityID) + isCommunityEncrypted, err := m.communitiesManager.IsEncrypted(chat.CommunityID) if err != nil { return rawMessage, err } + isChannelEncrypted, err := m.communitiesManager.IsChannelEncrypted(chat.CommunityID, chat.ID) + if err != nil { + return rawMessage, err + } + isEncrypted := isCommunityEncrypted || isChannelEncrypted if !isEncrypted { id, err = m.sender.SendPublic(ctx, chat.ID, rawMessage) + if err != nil { + return rawMessage, err + } } else { rawMessage.CommunityID, err = types.DecodeHex(chat.CommunityID) - - if err == nil { - id, err = m.sender.SendCommunityMessage(ctx, rawMessage) + if err != nil { + return rawMessage, err + } + + if isChannelEncrypted { + rawMessage.HashRatchetGroupID = []byte(chat.ID) + } else { + rawMessage.HashRatchetGroupID = rawMessage.CommunityID + } + + id, err = m.sender.SendCommunityMessage(ctx, rawMessage) + if err != nil { + return rawMessage, err } - } - if err != nil { - return rawMessage, err } case ChatTypePrivateGroupChat: logger.Debug("sending group message", zap.String("chatName", chat.Name)) diff --git a/protocol/messenger_communities.go b/protocol/messenger_communities.go index 69c91b9bb..199d753c1 100644 --- a/protocol/messenger_communities.go +++ b/protocol/messenger_communities.go @@ -1697,12 +1697,12 @@ func (m *Messenger) CreateCommunityTokenPermission(request *requests.CreateCommu if community.IsControlNode() { // check existing member permission once, then check periodically go func() { - err := m.communitiesManager.CheckMemberPermissions(community, true) + err := m.communitiesManager.ReevaluateMembers(community, true) if err != nil { m.logger.Debug("failed to check member permissions", zap.Error(err)) } - m.communitiesManager.CheckMemberPermissionsPeriodically(community.ID()) + m.communitiesManager.ReevaluateMembersPeriodically(community.ID()) }() } @@ -1729,7 +1729,7 @@ func (m *Messenger) EditCommunityTokenPermission(request *requests.EditCommunity // We do this in a separate routine to not block this function if community.IsControlNode() { go func() { - err := m.communitiesManager.CheckMemberPermissions(community, true) + err := m.communitiesManager.ReevaluateMembers(community, true) if err != nil { m.logger.Debug("failed to check member permissions", zap.Error(err)) } @@ -1760,14 +1760,10 @@ func (m *Messenger) DeleteCommunityTokenPermission(request *requests.DeleteCommu becomeAdminPermissions := community.TokenPermissionsByType(protobuf.CommunityTokenPermission_BECOME_ADMIN) // Make sure that we remove admins roles if we remove admin permissions - err = m.communitiesManager.CheckMemberPermissions(community, len(becomeAdminPermissions) == 0) + err = m.communitiesManager.ReevaluateMembers(community, len(becomeAdminPermissions) == 0) if err != nil { m.logger.Debug("failed to check member permissions", zap.Error(err)) } - - // Check if there's still permissions we need to track, - // if not we can stop checking token criteria on-chain - m.communitiesManager.CheckIfStopCheckingPermissionsPeriodically(community) }() }