feat_: limit number of members in a community and number of pending requests (#5107)
* feat(community): limit nb of requests to join and members Needed for https://github.com/status-im/status-desktop/issues/14532 * chore: simplify TestRequestAccessAgain * chore: add a test for the member limit
This commit is contained in:
parent
2fb6d615fd
commit
852a5beb39
|
@ -1001,6 +1001,13 @@ func (o *Community) Edit(description *protobuf.CommunityDescription) {
|
|||
o.config.CommunityDescription.AdminSettings.PinMessageAllMembersEnabled = description.AdminSettings.PinMessageAllMembersEnabled
|
||||
}
|
||||
|
||||
func (o *Community) EditPermissionAccess(permissionAccess protobuf.CommunityPermissions_Access) {
|
||||
o.config.CommunityDescription.Permissions.Access = permissionAccess
|
||||
if o.IsControlNode() {
|
||||
o.increaseClock()
|
||||
}
|
||||
}
|
||||
|
||||
func (o *Community) Join() {
|
||||
o.config.Joined = true
|
||||
o.config.JoinedAt = time.Now().Unix()
|
||||
|
|
|
@ -59,6 +59,9 @@ var pieceLength = 100 * 1024
|
|||
|
||||
const maxArchiveSizeInBytes = 30000000
|
||||
|
||||
var maxNbMembers = 5000
|
||||
var maxNbPendingRequestedMembers = 100
|
||||
|
||||
var memberPermissionsCheckInterval = 1 * time.Hour
|
||||
var validateInterval = 2 * time.Minute
|
||||
|
||||
|
@ -66,6 +69,12 @@ var validateInterval = 2 * time.Minute
|
|||
func SetValidateInterval(duration time.Duration) {
|
||||
validateInterval = duration
|
||||
}
|
||||
func SetMaxNbMembers(maxNb int) {
|
||||
maxNbMembers = maxNb
|
||||
}
|
||||
func SetMaxNbPendingRequestedMembers(maxNb int) {
|
||||
maxNbPendingRequestedMembers = maxNb
|
||||
}
|
||||
|
||||
// errors
|
||||
var (
|
||||
|
@ -1293,7 +1302,7 @@ func (m *Manager) EditCommunity(request *requests.EditCommunity) (*Community, er
|
|||
|
||||
newDescription, err := request.ToCommunityDescription()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Can't create community description: %v", err)
|
||||
return nil, fmt.Errorf("can't create community description: %v", err)
|
||||
}
|
||||
|
||||
// If permissions weren't explicitly set on original request, use existing ones
|
||||
|
@ -2645,6 +2654,14 @@ func (m *Manager) HandleCommunityRequestToJoin(signer *ecdsa.PublicKey, receiver
|
|||
return nil, nil, err
|
||||
}
|
||||
|
||||
nbPendingRequestsToJoin, err := m.persistence.GetNumberOfPendingRequestsToJoin(community.ID())
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if nbPendingRequestsToJoin >= maxNbPendingRequestedMembers {
|
||||
return nil, nil, errors.New("max number of requests to join reached")
|
||||
}
|
||||
|
||||
requestToJoin := &RequestToJoin{
|
||||
PublicKey: common.PubkeyToHex(signer),
|
||||
Clock: request.Clock,
|
||||
|
@ -2737,6 +2754,15 @@ func (m *Manager) HandleCommunityRequestToJoin(signer *ecdsa.PublicKey, receiver
|
|||
}
|
||||
}
|
||||
|
||||
// Check if we reached the limit, if we did, change the community setting to be On Request
|
||||
if community.AutoAccept() && community.MembersCount() >= maxNbMembers {
|
||||
community.EditPermissionAccess(protobuf.CommunityPermissions_MANUAL_ACCEPT)
|
||||
err = m.saveAndPublish(community)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// If user is already a member, then accept request automatically
|
||||
// It may happen when member removes itself from community and then tries to rejoin
|
||||
// More specifically, CommunityRequestToLeave may be delivered later than CommunityRequestToJoin, or not delivered at all
|
||||
|
|
|
@ -842,6 +842,15 @@ func (p *Persistence) GetRequestToJoin(id []byte) (*RequestToJoin, error) {
|
|||
return request, nil
|
||||
}
|
||||
|
||||
func (p *Persistence) GetNumberOfPendingRequestsToJoin(communityID types.HexBytes) (int, error) {
|
||||
var count int
|
||||
err := p.db.QueryRow(`SELECT count(1) FROM communities_requests_to_join WHERE community_id = ? AND state = ?`, communityID, RequestToJoinStatePending).Scan(&count)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (p *Persistence) GetRequestToJoinByPkAndCommunityID(pk string, communityID []byte) (*RequestToJoin, error) {
|
||||
request := &RequestToJoin{}
|
||||
err := p.db.QueryRow(`SELECT id,public_key,clock,ens_name,customization_color,chat_id,community_id,state FROM communities_requests_to_join WHERE public_key = ? AND community_id = ?`, pk, communityID).Scan(&request.ID, &request.PublicKey, &request.Clock, &request.ENSName, &request.CustomizationColor, &request.ChatID, &request.CommunityID, &request.State)
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
@ -1658,31 +1659,7 @@ func (s *MessengerCommunitiesSuite) TestRequestAccessAgain() {
|
|||
|
||||
community := response.Communities()[0]
|
||||
|
||||
chat := CreateOneToOneChat(common.PubkeyToHex(&s.alice.identity.PublicKey), &s.alice.identity.PublicKey, s.alice.transport)
|
||||
|
||||
s.Require().NoError(s.bob.SaveChat(chat))
|
||||
|
||||
message := buildTestMessage(*chat)
|
||||
message.CommunityID = community.IDString()
|
||||
|
||||
// We send a community link to alice
|
||||
response, err = s.bob.SendChatMessage(context.Background(), message)
|
||||
s.Require().NoError(err)
|
||||
s.Require().NotNil(response)
|
||||
|
||||
// Retrieve community link & community
|
||||
err = tt.RetryWithBackOff(func() error {
|
||||
response, err = s.alice.RetrieveAll()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(response.Communities()) == 0 {
|
||||
return errors.New("message not received")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
s.Require().NoError(err)
|
||||
s.advertiseCommunityTo(community, s.bob, s.alice)
|
||||
|
||||
request := &requests.RequestToJoinCommunity{CommunityID: community.ID()}
|
||||
// We try to join the org
|
||||
|
@ -3514,6 +3491,82 @@ func (s *MessengerCommunitiesSuite) TestCommunityBanUserRequestToJoin() {
|
|||
s.Require().ErrorContains(err, "can't request access")
|
||||
}
|
||||
|
||||
func (s *MessengerCommunitiesSuite) TestCommunityMaxNumberOfMembers() {
|
||||
john := s.newMessenger()
|
||||
_, err := john.Start()
|
||||
s.Require().NoError(err)
|
||||
|
||||
defer TearDownMessenger(&s.Suite, john)
|
||||
|
||||
// Bring back the original values
|
||||
defer communities.SetMaxNbMembers(5000)
|
||||
defer communities.SetMaxNbPendingRequestedMembers(100)
|
||||
|
||||
community, _ := s.createCommunity()
|
||||
|
||||
communities.SetMaxNbMembers(2)
|
||||
communities.SetMaxNbPendingRequestedMembers(1)
|
||||
|
||||
s.advertiseCommunityTo(community, s.owner, s.alice)
|
||||
s.advertiseCommunityTo(community, s.owner, s.bob)
|
||||
s.advertiseCommunityTo(community, s.owner, john)
|
||||
|
||||
// Alice joins the community correctly
|
||||
s.joinCommunity(community, s.owner, s.alice)
|
||||
|
||||
// Bob also tries to join, but he will be put in the requests to join to approve and won't join
|
||||
request := &requests.RequestToJoinCommunity{CommunityID: community.ID()}
|
||||
response, err := s.bob.RequestToJoinCommunity(request)
|
||||
s.Require().NoError(err)
|
||||
s.Require().NotNil(response)
|
||||
s.Require().Len(response.RequestsToJoinCommunity(), 1)
|
||||
requestID := response.RequestsToJoinCommunity()[0].ID
|
||||
|
||||
response, err = WaitOnMessengerResponse(
|
||||
s.owner,
|
||||
func(r *MessengerResponse) bool {
|
||||
for _, req := range r.RequestsToJoinCommunity() {
|
||||
if reflect.DeepEqual(req.ID, requestID) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
},
|
||||
"no request to join",
|
||||
)
|
||||
s.Require().NoError(err)
|
||||
s.Require().Len(response.RequestsToJoinCommunity(), 1)
|
||||
s.Require().Equal(communities.RequestToJoinStatePending, response.RequestsToJoinCommunity()[0].State)
|
||||
|
||||
// We confirm that there are still 2 members only and the access setting is now manual
|
||||
updatedCommunity, err := s.owner.communitiesManager.GetByID(community.ID())
|
||||
s.Require().NoError(err)
|
||||
s.Require().Len(updatedCommunity.Members(), 2)
|
||||
s.Require().Equal(protobuf.CommunityPermissions_MANUAL_ACCEPT, updatedCommunity.Permissions().Access)
|
||||
|
||||
// John also tries to join, but he his request will be ignored as it exceeds the max number of pending requests
|
||||
requestJohn := &requests.RequestToJoinCommunity{CommunityID: community.ID()}
|
||||
response, err = john.RequestToJoinCommunity(requestJohn)
|
||||
s.Require().NoError(err)
|
||||
s.Require().NotNil(response)
|
||||
s.Require().Len(response.RequestsToJoinCommunity(), 1)
|
||||
requestJohnID := response.RequestsToJoinCommunity()[0].ID
|
||||
|
||||
_, err = WaitOnMessengerResponse(
|
||||
s.owner,
|
||||
func(r *MessengerResponse) bool {
|
||||
for _, req := range r.RequestsToJoinCommunity() {
|
||||
if reflect.DeepEqual(req.ID, requestJohnID) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
},
|
||||
"no request to join",
|
||||
)
|
||||
s.Require().Error(err)
|
||||
}
|
||||
|
||||
func (s *MessengerCommunitiesSuite) TestHandleImport() {
|
||||
community, chat := s.createCommunity()
|
||||
|
||||
|
|
Loading…
Reference in New Issue