diff --git a/protocol/communities/manager.go b/protocol/communities/manager.go index 7b1d18a56..9b493d6f4 100644 --- a/protocol/communities/manager.go +++ b/protocol/communities/manager.go @@ -66,6 +66,7 @@ func SetValidateInterval(duration time.Duration) { var ( ErrTorrentTimedout = errors.New("torrent has timed out") ErrCommunityRequestAlreadyRejected = errors.New("that user was already rejected from the community") + ErrInvalidClock = errors.New("invalid clock to cancel request to join") ) type Manager struct { @@ -2326,6 +2327,15 @@ func (m *Manager) HandleCommunityCancelRequestToJoin(signer *ecdsa.PublicKey, re return nil, ErrOrgNotFound } + previousRequestToJoin, err := m.GetRequestToJoinByPkAndCommunityID(signer, community.ID()) + if err != nil { + return nil, err + } + + if request.Clock <= previousRequestToJoin.Clock { + return nil, ErrInvalidClock + } + retainDeclined, err := m.shouldUserRetainDeclined(signer, community, request.Clock) if err != nil { return nil, err @@ -2344,6 +2354,18 @@ func (m *Manager) HandleCommunityCancelRequestToJoin(signer *ecdsa.PublicKey, re return nil, err } + if community.HasMember(signer) { + _, err = community.RemoveUserFromOrg(signer) + if err != nil { + return nil, err + } + + err = m.saveAndPublish(community) + if err != nil { + return nil, err + } + } + return requestToJoin, nil } @@ -2922,6 +2944,10 @@ func (m *Manager) GetCommunityRequestToJoinClock(pk *ecdsa.PublicKey, communityI return request.Clock, nil } +func (m *Manager) GetRequestToJoinByPkAndCommunityID(pk *ecdsa.PublicKey, communityID []byte) (*RequestToJoin, error) { + return m.persistence.GetRequestToJoinByPkAndCommunityID(common.PubkeyToHex(pk), communityID) +} + func (m *Manager) UpdateCommunityDescriptionMagnetlinkMessageClock(communityID types.HexBytes, clock uint64) error { community, err := m.GetByIDString(communityID.String()) if err != nil { @@ -3272,6 +3298,10 @@ func (m *Manager) CanceledRequestsToJoinForUser(pk *ecdsa.PublicKey) ([]*Request return m.persistence.CanceledRequestsToJoinForUser(common.PubkeyToHex(pk)) } +func (m *Manager) CanceledRequestToJoinForUserForCommunityID(pk *ecdsa.PublicKey, communityID []byte) (*RequestToJoin, error) { + return m.persistence.CanceledRequestToJoinForUserForCommunityID(common.PubkeyToHex(pk), communityID) +} + func (m *Manager) PendingRequestsToJoin() ([]*RequestToJoin, error) { return m.persistence.PendingRequestsToJoin() } diff --git a/protocol/communities/persistence.go b/protocol/communities/persistence.go index 454ccb3f6..89493d6f5 100644 --- a/protocol/communities/persistence.go +++ b/protocol/communities/persistence.go @@ -688,6 +688,26 @@ func (p *Persistence) CanceledRequestsToJoinForUser(pk string) ([]*RequestToJoin return requests, nil } +func (p *Persistence) CanceledRequestToJoinForUserForCommunityID(pk string, communityID []byte) (*RequestToJoin, error) { + row := p.db.QueryRow(`SELECT id,public_key,clock,ens_name,chat_id,community_id,state + FROM + communities_requests_to_join + WHERE + state = ? AND public_key = ? AND community_id = ?`, + RequestToJoinStateCanceled, pk, communityID) + + request := &RequestToJoin{} + + err := row.Scan(&request.ID, &request.PublicKey, &request.Clock, &request.ENSName, &request.ChatID, &request.CommunityID, &request.State) + if err == sql.ErrNoRows { + return nil, nil + } else if err != nil { + return nil, err + } + + return request, nil +} + func (p *Persistence) RequestsToJoinForUserByState(pk string, state RequestToJoinState) ([]*RequestToJoin, error) { var requests []*RequestToJoin rows, err := p.db.Query(`SELECT id,public_key,clock,ens_name,chat_id,community_id,state FROM communities_requests_to_join WHERE state = ? AND public_key = ?`, state, pk) diff --git a/protocol/communities_messenger_test.go b/protocol/communities_messenger_test.go index 40120b0d9..08e553707 100644 --- a/protocol/communities_messenger_test.go +++ b/protocol/communities_messenger_test.go @@ -3774,3 +3774,164 @@ func (s *MessengerCommunitiesSuite) TestRetrieveBigCommunity() { }, "updated description not received") s.Require().NoError(err) } + +func (s *MessengerCommunitiesSuite) TestRequestAndCancelCommunityAdminOffline() { + ctx := context.Background() + + community, _ := s.createCommunity() + s.advertiseCommunityTo(community, s.owner, s.alice) + + request := &requests.RequestToJoinCommunity{CommunityID: community.ID()} + // We try to join the org + response, err := s.alice.RequestToJoinCommunity(request) + s.Require().NoError(err) + s.Require().NotNil(response) + s.Require().Len(response.RequestsToJoinCommunity, 1) + + requestToJoin1 := response.RequestsToJoinCommunity[0] + s.Require().NotNil(requestToJoin1) + s.Require().Equal(community.ID(), requestToJoin1.CommunityID) + s.Require().True(requestToJoin1.Our) + s.Require().NotEmpty(requestToJoin1.ID) + s.Require().NotEmpty(requestToJoin1.Clock) + s.Require().Equal(requestToJoin1.PublicKey, common.PubkeyToHex(&s.alice.identity.PublicKey)) + s.Require().Equal(communities.RequestToJoinStatePending, requestToJoin1.State) + + messageState := s.alice.buildMessageState() + messageState.CurrentMessageState = &CurrentMessageState{} + + messageState.CurrentMessageState.PublicKey = &s.alice.identity.PublicKey + + statusMessage := v1protocol.StatusMessage{} + statusMessage.TransportLayer.Dst = community.PublicKey() + + requestToJoinProto := &protobuf.CommunityRequestToJoin{ + Clock: requestToJoin1.Clock, + EnsName: requestToJoin1.ENSName, + DisplayName: "Alice", + CommunityId: community.ID(), + } + + err = s.owner.HandleCommunityRequestToJoin(messageState, requestToJoinProto, &statusMessage) + s.Require().NoError(err) + ownerCommunity, err := s.owner.GetCommunityByID(community.ID()) + // Check Alice has successfully joined at owner side, Because message order was correct + s.Require().True(ownerCommunity.HasMember(s.alice.IdentityPublicKey())) + s.Require().NoError(err) + + s.Require().Len(response.Communities(), 1) + s.Require().Equal(response.Communities()[0].RequestedToJoinAt(), requestToJoin1.Clock) + + // pull all communities to make sure we set RequestedToJoinAt + + allCommunities, err := s.alice.Communities() + s.Require().NoError(err) + s.Require().Len(allCommunities, 2) + + if bytes.Equal(allCommunities[0].ID(), community.ID()) { + s.Require().Equal(allCommunities[0].RequestedToJoinAt(), requestToJoin1.Clock) + } else { + s.Require().Equal(allCommunities[1].RequestedToJoinAt(), requestToJoin1.Clock) + } + + // pull to make sure it has been saved + requestsToJoin, err := s.alice.MyPendingRequestsToJoin() + s.Require().NoError(err) + s.Require().Len(requestsToJoin, 1) + + // Make sure the requests are fetched also by community + requestsToJoin, err = s.alice.PendingRequestsToJoinForCommunity(community.ID()) + s.Require().NoError(err) + s.Require().Len(requestsToJoin, 1) + + requestToJoin2 := response.RequestsToJoinCommunity[0] + + s.Require().NotNil(requestToJoin2) + s.Require().Equal(community.ID(), requestToJoin2.CommunityID) + s.Require().NotEmpty(requestToJoin2.ID) + s.Require().NotEmpty(requestToJoin2.Clock) + s.Require().Equal(requestToJoin2.PublicKey, common.PubkeyToHex(&s.alice.identity.PublicKey)) + s.Require().Equal(communities.RequestToJoinStatePending, requestToJoin2.State) + + s.Require().Equal(requestToJoin1.ID, requestToJoin2.ID) + + requestToCancel := &requests.CancelRequestToJoinCommunity{ID: requestToJoin1.ID} + response, err = s.alice.CancelRequestToJoinCommunity(ctx, requestToCancel) + s.Require().NoError(err) + s.Require().NotNil(response) + s.Require().Len(response.RequestsToJoinCommunity, 1) + s.Require().Equal(communities.RequestToJoinStateCanceled, response.RequestsToJoinCommunity[0].State) + + messageState = s.alice.buildMessageState() + messageState.CurrentMessageState = &CurrentMessageState{} + + messageState.CurrentMessageState.PublicKey = &s.alice.identity.PublicKey + + statusMessage.TransportLayer.Dst = community.PublicKey() + + requestToJoinCancelProto := &protobuf.CommunityRequestToJoinResponse{ + CommunityId: community.ID(), + Clock: requestToJoin1.Clock + 1, + Accepted: true, + } + + err = s.alice.HandleCommunityRequestToJoinResponse(messageState, requestToJoinCancelProto, &statusMessage) + s.Require().NoError(err) + aliceJoinedCommunities, err := s.alice.JoinedCommunities() + s.Require().NoError(err) + // Make sure on Alice side she hasn't joined any communities + s.Require().Empty(aliceJoinedCommunities) + + // pull to make sure it has been saved + cancelRequestsToJoin, err := s.alice.MyCanceledRequestToJoinForCommunityID(community.ID()) + s.Require().NoError(err) + s.Require().NotNil(cancelRequestsToJoin) + s.Require().Equal(cancelRequestsToJoin.State, communities.RequestToJoinStateCanceled) + + s.Require().NoError(err) + + messageState = s.alice.buildMessageState() + messageState.CurrentMessageState = &CurrentMessageState{} + + messageState.CurrentMessageState.PublicKey = &s.alice.identity.PublicKey + + statusMessage.TransportLayer.Dst = community.PublicKey() + + requestToJoinResponseProto := &protobuf.CommunityRequestToJoinResponse{ + Clock: cancelRequestsToJoin.Clock, + CommunityId: community.ID(), + Accepted: true, + } + + err = s.alice.HandleCommunityRequestToJoinResponse(messageState, requestToJoinResponseProto, &statusMessage) + s.Require().NoError(err) + // Make sure alice is NOT a member of the community that she cancelled her request to join to + s.Require().False(community.HasMember(s.alice.IdentityPublicKey())) + // Make sure there are no AC notifications for Alice + aliceNotifications, err := s.alice.ActivityCenterNotifications(ActivityCenterNotificationsRequest{ + Cursor: "", + Limit: 10, + ActivityTypes: []ActivityCenterType{}, + ReadType: ActivityCenterQueryParamsReadUnread, + }) + s.Require().NoError(err) + s.Require().Len(aliceNotifications.Notifications, 0) + + // Retrieve activity center notifications for admin to make sure the request notification is deleted + notifications, err := s.owner.ActivityCenterNotifications(ActivityCenterNotificationsRequest{ + Cursor: "", + Limit: 10, + ActivityTypes: []ActivityCenterType{}, + ReadType: ActivityCenterQueryParamsReadUnread, + }) + + s.Require().NoError(err) + s.Require().Len(notifications.Notifications, 0) + cancelRequestToJoin2 := response.RequestsToJoinCommunity[0] + s.Require().NotNil(cancelRequestToJoin2) + s.Require().Equal(community.ID(), cancelRequestToJoin2.CommunityID) + s.Require().False(cancelRequestToJoin2.Our) + s.Require().NotEmpty(cancelRequestToJoin2.ID) + s.Require().NotEmpty(cancelRequestToJoin2.Clock) + s.Require().Equal(cancelRequestToJoin2.PublicKey, common.PubkeyToHex(&s.alice.identity.PublicKey)) +} diff --git a/protocol/messenger_communities.go b/protocol/messenger_communities.go index 2e76f3cff..8578ab245 100644 --- a/protocol/messenger_communities.go +++ b/protocol/messenger_communities.go @@ -2355,6 +2355,10 @@ func (m *Messenger) MyCanceledRequestsToJoin() ([]*communities.RequestToJoin, er return m.communitiesManager.CanceledRequestsToJoinForUser(&m.identity.PublicKey) } +func (m *Messenger) MyCanceledRequestToJoinForCommunityID(communityID []byte) (*communities.RequestToJoin, error) { + return m.communitiesManager.CanceledRequestToJoinForUserForCommunityID(&m.identity.PublicKey, communityID) +} + func (m *Messenger) MyPendingRequestsToJoin() ([]*communities.RequestToJoin, error) { return m.communitiesManager.PendingRequestsToJoinForUser(&m.identity.PublicKey) } diff --git a/protocol/messenger_handler.go b/protocol/messenger_handler.go index 7653682e1..353b62795 100644 --- a/protocol/messenger_handler.go +++ b/protocol/messenger_handler.go @@ -1596,6 +1596,16 @@ func (m *Messenger) HandleCommunityRequestToJoinResponse(state *ReceivedMessageS return ErrInvalidCommunityID } + myCancelledRequestToJoin, err := m.MyCanceledRequestToJoinForCommunityID(requestToJoinResponseProto.CommunityId) + + if err != nil { + return err + } + + if myCancelledRequestToJoin != nil { + return nil + } + updatedRequest, err := m.communitiesManager.HandleCommunityRequestToJoinResponse(signer, requestToJoinResponseProto) if err != nil { return err @@ -1683,7 +1693,7 @@ func (m *Messenger) HandleCommunityRequestToJoinResponse(state *ReceivedMessageS notification.UpdatedAt = m.GetCurrentTimeInMillis() err = m.addActivityCenterNotification(state.Response, notification, nil) if err != nil { - m.logger.Warn("failed to update notification", zap.Error(err)) + m.logger.Error("failed to update notification", zap.Error(err)) return err } }