From c01316524c6c4a1112c7f38e3a504945a5429676 Mon Sep 17 00:00:00 2001 From: Patryk Osmaczko Date: Fri, 26 Aug 2022 11:25:54 +0200 Subject: [PATCH] feat: make any member able to add new users to group chat closes: #2823, #2825 --- protocol/contact.go | 9 + protocol/messenger_edit_message_test.go | 2 + protocol/messenger_emoji_test.go | 2 + protocol/messenger_group_chat.go | 26 +++ protocol/messenger_group_chat_test.go | 250 +++++++++++++++++++++++ protocol/messenger_test.go | 16 ++ protocol/v1/membership_update_message.go | 3 +- 7 files changed, 307 insertions(+), 1 deletion(-) create mode 100644 protocol/messenger_group_chat_test.go diff --git a/protocol/contact.go b/protocol/contact.go index 99b59b4c2..3588e16b1 100644 --- a/protocol/contact.go +++ b/protocol/contact.go @@ -255,3 +255,12 @@ func buildContact(publicKeyString string, publicKey *ecdsa.PublicKey) (*Contact, func contactIDFromPublicKey(key *ecdsa.PublicKey) string { return types.EncodeHex(crypto.FromECDSAPub(key)) } + +func contactIDFromPublicKeyString(key string) (string, error) { + pubKey, err := common.HexToPubkey(key) + if err != nil { + return "", err + } + + return contactIDFromPublicKey(pubKey), nil +} diff --git a/protocol/messenger_edit_message_test.go b/protocol/messenger_edit_message_test.go index 97f819810..6694954a1 100644 --- a/protocol/messenger_edit_message_test.go +++ b/protocol/messenger_edit_message_test.go @@ -379,6 +379,8 @@ func (s *MessengerEditMessageSuite) TestEditGroupChatMessage() { err = s.m.SaveChat(ourChat) s.NoError(err) + s.Require().NoError(makeMutualContact(s.m, &theirMessenger.identity.PublicKey)) + members := []string{common.PubkeyToHex(&theirMessenger.identity.PublicKey)} _, err = s.m.AddMembersToGroupChat(context.Background(), ourChat.ID, members) s.NoError(err) diff --git a/protocol/messenger_emoji_test.go b/protocol/messenger_emoji_test.go index 676c230f9..cc8b6e6a5 100644 --- a/protocol/messenger_emoji_test.go +++ b/protocol/messenger_emoji_test.go @@ -153,6 +153,8 @@ func (s *MessengerEmojiSuite) TestEmojiPrivateGroup() { response, err := bob.CreateGroupChatWithMembers(context.Background(), "test", []string{}) s.NoError(err) + s.Require().NoError(makeMutualContact(bob, &alice.identity.PublicKey)) + chat := response.Chats()[0] members := []string{types.EncodeHex(crypto.FromECDSAPub(&alice.identity.PublicKey))} _, err = bob.AddMembersToGroupChat(context.Background(), chat.ID, members) diff --git a/protocol/messenger_group_chat.go b/protocol/messenger_group_chat.go index 30def0c69..ea5228090 100644 --- a/protocol/messenger_group_chat.go +++ b/protocol/messenger_group_chat.go @@ -3,6 +3,7 @@ package protocol import ( "context" "encoding/hex" + "errors" "github.com/golang/protobuf/proto" "go.uber.org/zap" @@ -15,7 +16,28 @@ import ( v1protocol "github.com/status-im/status-go/protocol/v1" ) +var ErrGroupChatAddedContacts = errors.New("group-chat: can't add members who are not mutual contacts") + +func (m *Messenger) validateAddedGroupMembers(members []string) error { + for _, memberPubkey := range members { + contactID, err := contactIDFromPublicKeyString(memberPubkey) + if err != nil { + return err + } + + contact, _ := m.allContacts.Load(contactID) + if contact == nil || !(contact.Added && contact.HasAddedUs) { + return ErrGroupChatAddedContacts + } + } + return nil +} + func (m *Messenger) CreateGroupChatWithMembers(ctx context.Context, name string, members []string) (*MessengerResponse, error) { + if err := m.validateAddedGroupMembers(members); err != nil { + return nil, err + } + var response MessengerResponse logger := m.logger.With(zap.String("site", "CreateGroupChatWithMembers")) logger.Info("Creating group chat", zap.String("name", name), zap.Any("members", members)) @@ -147,6 +169,10 @@ func (m *Messenger) RemoveMemberFromGroupChat(ctx context.Context, chatID string } func (m *Messenger) AddMembersToGroupChat(ctx context.Context, chatID string, members []string) (*MessengerResponse, error) { + if err := m.validateAddedGroupMembers(members); err != nil { + return nil, err + } + var response MessengerResponse logger := m.logger.With(zap.String("site", "AddMembersFromGroupChat")) logger.Info("Adding members form group chat", zap.String("chatID", chatID), zap.Any("members", members)) diff --git a/protocol/messenger_group_chat_test.go b/protocol/messenger_group_chat_test.go new file mode 100644 index 000000000..a41326e7f --- /dev/null +++ b/protocol/messenger_group_chat_test.go @@ -0,0 +1,250 @@ +package protocol + +import ( + "context" + "crypto/ecdsa" + "fmt" + "testing" + + "github.com/stretchr/testify/suite" + "go.uber.org/zap" + + gethbridge "github.com/status-im/status-go/eth-node/bridge/geth" + "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/tt" + "github.com/status-im/status-go/waku" +) + +func TestGroupChatSuite(t *testing.T) { + suite.Run(t, new(MessengerGroupChatSuite)) +} + +type MessengerGroupChatSuite struct { + suite.Suite + + // If one wants to send messages between different instances of Messenger, + // a single Waku service should be shared. + shh types.Waku + logger *zap.Logger +} + +func (s *MessengerGroupChatSuite) newMessenger() *Messenger { + privateKey, err := crypto.GenerateKey() + s.Require().NoError(err) + + messenger, err := newMessengerWithKey(s.shh, privateKey, s.logger, []Option{}) + s.Require().NoError(err) + + return messenger +} + +func (s *MessengerGroupChatSuite) startNewMessenger() *Messenger { + messenger := s.newMessenger() + + _, err := messenger.Start() + s.Require().NoError(err) + + return messenger +} + +func (s *MessengerGroupChatSuite) SetupTest() { + s.logger = tt.MustCreateTestLogger() + + config := waku.DefaultConfig + config.MinimumAcceptedPoW = 0 + shh := waku.New(&config, s.logger) + s.shh = gethbridge.NewGethWakuWrapper(shh) + s.Require().NoError(shh.Start()) +} + +func (s *MessengerGroupChatSuite) TearDownTest() { +} + +func (s *MessengerGroupChatSuite) createGroupChat(creator *Messenger, name string, members []string) *Chat { + response, err := creator.CreateGroupChatWithMembers(context.Background(), name, members) + s.Require().NoError(err) + s.Require().Len(response.Chats(), 1) + + chat := response.Chats()[0] + err = creator.SaveChat(chat) + s.Require().NoError(err) + + return chat +} + +func (s *MessengerGroupChatSuite) createEmptyGroupChat(creator *Messenger, name string) *Chat { + return s.createGroupChat(creator, name, []string{}) +} + +func (s *MessengerGroupChatSuite) verifyGroupChatCreated(member *Messenger, expectedChatActive bool) { + response, err := WaitOnMessengerResponse( + member, + func(r *MessengerResponse) bool { return len(r.Chats()) > 0 }, + "chat invitation not received", + ) + s.Require().NoError(err) + s.Require().Len(response.Chats(), 1) + s.Require().True(response.Chats()[0].Active == expectedChatActive) +} + +func makeMutualContact(origin *Messenger, contactPubkey *ecdsa.PublicKey) error { + contact, err := BuildContactFromPublicKey(contactPubkey) + if err != nil { + return err + } + contact.Added = true + contact.HasAddedUs = true + origin.allContacts.Store(contact.ID, contact) + + return nil +} + +func (s *MessengerGroupChatSuite) makeContact(origin *Messenger, toAdd *Messenger) { + s.Require().NoError(makeMutualContact(origin, &toAdd.identity.PublicKey)) +} + +func (s *MessengerGroupChatSuite) TestGroupChatCreation() { + testCases := []struct { + name string + creatorAddedMemberAsContact bool + memberAddedCreatorAsContact bool + expectedCreationSuccess bool + expectedAddedMemberChatActive bool + }{ + { + name: "not added - not added", + creatorAddedMemberAsContact: false, + memberAddedCreatorAsContact: false, + expectedCreationSuccess: false, + expectedAddedMemberChatActive: false, + }, + { + name: "added - not added", + creatorAddedMemberAsContact: true, + memberAddedCreatorAsContact: false, + expectedCreationSuccess: true, + expectedAddedMemberChatActive: false, + }, + { + name: "not added - added", + creatorAddedMemberAsContact: false, + memberAddedCreatorAsContact: true, + expectedCreationSuccess: false, + expectedAddedMemberChatActive: false, + }, + { + name: "added - added", + creatorAddedMemberAsContact: true, + memberAddedCreatorAsContact: true, + expectedCreationSuccess: true, + expectedAddedMemberChatActive: true, + }, + } + + for i, testCase := range testCases { + creator := s.startNewMessenger() + member := s.startNewMessenger() + members := []string{common.PubkeyToHex(&member.identity.PublicKey)} + + if testCase.creatorAddedMemberAsContact { + s.makeContact(creator, member) + } + if testCase.memberAddedCreatorAsContact { + s.makeContact(member, creator) + } + + _, err := creator.CreateGroupChatWithMembers(context.Background(), fmt.Sprintf("test_group_chat_%d", i), members) + if testCase.creatorAddedMemberAsContact { + s.Require().NoError(err) + s.verifyGroupChatCreated(member, testCase.expectedAddedMemberChatActive) + } else { + s.Require().EqualError(err, "group-chat: can't add members who are not mutual contacts") + } + + defer creator.Shutdown() + defer member.Shutdown() + } +} + +func (s *MessengerGroupChatSuite) TestGroupChatMembersAddition() { + testCases := []struct { + name string + inviterAddedMemberAsContact bool + memberAddedInviterAsContact bool + expectedAdditionSuccess bool + expectedAddedMemberChatActive bool + }{ + { + name: "not added - not added", + inviterAddedMemberAsContact: false, + memberAddedInviterAsContact: false, + expectedAdditionSuccess: false, + expectedAddedMemberChatActive: false, + }, + { + name: "added - not added", + inviterAddedMemberAsContact: true, + memberAddedInviterAsContact: false, + expectedAdditionSuccess: true, + expectedAddedMemberChatActive: false, + }, + { + name: "not added - added", + inviterAddedMemberAsContact: false, + memberAddedInviterAsContact: true, + expectedAdditionSuccess: false, + expectedAddedMemberChatActive: false, + }, + { + name: "added - added", + inviterAddedMemberAsContact: true, + memberAddedInviterAsContact: true, + expectedAdditionSuccess: true, + expectedAddedMemberChatActive: true, + }, + } + + for i, testCase := range testCases { + admin := s.startNewMessenger() + inviter := s.startNewMessenger() + member := s.startNewMessenger() + members := []string{common.PubkeyToHex(&member.identity.PublicKey)} + + if testCase.inviterAddedMemberAsContact { + s.makeContact(inviter, member) + } + if testCase.memberAddedInviterAsContact { + s.makeContact(member, inviter) + } + + for j, inviterIsAlsoGroupCreator := range []bool{true, false} { + var groupChat *Chat + if inviterIsAlsoGroupCreator { + groupChat = s.createEmptyGroupChat(inviter, fmt.Sprintf("test_group_chat_%d_%d", i, j)) + } else { + s.makeContact(admin, inviter) + groupChat = s.createGroupChat(admin, fmt.Sprintf("test_group_chat_%d_%d", i, j), []string{common.PubkeyToHex(&inviter.identity.PublicKey)}) + err := inviter.SaveChat(groupChat) + s.Require().NoError(err) + } + + _, err := inviter.AddMembersToGroupChat(context.Background(), groupChat.ID, members) + if testCase.inviterAddedMemberAsContact { + s.Require().NoError(err) + s.verifyGroupChatCreated(member, testCase.expectedAddedMemberChatActive) + } else { + s.Require().EqualError(err, "group-chat: can't add members who are not mutual contacts") + } + } + + defer admin.Shutdown() + defer inviter.Shutdown() + defer member.Shutdown() + } +} + +func (s *MessengerGroupChatSuite) TestGroupChatEdit() { + +} diff --git a/protocol/messenger_test.go b/protocol/messenger_test.go index 70144ede9..ee92b32d6 100644 --- a/protocol/messenger_test.go +++ b/protocol/messenger_test.go @@ -499,6 +499,9 @@ func (s *MessengerSuite) TestSendPrivateGroup() { chat := response.Chats()[0] key, err := crypto.GenerateKey() s.NoError(err) + + s.Require().NoError(makeMutualContact(s.m, &key.PublicKey)) + members := []string{"0x" + hex.EncodeToString(crypto.FromECDSAPub(&key.PublicKey))} _, err = s.m.AddMembersToGroupChat(context.Background(), chat.ID, members) s.NoError(err) @@ -906,6 +909,8 @@ func (s *MessengerSuite) TestRetrieveTheirPrivateGroupChat() { err = s.m.SaveChat(ourChat) s.NoError(err) + s.Require().NoError(makeMutualContact(s.m, &theirMessenger.identity.PublicKey)) + members := []string{"0x" + hex.EncodeToString(crypto.FromECDSAPub(&theirMessenger.identity.PublicKey))} _, err = s.m.AddMembersToGroupChat(context.Background(), ourChat.ID, members) s.NoError(err) @@ -972,6 +977,8 @@ func (s *MessengerSuite) TestChangeNameGroupChat() { err = s.m.SaveChat(ourChat) s.NoError(err) + s.Require().NoError(makeMutualContact(s.m, &theirMessenger.identity.PublicKey)) + members := []string{"0x" + hex.EncodeToString(crypto.FromECDSAPub(&theirMessenger.identity.PublicKey))} _, err = s.m.AddMembersToGroupChat(context.Background(), ourChat.ID, members) s.NoError(err) @@ -1027,6 +1034,8 @@ func (s *MessengerSuite) TestReInvitedToGroupChat() { err = s.m.SaveChat(ourChat) s.NoError(err) + s.Require().NoError(makeMutualContact(s.m, &theirMessenger.identity.PublicKey)) + members := []string{"0x" + hex.EncodeToString(crypto.FromECDSAPub(&theirMessenger.identity.PublicKey))} _, err = s.m.AddMembersToGroupChat(context.Background(), ourChat.ID, members) s.NoError(err) @@ -1486,6 +1495,11 @@ func (s *MessengerSuite) TestSharedSecretHandler() { func (s *MessengerSuite) TestCreateGroupChatWithMembers() { members := []string{testPK} + + pubKey, err := common.HexToPubkey(testPK) + s.Require().NoError(err) + s.Require().NoError(makeMutualContact(s.m, pubKey)) + response, err := s.m.CreateGroupChatWithMembers(context.Background(), "test", members) s.NoError(err) s.Require().Len(response.Chats(), 1) @@ -1510,6 +1524,8 @@ func (s *MessengerSuite) TestAddMembersToChat() { s.Require().NoError(err) members := []string{"0x" + hex.EncodeToString(crypto.FromECDSAPub(&key.PublicKey))} + s.Require().NoError(makeMutualContact(s.m, &key.PublicKey)) + response, err = s.m.AddMembersToGroupChat(context.Background(), chat.ID, members) s.Require().NoError(err) s.Require().Len(response.Chats(), 1) diff --git a/protocol/v1/membership_update_message.go b/protocol/v1/membership_update_message.go index 593aa1a6c..bd5f8bd54 100644 --- a/protocol/v1/membership_update_message.go +++ b/protocol/v1/membership_update_message.go @@ -506,7 +506,8 @@ func (g Group) validateEvent(event MembershipUpdateEvent) bool { case protobuf.MembershipUpdateEvent_IMAGE_CHANGED: return g.admins.Has(event.From) && len(event.Image) > 0 case protobuf.MembershipUpdateEvent_MEMBERS_ADDED: - return g.admins.Has(event.From) + // Admins and members can add new members + return g.admins.Has(event.From) || g.members.Has(event.From) case protobuf.MembershipUpdateEvent_MEMBER_JOINED: return g.members.Has(event.From) case protobuf.MembershipUpdateEvent_MEMBER_REMOVED: