From bf64f97d5a2bcb1b5fb6b134dda69994e0ddd2bb Mon Sep 17 00:00:00 2001 From: Pascal Precht <445106+0x-r4bbit@users.noreply.github.com> Date: Tue, 13 Jun 2023 14:50:15 +0200 Subject: [PATCH] feat: introduce `CheckAllCommunityChannelsPermissions()` API This API is used to get a permission status of all channels of a given community. Clients can use this API to get the provided information for all community channels with a single RPC call instead of doing one call for each channel separately. --- protocol/communities/manager.go | 35 ++ protocol/communities/manager_test.go | 362 ++++++++++++++++++ protocol/messenger_communities.go | 19 + ...heck_all_community_channels_permissions.go | 23 ++ services/ext/api.go | 4 + 5 files changed, 443 insertions(+) create mode 100644 protocol/requests/check_all_community_channels_permissions.go diff --git a/protocol/communities/manager.go b/protocol/communities/manager.go index 3ac0be328..2d7b0495c 100644 --- a/protocol/communities/manager.go +++ b/protocol/communities/manager.go @@ -2334,6 +2334,41 @@ func (m *Manager) checkChannelPermissions(viewOnlyPermissions []*protobuf.Commun return response, nil } +func (m *Manager) CheckAllChannelsPermissions(communityID types.HexBytes, addresses []gethcommon.Address) (*CheckAllChannelsPermissionsResponse, error) { + + community, err := m.GetByID(communityID) + if err != nil { + return nil, err + } + channels := community.Chats() + + allChainIDs, err := m.tokenManager.GetAllChainIDs() + if err != nil { + return nil, err + } + accountsAndChainIDs := combineAddressesAndChainIDs(addresses, allChainIDs) + + response := &CheckAllChannelsPermissionsResponse{ + Channels: make(map[string]*CheckChannelPermissionsResponse), + } + + for channelID := range channels { + viewOnlyPermissions := community.ChannelTokenPermissionsByType(community.IDString()+channelID, protobuf.CommunityTokenPermission_CAN_VIEW_CHANNEL) + viewAndPostPermissions := community.ChannelTokenPermissionsByType(community.IDString()+channelID, protobuf.CommunityTokenPermission_CAN_VIEW_AND_POST_CHANNEL) + + checkChannelPermissionsResponse, err := m.checkChannelPermissions(viewOnlyPermissions, viewAndPostPermissions, accountsAndChainIDs, false) + if err != nil { + return nil, err + } + response.Channels[community.IDString()+channelID] = checkChannelPermissionsResponse + } + return response, nil +} + +type CheckAllChannelsPermissionsResponse struct { + Channels map[string]*CheckChannelPermissionsResponse `json:"channels"` +} + func (m *Manager) HandleCommunityRequestToJoinResponse(signer *ecdsa.PublicKey, request *protobuf.CommunityRequestToJoinResponse) (*RequestToJoin, error) { pkString := common.PubkeyToHex(&m.identity.PublicKey) diff --git a/protocol/communities/manager_test.go b/protocol/communities/manager_test.go index e94440596..3f5a63c1a 100644 --- a/protocol/communities/manager_test.go +++ b/protocol/communities/manager_test.go @@ -1030,6 +1030,368 @@ func (s *ManagerSuite) TestCheckChannelPermissions_ViewAndPostPermissionsCombina s.Require().False(resp.ViewAndPostPermissions.Satisfied) } +func (s *ManagerSuite) TestCheckAllChannelsPermissions_EmptyPermissions() { + + m, _ := s.setupManagerForTokenPermissions() + + createRequest := &requests.CreateCommunity{ + Name: "channel permission community", + Description: "some description", + Membership: protobuf.CommunityPermissions_NO_MEMBERSHIP, + } + community, err := m.CreateCommunity(createRequest, true) + s.Require().NoError(err) + + // create community chats + chat := &protobuf.CommunityChat{ + Identity: &protobuf.ChatIdentity{ + DisplayName: "chat1", + Description: "description", + }, + Permissions: &protobuf.CommunityPermissions{ + Access: protobuf.CommunityPermissions_NO_MEMBERSHIP, + }, + Members: make(map[string]*protobuf.CommunityMember), + } + + _, changes, err := m.CreateChat(community.ID(), chat, true, "") + s.Require().NoError(err) + + var chatID string + for cid := range changes.ChatsAdded { + chatID = community.IDString() + cid + } + + response, err := m.CheckAllChannelsPermissions(community.ID(), []gethcommon.Address{ + gethcommon.HexToAddress("0xD6b912e09E797D291E8D0eA3D3D17F8000e01c32"), + }) + s.Require().NoError(err) + s.Require().NotNil(response) + + s.Require().Len(response.Channels, 1) + // we expect both, viewOnly and viewAndPost permissions to be satisfied + // as there aren't any permissions on this channel + s.Require().True(response.Channels[chatID].ViewOnlyPermissions.Satisfied) + s.Require().True(response.Channels[chatID].ViewAndPostPermissions.Satisfied) + s.Require().Len(response.Channels[chatID].ViewOnlyPermissions.Permissions, 0) + s.Require().Len(response.Channels[chatID].ViewAndPostPermissions.Permissions, 0) +} + +func (s *ManagerSuite) TestCheckAllChannelsPermissions() { + + m, tm := s.setupManagerForTokenPermissions() + + var chatID1 string + var chatID2 string + + // create community + createRequest := &requests.CreateCommunity{ + Name: "channel permission community", + Description: "some description", + Membership: protobuf.CommunityPermissions_NO_MEMBERSHIP, + } + community, err := m.CreateCommunity(createRequest, true) + s.Require().NoError(err) + + // create first community chat + chat := &protobuf.CommunityChat{ + Identity: &protobuf.ChatIdentity{ + DisplayName: "chat1", + Description: "description", + }, + Permissions: &protobuf.CommunityPermissions{ + Access: protobuf.CommunityPermissions_NO_MEMBERSHIP, + }, + Members: make(map[string]*protobuf.CommunityMember), + } + + _, changes, err := m.CreateChat(community.ID(), chat, true, "") + s.Require().NoError(err) + + for chatID := range changes.ChatsAdded { + chatID1 = community.IDString() + chatID + } + + // create second community chat + chat = &protobuf.CommunityChat{ + Identity: &protobuf.ChatIdentity{ + DisplayName: "chat2", + Description: "description", + }, + Permissions: &protobuf.CommunityPermissions{ + Access: protobuf.CommunityPermissions_NO_MEMBERSHIP, + }, + Members: make(map[string]*protobuf.CommunityMember), + } + + _, changes, err = m.CreateChat(community.ID(), chat, true, "") + s.Require().NoError(err) + + for chatID := range changes.ChatsAdded { + chatID2 = community.IDString() + chatID + } + + var chainID uint64 = 5 + contractAddresses := make(map[uint64]string) + contractAddresses[chainID] = "0x3d6afaa395c31fcd391fe3d562e75fe9e8ec7e6a" + var decimals uint64 = 18 + + accountChainIDsCombination := []*AccountChainIDsCombination{ + &AccountChainIDsCombination{ + Address: gethcommon.HexToAddress("0xD6b912e09E797D291E8D0eA3D3D17F8000e01c32"), + ChainIDs: []uint64{chainID}, + }, + } + + var tokenCriteria = []*protobuf.TokenCriteria{ + &protobuf.TokenCriteria{ + ContractAddresses: contractAddresses, + Symbol: "STT", + Type: protobuf.CommunityTokenType_ERC20, + Name: "Status Test Token", + Amount: "1.000000000000000000", + Decimals: decimals, + }, + } + + // create view only permission + viewOnlyPermission := &requests.CreateCommunityTokenPermission{ + CommunityID: community.ID(), + Type: protobuf.CommunityTokenPermission_CAN_VIEW_CHANNEL, + TokenCriteria: tokenCriteria, + ChatIds: []string{chatID1, chatID2}, + } + + _, changes, err = m.CreateCommunityTokenPermission(viewOnlyPermission) + s.Require().NoError(err) + + var viewOnlyPermissionID string + for permissionID := range changes.TokenPermissionsAdded { + viewOnlyPermissionID = permissionID + } + + response, err := m.CheckAllChannelsPermissions(community.ID(), []gethcommon.Address{ + gethcommon.HexToAddress("0xD6b912e09E797D291E8D0eA3D3D17F8000e01c32"), + }) + s.Require().NoError(err) + s.Require().NotNil(response) + + // we've added to chats to the community, so there should be 2 items + s.Require().Len(response.Channels, 2) + + // viewOnly permissions should not be satisfied because the account doesn't + // have the necessary funds + + // channel1 + s.Require().False(response.Channels[chatID1].ViewOnlyPermissions.Satisfied) + s.Require().Len(response.Channels[chatID1].ViewOnlyPermissions.Permissions, 1) + s.Require().Len(response.Channels[chatID1].ViewOnlyPermissions.Permissions[viewOnlyPermissionID].Criteria, 1) + s.Require().False(response.Channels[chatID1].ViewOnlyPermissions.Permissions[viewOnlyPermissionID].Criteria[0]) + + // channel2 + s.Require().False(response.Channels[chatID2].ViewOnlyPermissions.Satisfied) + s.Require().Len(response.Channels[chatID2].ViewOnlyPermissions.Permissions, 1) + s.Require().Len(response.Channels[chatID2].ViewOnlyPermissions.Permissions[viewOnlyPermissionID].Criteria, 1) + s.Require().False(response.Channels[chatID2].ViewOnlyPermissions.Permissions[viewOnlyPermissionID].Criteria[0]) + + // viewAndPost permissions are flagged as not satisfied either because + // viewOnly permission is not satisfied and there are no viewAndPost permissions + + // channel1 + s.Require().False(response.Channels[chatID1].ViewAndPostPermissions.Satisfied) + s.Require().Len(response.Channels[chatID1].ViewAndPostPermissions.Permissions, 0) + + // channel2 + s.Require().False(response.Channels[chatID2].ViewAndPostPermissions.Satisfied) + s.Require().Len(response.Channels[chatID2].ViewAndPostPermissions.Permissions, 0) + + // now change balance such that viewOnly permission should be satisfied + tm.setResponse(chainID, accountChainIDsCombination[0].Address, gethcommon.HexToAddress(contractAddresses[chainID]), int64(1*math.Pow(10, float64(decimals)))) + + response, err = m.CheckAllChannelsPermissions(community.ID(), []gethcommon.Address{ + gethcommon.HexToAddress("0xD6b912e09E797D291E8D0eA3D3D17F8000e01c32"), + }) + s.Require().NoError(err) + s.Require().NotNil(response) + s.Require().Len(response.Channels, 2) + + // viewOnly permissions should be satisfied for both channels while + // viewAndPost permissions should not be satisfied (as there aren't any) + + // channel1 + s.Require().True(response.Channels[chatID1].ViewOnlyPermissions.Satisfied) + s.Require().Len(response.Channels[chatID1].ViewOnlyPermissions.Permissions, 1) + s.Require().Len(response.Channels[chatID1].ViewOnlyPermissions.Permissions[viewOnlyPermissionID].Criteria, 1) + s.Require().True(response.Channels[chatID1].ViewOnlyPermissions.Permissions[viewOnlyPermissionID].Criteria[0]) + + s.Require().False(response.Channels[chatID1].ViewAndPostPermissions.Satisfied) + s.Require().Len(response.Channels[chatID1].ViewAndPostPermissions.Permissions, 0) + + // channel2 + s.Require().True(response.Channels[chatID2].ViewOnlyPermissions.Satisfied) + s.Require().Len(response.Channels[chatID2].ViewOnlyPermissions.Permissions, 1) + s.Require().Len(response.Channels[chatID2].ViewOnlyPermissions.Permissions[viewOnlyPermissionID].Criteria, 1) + s.Require().True(response.Channels[chatID2].ViewOnlyPermissions.Permissions[viewOnlyPermissionID].Criteria[0]) + + s.Require().False(response.Channels[chatID2].ViewAndPostPermissions.Satisfied) + s.Require().Len(response.Channels[chatID2].ViewAndPostPermissions.Permissions, 0) + + // next, create viewAndPost permission + // create view only permission + viewAndPostPermission := &requests.CreateCommunityTokenPermission{ + CommunityID: community.ID(), + Type: protobuf.CommunityTokenPermission_CAN_VIEW_AND_POST_CHANNEL, + TokenCriteria: tokenCriteria, + ChatIds: []string{chatID1, chatID2}, + } + + _, changes, err = m.CreateCommunityTokenPermission(viewAndPostPermission) + s.Require().NoError(err) + + var viewAndPostPermissionID string + for permissionID := range changes.TokenPermissionsAdded { + viewAndPostPermissionID = permissionID + } + + // now change balance such that viewAndPost permission is not satisfied + tm.setResponse(chainID, accountChainIDsCombination[0].Address, gethcommon.HexToAddress(contractAddresses[chainID]), 0) + + response, err = m.CheckAllChannelsPermissions(community.ID(), []gethcommon.Address{ + gethcommon.HexToAddress("0xD6b912e09E797D291E8D0eA3D3D17F8000e01c32"), + }) + s.Require().NoError(err) + s.Require().NotNil(response) + s.Require().Len(response.Channels, 2) + + // Both, viewOnly and viewAndPost permissions exist on channel1 and channel2 + // but shouldn't be satisfied + + // channel1 + s.Require().False(response.Channels[chatID1].ViewOnlyPermissions.Satisfied) + s.Require().Len(response.Channels[chatID1].ViewOnlyPermissions.Permissions, 1) + s.Require().Len(response.Channels[chatID1].ViewOnlyPermissions.Permissions[viewOnlyPermissionID].Criteria, 1) + s.Require().False(response.Channels[chatID1].ViewOnlyPermissions.Permissions[viewOnlyPermissionID].Criteria[0]) + + s.Require().False(response.Channels[chatID1].ViewAndPostPermissions.Satisfied) + s.Require().Len(response.Channels[chatID1].ViewAndPostPermissions.Permissions, 1) + s.Require().Len(response.Channels[chatID1].ViewAndPostPermissions.Permissions[viewAndPostPermissionID].Criteria, 1) + s.Require().False(response.Channels[chatID1].ViewAndPostPermissions.Permissions[viewAndPostPermissionID].Criteria[0]) + + // channel2 + s.Require().False(response.Channels[chatID2].ViewOnlyPermissions.Satisfied) + s.Require().Len(response.Channels[chatID2].ViewOnlyPermissions.Permissions, 1) + s.Require().Len(response.Channels[chatID2].ViewOnlyPermissions.Permissions[viewOnlyPermissionID].Criteria, 1) + s.Require().False(response.Channels[chatID2].ViewOnlyPermissions.Permissions[viewOnlyPermissionID].Criteria[0]) + + s.Require().False(response.Channels[chatID2].ViewAndPostPermissions.Satisfied) + s.Require().Len(response.Channels[chatID2].ViewAndPostPermissions.Permissions, 1) + s.Require().Len(response.Channels[chatID2].ViewAndPostPermissions.Permissions[viewAndPostPermissionID].Criteria, 1) + s.Require().False(response.Channels[chatID2].ViewAndPostPermissions.Permissions[viewAndPostPermissionID].Criteria[0]) + + // now change balance such that both, viewOnly and viewAndPost permission, are satisfied + tm.setResponse(chainID, accountChainIDsCombination[0].Address, gethcommon.HexToAddress(contractAddresses[chainID]), int64(1*math.Pow(10, float64(decimals)))) + + response, err = m.CheckAllChannelsPermissions(community.ID(), []gethcommon.Address{ + gethcommon.HexToAddress("0xD6b912e09E797D291E8D0eA3D3D17F8000e01c32"), + }) + s.Require().NoError(err) + s.Require().NotNil(response) + s.Require().Len(response.Channels, 2) + + // Both, viewOnly and viewAndPost permissions exist on channel1 and channel2 + // and are satisfied + + // channel1 + s.Require().True(response.Channels[chatID1].ViewOnlyPermissions.Satisfied) + s.Require().Len(response.Channels[chatID1].ViewOnlyPermissions.Permissions, 1) + s.Require().Len(response.Channels[chatID1].ViewOnlyPermissions.Permissions[viewOnlyPermissionID].Criteria, 1) + s.Require().True(response.Channels[chatID1].ViewOnlyPermissions.Permissions[viewOnlyPermissionID].Criteria[0]) + + s.Require().True(response.Channels[chatID1].ViewAndPostPermissions.Satisfied) + s.Require().Len(response.Channels[chatID1].ViewAndPostPermissions.Permissions, 1) + s.Require().Len(response.Channels[chatID1].ViewAndPostPermissions.Permissions[viewAndPostPermissionID].Criteria, 1) + s.Require().True(response.Channels[chatID1].ViewAndPostPermissions.Permissions[viewAndPostPermissionID].Criteria[0]) + + // channel2 + s.Require().True(response.Channels[chatID2].ViewOnlyPermissions.Satisfied) + s.Require().Len(response.Channels[chatID2].ViewOnlyPermissions.Permissions, 1) + s.Require().Len(response.Channels[chatID2].ViewOnlyPermissions.Permissions[viewOnlyPermissionID].Criteria, 1) + s.Require().True(response.Channels[chatID2].ViewOnlyPermissions.Permissions[viewOnlyPermissionID].Criteria[0]) + + s.Require().True(response.Channels[chatID2].ViewAndPostPermissions.Satisfied) + s.Require().Len(response.Channels[chatID2].ViewAndPostPermissions.Permissions, 1) + s.Require().Len(response.Channels[chatID2].ViewAndPostPermissions.Permissions[viewAndPostPermissionID].Criteria, 1) + s.Require().True(response.Channels[chatID2].ViewAndPostPermissions.Permissions[viewAndPostPermissionID].Criteria[0]) + + // next, delete viewOnly permission so we can check the viewAndPost permission-only case + deleteViewOnlyPermission := &requests.DeleteCommunityTokenPermission{ + CommunityID: community.ID(), + PermissionID: viewOnlyPermissionID, + } + _, _, err = m.DeleteCommunityTokenPermission(deleteViewOnlyPermission) + s.Require().NoError(err) + + response, err = m.CheckAllChannelsPermissions(community.ID(), []gethcommon.Address{ + gethcommon.HexToAddress("0xD6b912e09E797D291E8D0eA3D3D17F8000e01c32"), + }) + s.Require().NoError(err) + s.Require().NotNil(response) + s.Require().Len(response.Channels, 2) + + // Both, channel1 and channel2 now have viewAndPost only permissions that should + // be satisfied, there's no viewOnly permission anymore the response should mark it + // as satisfied as well + + // channel1 + s.Require().True(response.Channels[chatID1].ViewAndPostPermissions.Satisfied) + s.Require().Len(response.Channels[chatID1].ViewAndPostPermissions.Permissions, 1) + s.Require().Len(response.Channels[chatID1].ViewAndPostPermissions.Permissions[viewAndPostPermissionID].Criteria, 1) + s.Require().True(response.Channels[chatID1].ViewAndPostPermissions.Permissions[viewAndPostPermissionID].Criteria[0]) + + s.Require().True(response.Channels[chatID1].ViewOnlyPermissions.Satisfied) + s.Require().Len(response.Channels[chatID1].ViewOnlyPermissions.Permissions, 0) + + // channel2 + s.Require().True(response.Channels[chatID2].ViewAndPostPermissions.Satisfied) + s.Require().Len(response.Channels[chatID2].ViewAndPostPermissions.Permissions, 1) + s.Require().Len(response.Channels[chatID2].ViewAndPostPermissions.Permissions[viewAndPostPermissionID].Criteria, 1) + s.Require().True(response.Channels[chatID2].ViewAndPostPermissions.Permissions[viewAndPostPermissionID].Criteria[0]) + + s.Require().True(response.Channels[chatID2].ViewOnlyPermissions.Satisfied) + s.Require().Len(response.Channels[chatID2].ViewOnlyPermissions.Permissions, 0) + + // now change balance such that viewAndPost permission is no longer satisfied + tm.setResponse(chainID, accountChainIDsCombination[0].Address, gethcommon.HexToAddress(contractAddresses[chainID]), 0) + + response, err = m.CheckAllChannelsPermissions(community.ID(), []gethcommon.Address{ + gethcommon.HexToAddress("0xD6b912e09E797D291E8D0eA3D3D17F8000e01c32"), + }) + s.Require().NoError(err) + s.Require().NotNil(response) + s.Require().Len(response.Channels, 2) + + // because viewAndPost permission is not satisfied and there are no viewOnly permissions + // on the channels, the response should mark the viewOnly permissions as not satisfied as well + + // channel1 + s.Require().False(response.Channels[chatID1].ViewAndPostPermissions.Satisfied) + s.Require().Len(response.Channels[chatID1].ViewAndPostPermissions.Permissions, 1) + s.Require().Len(response.Channels[chatID1].ViewAndPostPermissions.Permissions[viewAndPostPermissionID].Criteria, 1) + s.Require().False(response.Channels[chatID1].ViewAndPostPermissions.Permissions[viewAndPostPermissionID].Criteria[0]) + + s.Require().False(response.Channels[chatID1].ViewOnlyPermissions.Satisfied) + s.Require().Len(response.Channels[chatID1].ViewOnlyPermissions.Permissions, 0) + + // channel2 + s.Require().False(response.Channels[chatID2].ViewAndPostPermissions.Satisfied) + s.Require().Len(response.Channels[chatID2].ViewAndPostPermissions.Permissions, 1) + s.Require().Len(response.Channels[chatID2].ViewAndPostPermissions.Permissions[viewAndPostPermissionID].Criteria, 1) + s.Require().False(response.Channels[chatID2].ViewAndPostPermissions.Permissions[viewAndPostPermissionID].Criteria[0]) + + s.Require().False(response.Channels[chatID2].ViewOnlyPermissions.Satisfied) + s.Require().Len(response.Channels[chatID2].ViewOnlyPermissions.Permissions, 0) +} + func buildTorrentConfig() params.TorrentConfig { torrentConfig := params.TorrentConfig{ Enabled: true, diff --git a/protocol/messenger_communities.go b/protocol/messenger_communities.go index cdf0b54b7..66765fc0a 100644 --- a/protocol/messenger_communities.go +++ b/protocol/messenger_communities.go @@ -3875,6 +3875,25 @@ func (m *Messenger) CheckCommunityChannelPermissions(request *requests.CheckComm return m.communitiesManager.CheckChannelPermissions(request.CommunityID, request.ChatID, addresses) } +func (m *Messenger) CheckAllCommunityChannelsPermissions(request *requests.CheckAllCommunityChannelsPermissions) (*communities.CheckAllChannelsPermissionsResponse, error) { + if err := request.Validate(); err != nil { + return nil, err + } + + accounts, err := m.settings.GetAccounts() + if err != nil { + return nil, err + } + + var addresses []gethcommon.Address + + for _, a := range accounts { + addresses = append(addresses, gethcommon.HexToAddress(a.Address.Hex())) + } + + return m.communitiesManager.CheckAllChannelsPermissions(request.CommunityID, addresses) +} + func chunkSlice[T comparable](slice []T, chunkSize int) [][]T { var chunks [][]T for i := 0; i < len(slice); i += chunkSize { diff --git a/protocol/requests/check_all_community_channels_permissions.go b/protocol/requests/check_all_community_channels_permissions.go new file mode 100644 index 000000000..a811f6ae2 --- /dev/null +++ b/protocol/requests/check_all_community_channels_permissions.go @@ -0,0 +1,23 @@ +package requests + +import ( + "errors" + + "github.com/status-im/status-go/eth-node/types" +) + +var ( + ErrCheckAllCommunityChannelsPermissionsInvalidID = errors.New("check-community-channel-permissions: invalid id") +) + +type CheckAllCommunityChannelsPermissions struct { + CommunityID types.HexBytes +} + +func (u *CheckAllCommunityChannelsPermissions) Validate() error { + if len(u.CommunityID) == 0 { + return ErrCheckAllCommunityChannelsPermissionsInvalidID + } + + return nil +} diff --git a/services/ext/api.go b/services/ext/api.go index 111b96194..d7efae25c 100644 --- a/services/ext/api.go +++ b/services/ext/api.go @@ -1333,6 +1333,10 @@ func (api *PublicAPI) CheckCommunityChannelPermissions(request *requests.CheckCo return api.service.messenger.CheckCommunityChannelPermissions(request) } +func (api *PublicAPI) CheckAllCommunityChannelsPermissions(request *requests.CheckAllCommunityChannelsPermissions) (*communities.CheckAllChannelsPermissionsResponse, error) { + return api.service.messenger.CheckAllCommunityChannelsPermissions(request) +} + func (api *PublicAPI) Messenger() *protocol.Messenger { return api.service.messenger }