diff --git a/protocol/communities/manager.go b/protocol/communities/manager.go index 66f705110..d85d51e3e 100644 --- a/protocol/communities/manager.go +++ b/protocol/communities/manager.go @@ -295,6 +295,10 @@ type Subscription struct { DownloadingHistoryArchivesFinishedSignal *signal.DownloadingHistoryArchivesFinishedSignal ImportingHistoryArchiveMessagesSignal *signal.ImportingHistoryArchiveMessagesSignal CommunityAdminEvent *protobuf.CommunityAdminEvent + MemberPermissionsCheckedSignal *MemberPermissionsCheckedSignal +} + +type MemberPermissionsCheckedSignal struct { } type CommunityResponse struct { @@ -667,7 +671,6 @@ func (m *Manager) checkMemberPermissions(community *Community, removeAdmins bool memberPermissions := len(becomeMemberPermissions) > 0 if !adminPermissions && !memberPermissions && !removeAdmins { - m.publish(&Subscription{Community: community}) return nil } @@ -741,7 +744,10 @@ func (m *Manager) checkMemberPermissions(community *Community, removeAdmins bool } } - m.publish(&Subscription{Community: community}) + m.publish(&Subscription{ + Community: community, + MemberPermissionsCheckedSignal: &MemberPermissionsCheckedSignal{}, + }) return nil } diff --git a/protocol/communities_messenger_helpers_test.go b/protocol/communities_messenger_helpers_test.go index f751f23c3..3f22d2381 100644 --- a/protocol/communities_messenger_helpers_test.go +++ b/protocol/communities_messenger_helpers_test.go @@ -203,3 +203,18 @@ func joinCommunity(s *suite.Suite, community *communities.Community, owner *Mess }) s.Require().NoError(err) } + +func sendChatMessage(s *suite.Suite, sender *Messenger, chatID string, text string) *common.Message { + msg := &common.Message{ + ChatMessage: protobuf.ChatMessage{ + ChatId: chatID, + ContentType: protobuf.ChatMessage_TEXT_PLAIN, + Text: text, + }, + } + + _, err := sender.SendChatMessage(context.Background(), msg) + s.Require().NoError(err) + + return msg +} diff --git a/protocol/communities_messenger_token_permissions_test.go b/protocol/communities_messenger_token_permissions_test.go index f8bcb1c1b..d34f8cb91 100644 --- a/protocol/communities_messenger_token_permissions_test.go +++ b/protocol/communities_messenger_token_permissions_test.go @@ -3,6 +3,8 @@ package protocol import ( "bytes" "context" + "errors" + "math/big" "testing" "time" @@ -24,6 +26,8 @@ import ( "github.com/status-im/status-go/waku" ) +const testChainID1 = 1 + const ownerPassword = "123456" const alicePassword = "qwerty" const bobPassword = "bob123" @@ -52,14 +56,20 @@ func (m *AccountManagerMock) Sign(rpcParams account.SignParams, verifiedAccount } type TokenManagerMock struct { + Balances *map[uint64]map[gethcommon.Address]map[gethcommon.Address]*hexutil.Big } func (m *TokenManagerMock) GetAllChainIDs() ([]uint64, error) { - return []uint64{5}, nil + chainIDs := make([]uint64, 0, len(*m.Balances)) + for key := range *m.Balances { + chainIDs = append(chainIDs, key) + } + return chainIDs, nil } func (m *TokenManagerMock) GetBalancesByChain(ctx context.Context, accounts, tokenAddresses []gethcommon.Address, chainIDs []uint64) (map[uint64]map[gethcommon.Address]map[gethcommon.Address]*hexutil.Big, error) { - return nil, nil + time.Sleep(100 * time.Millisecond) // simulate response time + return *m.Balances, nil } func TestMessengerCommunitiesTokenPermissionsSuite(t *testing.T) { @@ -75,6 +85,8 @@ type MessengerCommunitiesTokenPermissionsSuite struct { // a single Waku service should be shared. shh types.Waku logger *zap.Logger + + mockedBalances map[uint64]map[gethcommon.Address]map[gethcommon.Address]*hexutil.Big // chainID, account, token, balance } func (s *MessengerCommunitiesTokenPermissionsSuite) SetupTest() { @@ -95,6 +107,12 @@ func (s *MessengerCommunitiesTokenPermissionsSuite) SetupTest() { s.Require().NoError(err) _, err = s.alice.Start() s.Require().NoError(err) + + s.mockedBalances = make(map[uint64]map[gethcommon.Address]map[gethcommon.Address]*hexutil.Big) + s.mockedBalances[testChainID1] = make(map[gethcommon.Address]map[gethcommon.Address]*hexutil.Big) + s.mockedBalances[testChainID1][gethcommon.HexToAddress(aliceAddress1)] = make(map[gethcommon.Address]*hexutil.Big) + s.mockedBalances[testChainID1][gethcommon.HexToAddress(aliceAddress2)] = make(map[gethcommon.Address]*hexutil.Big) + s.mockedBalances[testChainID1][gethcommon.HexToAddress(bobAddress)] = make(map[gethcommon.Address]*hexutil.Big) } func (s *MessengerCommunitiesTokenPermissionsSuite) TearDownTest() { @@ -111,7 +129,9 @@ func (s *MessengerCommunitiesTokenPermissionsSuite) newMessenger(password string accountsManagerMock.AccountsMap[walletAddress] = types.EncodeHex(crypto.Keccak256([]byte(password))) } - tokenManagerMock := &TokenManagerMock{} + tokenManagerMock := &TokenManagerMock{ + Balances: &s.mockedBalances, + } privateKey, err := crypto.GenerateKey() s.Require().NoError(err) @@ -150,6 +170,56 @@ func (s *MessengerCommunitiesTokenPermissionsSuite) createCommunity() (*communit return createCommunity(&s.Suite, s.owner) } +func (s *MessengerCommunitiesTokenPermissionsSuite) sendChatMessage(sender *Messenger, chatID string, text string) *common.Message { + return sendChatMessage(&s.Suite, sender, chatID, text) +} + +func (s *MessengerCommunitiesTokenPermissionsSuite) makeAddressSatisfyTheCriteria(chainID uint64, address string, criteria *protobuf.TokenCriteria) { + walletAddress := gethcommon.HexToAddress(address) + contractAddress := gethcommon.HexToAddress(criteria.ContractAddresses[chainID]) + balance, ok := new(big.Int).SetString(criteria.Amount, 10) + s.Require().True(ok) + decimalsFactor := new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(criteria.Decimals)), nil) + balance.Mul(balance, decimalsFactor) + + s.mockedBalances[chainID][walletAddress][contractAddress] = (*hexutil.Big)(balance) +} + +func (s *MessengerCommunitiesTokenPermissionsSuite) waitOnCommunitiesEvent(user *Messenger, condition func(*communities.Subscription) bool) <-chan error { + errCh := make(chan error, 1) + + go func() { + defer close(errCh) + + for { + select { + case sub, more := <-user.communitiesManager.Subscribe(): + if !more { + errCh <- errors.New("channel closed when waiting for communities event") + return + } + + if condition(sub) { + return + } + + case <-time.After(500 * time.Millisecond): + errCh <- errors.New("timed out when waiting for communities event") + return + } + } + }() + + return errCh +} + +func (s *MessengerCommunitiesTokenPermissionsSuite) waitOnCommunityEncryption(community *communities.Community) <-chan error { + s.Require().False(community.Encrypted()) + return s.waitOnCommunitiesEvent(s.owner, func(sub *communities.Subscription) bool { + return sub.Community != nil && sub.Community.IDString() == community.IDString() && sub.Community.Encrypted() + }) +} + func (s *MessengerCommunitiesTokenPermissionsSuite) TestCreateTokenPermission() { community, _ := s.createCommunity() @@ -159,7 +229,7 @@ func (s *MessengerCommunitiesTokenPermissionsSuite) TestCreateTokenPermission() TokenCriteria: []*protobuf.TokenCriteria{ &protobuf.TokenCriteria{ Type: protobuf.CommunityTokenType_ERC20, - ContractAddresses: map[uint64]string{uint64(1): "0x123"}, + ContractAddresses: map[uint64]string{uint64(testChainID1): "0x123"}, Symbol: "TEST", Amount: "100", Decimals: uint64(18), @@ -192,7 +262,7 @@ func (s *MessengerCommunitiesTokenPermissionsSuite) TestEditTokenPermission() { TokenCriteria: []*protobuf.TokenCriteria{ &protobuf.TokenCriteria{ Type: protobuf.CommunityTokenType_ERC20, - ContractAddresses: map[uint64]string{uint64(1): "0x123"}, + ContractAddresses: map[uint64]string{testChainID1: "0x123"}, Symbol: "TEST", Amount: "100", Decimals: uint64(18), @@ -246,7 +316,7 @@ func (s *MessengerCommunitiesTokenPermissionsSuite) TestCommunityTokensMetadata( s.Require().Len(tokensMetadata, 0) newToken := &protobuf.CommunityTokenMetadata{ - ContractAddresses: map[uint64]string{3: "0xasd"}, + ContractAddresses: map[uint64]string{testChainID1: "0xasd"}, Description: "desc1", Image: "IMG1", TokenType: protobuf.CommunityTokenType_ERC721, @@ -370,3 +440,106 @@ func (s *MessengerCommunitiesTokenPermissionsSuite) TestJoinedCommunityMembersSe } } } + +func (s *MessengerCommunitiesTokenPermissionsSuite) TestBecomeMemberPermissions() { + 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 become member permission + permissionRequest := requests.CreateCommunityTokenPermission{ + CommunityID: community.ID(), + Type: protobuf.CommunityTokenPermission_BECOME_MEMBER, + TokenCriteria: []*protobuf.TokenCriteria{ + &protobuf.TokenCriteria{ + Type: protobuf.CommunityTokenType_ERC20, + ContractAddresses: map[uint64]string{testChainID1: "0x123"}, + Symbol: "TEST", + Amount: "100", + Decimals: uint64(18), + }, + }, + } + + waitOnCommunityEncryptionErrCh := s.waitOnCommunityEncryption(community) + + response, err = s.owner.CreateCommunityTokenPermission(&permissionRequest) + s.Require().NoError(err) + s.Require().Len(response.Communities(), 1) + + err = <-waitOnCommunityEncryptionErrCh + s.Require().NoError(err) + + // bob should be kicked from the community, + // because he doesn't meet the criteria + community, err = s.owner.communitiesManager.GetByID(community.ID()) + s.Require().NoError(err) + s.Require().Len(community.Members(), 1) + + // send message to channel + msg = s.sendChatMessage(s.owner, chat.ID, "hello on encrypted community") + + // 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 the criteria + s.makeAddressSatisfyTheCriteria(testChainID1, bobAddress, permissionRequest.TokenCriteria[0]) + + // bob re-joins the community + s.joinCommunity(community, s.bob, bobPassword, []string{}) + + // send message to channel + msg = s.sendChatMessage(s.owner, chat.ID, "hello on encrypted 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) +} diff --git a/protocol/messenger_communities.go b/protocol/messenger_communities.go index 10f931e84..815b691aa 100644 --- a/protocol/messenger_communities.go +++ b/protocol/messenger_communities.go @@ -227,6 +227,22 @@ func (m *Messenger) handleCommunitiesSubscription(c chan *communities.Subscripti if err != nil { m.logger.Warn("failed to publish org", zap.Error(err)) } + + for _, invitation := range sub.Invitations { + err := m.publishOrgInvitation(sub.Community, invitation) + if err != nil { + m.logger.Warn("failed to publish org invitation", zap.Error(err)) + } + } + + if sub.MemberPermissionsCheckedSignal != nil { + err := m.UpdateCommunityEncryption(sub.Community) + if err != nil { + m.logger.Warn("failed to update community encryption", zap.Error(err)) + } + } + + m.logger.Debug("published org") } if sub.CommunityAdminEvent != nil { @@ -236,14 +252,6 @@ func (m *Messenger) handleCommunitiesSubscription(c chan *communities.Subscripti } } - for _, invitation := range sub.Invitations { - err := m.publishOrgInvitation(sub.Community, invitation) - if err != nil { - m.logger.Warn("failed to publish org invitation", zap.Error(err)) - } - } - - m.logger.Debug("published org") case <-ticker.C: // If we are not online, we don't even try if !m.online() { @@ -1460,16 +1468,8 @@ func (m *Messenger) CreateCommunityTokenPermission(request *requests.CreateCommu return nil, err } - response, err := m.UpdateCommunityEncryption(community) - if err != nil { - return nil, err - } - - if response == nil { - response = &MessengerResponse{} - response.AddCommunity(community) - } - + response := &MessengerResponse{} + response.AddCommunity(community) response.CommunityChanges = []*communities.CommunityChanges{changes} return response, nil @@ -1485,16 +1485,8 @@ func (m *Messenger) EditCommunityTokenPermission(request *requests.EditCommunity return nil, err } - response, err := m.UpdateCommunityEncryption(community) - if err != nil { - return nil, err - } - - if response == nil { - response = &MessengerResponse{} - response.AddCommunity(community) - } - + response := &MessengerResponse{} + response.AddCommunity(community) response.CommunityChanges = []*communities.CommunityChanges{changes} return response, nil @@ -1510,17 +1502,10 @@ func (m *Messenger) DeleteCommunityTokenPermission(request *requests.DeleteCommu return nil, err } - response, err := m.UpdateCommunityEncryption(community) - if err != nil { - return nil, err - } - - if response == nil { - response = &MessengerResponse{} - response.AddCommunity(community) - } - + response := &MessengerResponse{} + response.AddCommunity(community) response.CommunityChanges = []*communities.CommunityChanges{changes} + return response, nil } @@ -3812,28 +3797,28 @@ func (m *Messenger) UpdateCommunityTokenSupply(chainID int, contractAddress stri // This functionality introduces some race conditions: // - community description is processed by members before the receiving the key exchange messages // - members maybe sending encrypted messages after the community description is updated and a new member joins -func (m *Messenger) UpdateCommunityEncryption(community *communities.Community) (*MessengerResponse, error) { +func (m *Messenger) UpdateCommunityEncryption(community *communities.Community) error { if community == nil { - return nil, errors.New("community is nil") + return errors.New("community is nil") } becomeMemberPermissions := community.TokenPermissionsByType(protobuf.CommunityTokenPermission_BECOME_MEMBER) isEncrypted := len(becomeMemberPermissions) > 0 if community.Encrypted() == isEncrypted { - return nil, nil + return nil } if isEncrypted { // 🪄 The magic that encrypts a community _, err := m.encryptor.GenerateHashRatchetKey(community.ID()) if err != nil { - return nil, err + return err } err = m.SendKeyExchangeMessage(community.ID(), community.GetMemberPubkeys(), common.KeyExMsgReuse) if err != nil { - return nil, err + return err } } @@ -3843,14 +3828,10 @@ func (m *Messenger) UpdateCommunityEncryption(community *communities.Community) community.SetEncrypted(isEncrypted) err := m.communitiesManager.UpdateCommunity(community) if err != nil { - return nil, err + return err } - response := &MessengerResponse{} - response.AddCommunity(community) - - return response, nil - + return nil } func (m *Messenger) CheckPermissionsToJoinCommunity(request *requests.CheckPermissionToJoinCommunity) (*communities.CheckPermissionToJoinResponse, error) {