diff --git a/protocol/chat.go b/protocol/chat.go index fdb11f503..58219e175 100644 --- a/protocol/chat.go +++ b/protocol/chat.go @@ -514,6 +514,10 @@ func CreateCommunityChat(orgID, chatID string, orgChat *protobuf.CommunityChat, } } +func (c *Chat) CommunityChannelID() string { + return strings.TrimPrefix(c.ID, c.CommunityID) +} + func (c *Chat) DeepLink() string { if c.OneToOne() { return "status-app://p/" + c.ID @@ -523,7 +527,7 @@ func (c *Chat) DeepLink() string { } if c.CommunityChat() { - communityChannelID := strings.TrimPrefix(c.ID, c.CommunityID) + communityChannelID := c.CommunityChannelID() pubkey, err := types.DecodeHex(c.CommunityID) if err != nil { return "" diff --git a/protocol/communities_messenger_token_permissions_test.go b/protocol/communities_messenger_token_permissions_test.go index db754db7d..f20ed4008 100644 --- a/protocol/communities_messenger_token_permissions_test.go +++ b/protocol/communities_messenger_token_permissions_test.go @@ -1246,6 +1246,133 @@ func (s *MessengerCommunitiesTokenPermissionsSuite) TestViewChannelPermissions() } } +func (s *MessengerCommunitiesTokenPermissionsSuite) TestSearchMessageinPermissionedChannel() { + community, chat := s.createCommunity() + + newChat := protobuf.CommunityChat{ + Permissions: &protobuf.CommunityPermissions{ + EnsOnly: false, + Private: false, + Access: 1, + }, + Identity: &protobuf.ChatIdentity{ + DisplayName: "new-channel", + Description: "description", + Emoji: "", + Color: "", + }, + CategoryId: "", + ViewersCanPostReactions: true, + HideIfPermissionsNotMet: false, + } + + response, err := s.owner.CreateCommunityChat(community.ID(), &newChat) + s.Require().NoError(err) + s.Require().NotNil(response) + newChatID := response.Chats()[0].ID + + // bob joins the community + s.advertiseCommunityTo(community, s.bob) + s.joinCommunity(community, s.bob, bobPassword, []string{}) + + // send message to the original 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 { + _, ok := r.messages[msg.ID] + return ok + }, + "no messages", + ) + s.Require().NoError(err) + s.Require().Len(response.Messages(), 1) + s.Require().Equal(msg.Text, response.Messages()[0].Text) + + // send message to the new channel + msgText := "hello on new chat" + msg = s.sendChatMessage(s.owner, newChatID, msgText) + + // bob can read the message + response, err = WaitOnMessengerResponse( + s.bob, + func(r *MessengerResponse) bool { + _, ok := r.messages[msg.ID] + return ok + }, + "no messages", + ) + s.Require().NoError(err) + s.Require().Len(response.Messages(), 1) + s.Require().Equal(msg.Text, response.Messages()[0].Text) + + waitOnBobToBeKickedFromChannel := waitOnCommunitiesEvent(s.owner, func(sub *communities.Subscription) bool { + channel, ok := sub.Community.Chats()[chat.CommunityChatID()] + return ok && len(channel.Members) == 1 + }) + waitOnChannelToBeRekeyedOnceBobIsKicked := s.waitOnKeyDistribution(func(sub *CommunityAndKeyActions) bool { + action, ok := sub.keyActions.ChannelKeysActions[chat.CommunityChatID()] + return ok && (action.ActionType == communities.EncryptionKeyRekey || action.ActionType == communities.EncryptionKeyAdd) + }) + + // 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", + AmountInWei: "100000000000000000000", + Decimals: uint64(18), + }, + }, + ChatIds: []string{chat.ID}, + } + + 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) + + // bob receives community changes + // channel members should be empty, + // this info is available only to channel members + _, err = WaitOnMessengerResponse( + s.bob, + func(r *MessengerResponse) bool { + c, err := s.bob.GetCommunityByID(community.ID()) + if err != nil { + return false + } + if c == nil { + return false + } + channel := c.Chats()[chat.CommunityChatID()] + return channel != nil && len(channel.Members) == 0 + }, + "no community that satisfies criteria", + ) + s.Require().NoError(err) + + // Bob searches for "hello" but only finds it in the new channel + communities := make([]string, 1) + communities[0] = community.IDString() + messages, err := s.bob.AllMessagesFromChatsAndCommunitiesWhichMatchTerm(communities, make([]string, 0), "hello", false) + s.Require().NoError(err) + s.Require().Len(messages, 1) + s.Require().Equal(msgText, messages[0].Text) +} + func (s *MessengerCommunitiesTokenPermissionsSuite) TestMemberRoleGetUpdatedWhenChangingPermissions() { community, chat := s.createCommunity() diff --git a/protocol/messenger.go b/protocol/messenger.go index 7d74da0d7..3949871cd 100644 --- a/protocol/messenger.go +++ b/protocol/messenger.go @@ -82,6 +82,11 @@ const ( privateChat ChatContext = "private-chat" ) +// errors +var ( + ErrChatNotFoundError = errors.New("Chat not found") +) + var communityAdvertiseIntervalSecond int64 = 60 * 60 // messageCacheIntervalMs is how long we should keep processed messages in the cache, in ms @@ -2382,7 +2387,7 @@ func (m *Messenger) sendChatMessage(ctx context.Context, message *common.Message // A valid added chat is required. chat, ok := m.allChats.Load(message.ChatId) if !ok { - return nil, errors.New("Chat not found") + return nil, ErrChatNotFoundError } err = m.handleStandaloneChatIdentity(chat) @@ -4343,11 +4348,72 @@ func (m *Messenger) AllMessageByChatIDWhichMatchTerm(chatID string, searchTerm s return nil, err } - return m.persistence.AllMessageByChatIDWhichMatchTerm(chatID, searchTerm, caseSensitive) + messages, err := m.persistence.AllMessageByChatIDWhichMatchTerm(chatID, searchTerm, caseSensitive) + if err != nil { + return nil, err + } + + return m.filterOutHiddenChatMessages(messages) + } func (m *Messenger) AllMessagesFromChatsAndCommunitiesWhichMatchTerm(communityIds []string, chatIds []string, searchTerm string, caseSensitive bool) ([]*common.Message, error) { - return m.persistence.AllMessagesFromChatsAndCommunitiesWhichMatchTerm(communityIds, chatIds, searchTerm, caseSensitive) + messages, err := m.persistence.AllMessagesFromChatsAndCommunitiesWhichMatchTerm(communityIds, chatIds, searchTerm, caseSensitive) + if err != nil { + return nil, err + } + + return m.filterOutHiddenChatMessages(messages) +} + +func (m *Messenger) filterOutHiddenChatMessages(messages []*common.Message) ([]*common.Message, error) { + communitiesCache := make(map[string]*communities.Community) + chatVisibilityCache := make(map[string]bool) + var filteredMessages []*common.Message + + for _, message := range messages { + chatVisible, ok := chatVisibilityCache[message.ChatId] + if ok && chatVisible { + filteredMessages = append(filteredMessages, message) + continue + } + + chat, ok := m.allChats.Load(message.ChatId) + if !ok { + return nil, ErrChatNotFoundError + } + + if chat.CommunityID == "" { + filteredMessages = append(filteredMessages, message) + continue + } + + community, ok := communitiesCache[chat.CommunityID] + if !ok { + communityID, err := hexutil.Decode(chat.CommunityID) + if err != nil { + return nil, err + } + comm, err := m.communitiesManager.GetByID(communityID) + if err != nil { + if err == communities.ErrOrgNotFound { + continue + } + return nil, err + } + communitiesCache[chat.CommunityID] = comm + community = comm + } + + canView := community.CanView(&m.identity.PublicKey, chat.CommunityChannelID()) + chatVisibilityCache[chat.ID] = canView + + if canView { + filteredMessages = append(filteredMessages, message) + } + } + + return filteredMessages, nil } func (m *Messenger) SaveMessages(messages []*common.Message) error { @@ -4522,7 +4588,7 @@ func (m *Messenger) syncChatMessagesRead(ctx context.Context, chatID string, clo func (m *Messenger) markAllRead(chatID string, clock uint64, shouldBeSynced bool) error { chat, ok := m.allChats.Load(chatID) if !ok { - return errors.New("chat not found") + return ErrChatNotFoundError } _, _, err := m.persistence.MarkAllRead(chatID, clock) @@ -4566,7 +4632,7 @@ func (m *Messenger) MarkAllRead(ctx context.Context, chatID string) (*MessengerR if clock == 0 { chat, ok := m.allChats.Load(chatID) if !ok { - return nil, errors.New("chat not found") + return nil, ErrChatNotFoundError } clock, _ = chat.NextClockAndTimestamp(m.getTimesource()) } @@ -4709,7 +4775,7 @@ func (m *Messenger) muteChat(chat *Chat, contact *Contact, mutedTill time.Time) func (m *Messenger) UnmuteChat(chatID string) error { chat, ok := m.allChats.Load(chatID) if !ok { - return errors.New("chat not found") + return ErrChatNotFoundError } var contact *Contact @@ -4772,7 +4838,7 @@ func (m *Messenger) RequestTransaction(ctx context.Context, chatID, value, contr // A valid added chat is required. chat, ok := m.allChats.Load(chatID) if !ok { - return nil, errors.New("Chat not found") + return nil, ErrChatNotFoundError } if chat.ChatType != ChatTypeOneToOne { return nil, errors.New("Need to be a one-to-one chat") @@ -4850,7 +4916,7 @@ func (m *Messenger) RequestAddressForTransaction(ctx context.Context, chatID, fr // A valid added chat is required. chat, ok := m.allChats.Load(chatID) if !ok { - return nil, errors.New("Chat not found") + return nil, ErrChatNotFoundError } if chat.ChatType != ChatTypeOneToOne { return nil, errors.New("Need to be a one-to-one chat") @@ -4939,7 +5005,7 @@ func (m *Messenger) AcceptRequestAddressForTransaction(ctx context.Context, mess // A valid added chat is required. chat, ok := m.allChats.Load(chatID) if !ok { - return nil, errors.New("Chat not found") + return nil, ErrChatNotFoundError } if chat.ChatType != ChatTypeOneToOne { return nil, errors.New("Need to be a one-to-one chat") @@ -5035,7 +5101,7 @@ func (m *Messenger) DeclineRequestTransaction(ctx context.Context, messageID str // A valid added chat is required. chat, ok := m.allChats.Load(chatID) if !ok { - return nil, errors.New("Chat not found") + return nil, ErrChatNotFoundError } if chat.ChatType != ChatTypeOneToOne { return nil, errors.New("Need to be a one-to-one chat") @@ -5118,7 +5184,7 @@ func (m *Messenger) DeclineRequestAddressForTransaction(ctx context.Context, mes // A valid added chat is required. chat, ok := m.allChats.Load(chatID) if !ok { - return nil, errors.New("Chat not found") + return nil, ErrChatNotFoundError } if chat.ChatType != ChatTypeOneToOne { return nil, errors.New("Need to be a one-to-one chat") @@ -5201,7 +5267,7 @@ func (m *Messenger) AcceptRequestTransaction(ctx context.Context, transactionHas // A valid added chat is required. chat, ok := m.allChats.Load(chatID) if !ok { - return nil, errors.New("Chat not found") + return nil, ErrChatNotFoundError } if chat.ChatType != ChatTypeOneToOne { return nil, errors.New("Need to be a one-to-one chat") @@ -5290,7 +5356,7 @@ func (m *Messenger) SendTransaction(ctx context.Context, chatID, value, contract // A valid added chat is required. chat, ok := m.allChats.Load(chatID) if !ok { - return nil, errors.New("Chat not found") + return nil, ErrChatNotFoundError } if chat.ChatType != ChatTypeOneToOne { return nil, errors.New("Need to be a one-to-one chat")