Handle remove event out of order in private group chats

When we received a remove event from a private group chat out of order,
the chat would not be created.
This was causing some issues if later on we received the previous event.
This commit changes the behavior so that a chat is created.
This commit is contained in:
Andrea Maria Piana 2023-08-09 18:07:28 +01:00
parent 423991fb30
commit 4b9517c0e2
5 changed files with 148 additions and 15 deletions

View File

@ -2,6 +2,7 @@ package protocol
import (
"context"
"crypto/ecdsa"
"encoding/hex"
"errors"
@ -124,15 +125,16 @@ func (m *Messenger) CreateGroupChatFromInvitation(name string, chatID string, ad
return &response, m.saveChat(&chat)
}
func (m *Messenger) RemoveMembersFromGroupChat(ctx context.Context, chatID string, members []string) (*MessengerResponse, error) {
var response MessengerResponse
type removeMembersFromGroupChatResponse struct {
oldRecipients []*ecdsa.PublicKey
group *v1protocol.Group
encodedProtobuf []byte
}
func (m *Messenger) removeMembersFromGroupChat(ctx context.Context, chat *Chat, members []string) (*removeMembersFromGroupChatResponse, error) {
chatID := chat.ID
logger := m.logger.With(zap.String("site", "RemoveMembersFromGroupChat"))
logger.Info("Removing members form group chat", zap.String("chatID", chatID), zap.Any("members", members))
chat, ok := m.allChats.Load(chatID)
if !ok {
return nil, ErrChatNotFound
}
group, err := newProtocolGroupFromChat(chat)
if err != nil {
return nil, err
@ -162,21 +164,42 @@ func (m *Messenger) RemoveMembersFromGroupChat(ctx context.Context, chatID strin
}
}
encodedMessage, err := m.sender.EncodeMembershipUpdate(group, nil)
encoded, err := m.sender.EncodeMembershipUpdate(group, nil)
if err != nil {
return nil, err
}
return &removeMembersFromGroupChatResponse{
oldRecipients: oldRecipients,
group: group,
encodedProtobuf: encoded,
}, nil
}
func (m *Messenger) RemoveMembersFromGroupChat(ctx context.Context, chatID string, members []string) (*MessengerResponse, error) {
var response MessengerResponse
chat, ok := m.allChats.Load(chatID)
if !ok {
return nil, ErrChatNotFound
}
removeMembersResponse, err := m.removeMembersFromGroupChat(ctx, chat, members)
if err != nil {
return nil, err
}
_, err = m.dispatchMessage(ctx, common.RawMessage{
LocalChatID: chat.ID,
Payload: encodedMessage,
Payload: removeMembersResponse.encodedProtobuf,
MessageType: protobuf.ApplicationMetadataMessage_MEMBERSHIP_UPDATE_MESSAGE,
Recipients: oldRecipients,
Recipients: removeMembersResponse.oldRecipients,
})
if err != nil {
return nil, err
}
chat.updateChatFromGroupMembershipChanges(group)
chat.updateChatFromGroupMembershipChanges(removeMembersResponse.group)
return m.addMessagesAndChat(chat, buildSystemMessages(chat.MembershipUpdates, m.systemMessagesTranslations), &response)
}

View File

@ -6,6 +6,7 @@ import (
"fmt"
"testing"
"github.com/golang/protobuf/proto"
"github.com/stretchr/testify/suite"
userimage "github.com/status-im/status-go/images"
@ -409,3 +410,47 @@ func (s *MessengerGroupChatSuite) TestGroupChatHandleDeleteMemberMessage() {
defer s.NoError(admin.Shutdown())
defer s.NoError(member.Shutdown())
}
func (s *MessengerGroupChatSuite) TestGroupChatMembersRemovalOutOfOrder() {
admin := s.startNewMessenger()
memberA := s.startNewMessenger()
members := []string{common.PubkeyToHex(&memberA.identity.PublicKey)}
s.makeMutualContacts(admin, memberA)
groupChat := s.createGroupChat(admin, "test_group_chat", members)
removeMembersResponse, err := admin.removeMembersFromGroupChat(context.Background(), groupChat, []string{common.PubkeyToHex(&memberA.identity.PublicKey)})
s.Require().NoError(err)
encodedMessage := removeMembersResponse.encodedProtobuf
message := protobuf.MembershipUpdateMessage{}
err = proto.Unmarshal(encodedMessage, &message)
s.Require().NoError(err)
response := &MessengerResponse{}
messageState := &ReceivedMessageState{
ExistingMessagesMap: make(map[string]bool),
Response: response,
AllChats: new(chatMap),
Timesource: memberA.getTimesource(),
}
c, err := buildContact(admin.myHexIdentity(), &admin.identity.PublicKey)
s.Require().NoError(err)
messageState.CurrentMessageState = &CurrentMessageState{
Contact: c,
}
err = memberA.HandleMembershipUpdate(messageState, nil, &message, memberA.systemMessagesTranslations)
s.Require().NoError(err)
s.Require().NotNil(messageState.Response)
s.Require().Len(messageState.Response.Chats(), 1)
s.Require().Len(messageState.Response.Chats()[0].Members, 1)
defer s.NoError(admin.Shutdown())
defer s.NoError(memberA.Shutdown())
}

View File

@ -138,12 +138,17 @@ func (m *Messenger) HandleMembershipUpdate(messageState *ReceivedMessageState, c
return err
}
// A new chat must contain us
if !group.IsMember(ourKey) {
// A new chat must have contained us at some point
wasEverMember, err := group.WasEverMember(ourKey)
if err != nil {
return err
}
if !wasEverMember {
return errors.New("can't create a new group chat without us being a member")
}
// A new chat always adds us
wasUserAdded = true
wasUserAdded = group.IsMember(ourKey)
newChat := CreateGroupChat(messageState.Timesource)
// We set group chat inactive and create a notification instead
// unless is coming from us or a contact or were waiting for approval.

View File

@ -490,6 +490,15 @@ func (g Group) Creator() (string, error) {
return first.From, nil
}
func (g Group) isCreator(id string) (bool, error) {
c, err := g.Creator()
if err != nil {
return false, err
}
return id == c, nil
}
func (g Group) validateChatID(chatID string) bool {
creator, err := g.Creator()
if err != nil || creator == "" {
@ -504,6 +513,28 @@ func (g Group) IsMember(id string) bool {
return g.members.Has(id)
}
func (g Group) WasEverMember(id string) (bool, error) {
isCreator, err := g.isCreator(id)
if err != nil {
return false, err
}
if isCreator {
return true, nil
}
for _, event := range g.events {
if event.Type == protobuf.MembershipUpdateEvent_MEMBERS_ADDED {
for _, member := range event.Members {
if member == id {
return true, nil
}
}
}
}
return false, nil
}
// validateEvent returns true if a given event is valid.
func (g Group) validateEvent(event MembershipUpdateEvent) bool {
if len(event.From) == 0 {

View File

@ -543,3 +543,32 @@ func TestAbridgedEventsAdmins(t *testing.T) {
// All the events are relevant here, so it should be the same
require.Len(t, g.AbridgedEvents(), 3)
}
func TestWasEverMember(t *testing.T) {
key, err := crypto.GenerateKey()
require.NoError(t, err)
g, err := NewGroupWithCreator("abc", "#fa6565", 20, key)
require.NoError(t, err)
wasMember, err := g.WasEverMember(publicKeyToString(&key.PublicKey))
require.NoError(t, err)
require.True(t, wasMember)
key2, err := crypto.GenerateKey()
require.NoError(t, err)
wasMember, err = g.WasEverMember(publicKeyToString(&key2.PublicKey))
require.NoError(t, err)
require.False(t, wasMember)
// Add a new member
event := NewMembersAddedEvent([]string{publicKeyToString(&key2.PublicKey)}, 21)
event.From = publicKeyToString(&key.PublicKey)
event.ChatID = g.chatID
err = g.ProcessEvent(event)
require.NoError(t, err)
wasMember, err = g.WasEverMember(publicKeyToString(&key2.PublicKey))
require.NoError(t, err)
require.True(t, wasMember)
}