refactor: simplify community requests logic

With the recent introduction of pending states, the community requests
logic became more complex. This commit simplifies the flow and
appropriately delegates logic to its corresponding abstraction levels:
messenger, manager and community. Additionally, it eliminates
redundancies in notifications and request-saving mechanism.
This commit is contained in:
Patryk Osmaczko 2023-10-12 23:42:03 +02:00 committed by osmaczko
parent b932cc97bb
commit a9cde06e44
6 changed files with 186 additions and 208 deletions

View File

@ -1021,12 +1021,19 @@ func (o *Community) ValidateRequestToJoin(signer *ecdsa.PublicKey, request *prot
o.mutex.Lock()
defer o.mutex.Unlock()
// If we are not admin, fuggetaboutit
if !o.IsControlNode() && !o.HasPermissionToSendCommunityEvents() {
if o.IsControlNode() {
// TODO: Enable this once mobile supports revealed addresses.
// if len(request.RevealedAccounts) == 0 {
// return errors.New("no addresses revealed")
// }
} else if o.HasPermissionToSendCommunityEvents() {
if o.AcceptRequestToJoinAutomatically() {
return errors.New("auto-accept community requests can only be processed by the control node")
}
} else {
return ErrNotAdmin
}
// If the org is ens name only, then reject if not present
if o.config.CommunityDescription.Permissions.EnsOnly && len(request.EnsName) == 0 {
return ErrCantRequestAccess
}
@ -1040,6 +1047,19 @@ func (o *Community) ValidateRequestToJoin(signer *ecdsa.PublicKey, request *prot
return err
}
if o.isBanned(signer) {
return ErrCantRequestAccess
}
timeNow := uint64(time.Now().Unix())
requestTimeOutClock, err := AddTimeoutToRequestToJoinClock(request.Clock)
if err != nil {
return err
}
if timeNow >= requestTimeOutClock {
return errors.New("request is expired")
}
return nil
}

View File

@ -338,21 +338,25 @@ func (s *CommunitySuite) TestValidateRequestToJoin() {
request := &protobuf.CommunityRequestToJoin{
EnsName: "donvanvliet.stateofus.eth",
CommunityId: s.communityID,
Clock: uint64(time.Now().Unix()),
}
requestWithChatID := &protobuf.CommunityRequestToJoin{
EnsName: "donvanvliet.stateofus.eth",
CommunityId: s.communityID,
ChatId: testChatID1,
Clock: uint64(time.Now().Unix()),
}
requestWithoutENS := &protobuf.CommunityRequestToJoin{
CommunityId: s.communityID,
Clock: uint64(time.Now().Unix()),
}
requestWithChatWithoutENS := &protobuf.CommunityRequestToJoin{
CommunityId: s.communityID,
ChatId: testChatID1,
Clock: uint64(time.Now().Unix()),
}
// MATRIX

View File

@ -2308,25 +2308,17 @@ func (m *Manager) DeclineRequestToJoin(dbRequest *RequestToJoin) (*Community, er
return community, nil
}
func (m *Manager) isUserRejectedFromCommunity(signer *ecdsa.PublicKey, community *Community, requestClock uint64) (bool, error) {
declinedRequestsToJoin, err := m.persistence.DeclinedRequestsToJoinForCommunity(community.ID())
func (m *Manager) shouldUserRetainDeclined(signer *ecdsa.PublicKey, community *Community, requestClock uint64) (bool, error) {
requestID := CalculateRequestID(common.PubkeyToHex(signer), types.HexBytes(community.IDString()))
request, err := m.persistence.GetRequestToJoin(requestID)
if err != nil {
if err == sql.ErrNoRows {
return false, nil
}
return false, err
}
for _, req := range declinedRequestsToJoin {
if req.PublicKey == common.PubkeyToHex(signer) {
dbRequestTimeOutClock, err := AddTimeoutToRequestToJoinClock(req.Clock)
if err != nil {
return false, err
}
if requestClock < dbRequestTimeOutClock {
return true, nil
}
}
}
return false, nil
return request.ShouldRetainDeclined(requestClock)
}
func (m *Manager) HandleCommunityCancelRequestToJoin(signer *ecdsa.PublicKey, request *protobuf.CommunityCancelRequestToJoin) (*RequestToJoin, error) {
@ -2338,11 +2330,11 @@ func (m *Manager) HandleCommunityCancelRequestToJoin(signer *ecdsa.PublicKey, re
return nil, ErrOrgNotFound
}
isUserRejected, err := m.isUserRejectedFromCommunity(signer, community, request.Clock)
retainDeclined, err := m.shouldUserRetainDeclined(signer, community, request.Clock)
if err != nil {
return nil, err
}
if isUserRejected {
if retainDeclined {
return nil, ErrCommunityRequestAlreadyRejected
}
@ -2359,41 +2351,18 @@ func (m *Manager) HandleCommunityCancelRequestToJoin(signer *ecdsa.PublicKey, re
return requestToJoin, nil
}
func (m *Manager) HandleCommunityRequestToJoin(signer *ecdsa.PublicKey, receiver *ecdsa.PublicKey, request *protobuf.CommunityRequestToJoin) (*RequestToJoin, error) {
func (m *Manager) HandleCommunityRequestToJoin(signer *ecdsa.PublicKey, receiver *ecdsa.PublicKey, request *protobuf.CommunityRequestToJoin) (*Community, *RequestToJoin, error) {
community, err := m.persistence.GetByID(&m.identity.PublicKey, request.CommunityId)
if err != nil {
return nil, err
return nil, nil, err
}
if community == nil {
return nil, ErrOrgNotFound
return nil, nil, ErrOrgNotFound
}
// don't process request as admin if community is configured as auto-accept
if !community.IsControlNode() && community.AcceptRequestToJoinAutomatically() {
return nil, nil
}
// control node must receive requests to join only on community address
// ignore duplicate messages sent to control node pubKey
if community.IsControlNode() && receiver.Equal(m.identity) {
return nil, errors.New("duplicate msg sent to the owner")
}
isUserRejected, err := m.isUserRejectedFromCommunity(signer, community, request.Clock)
err = community.ValidateRequestToJoin(signer, request)
if err != nil {
return nil, err
}
if isUserRejected {
return nil, ErrCommunityRequestAlreadyRejected
}
// Banned member can't request to join community
if community.isBanned(signer) {
return nil, ErrCantRequestAccess
}
if err := community.ValidateRequestToJoin(signer, request); err != nil {
return nil, err
return nil, nil, err
}
requestToJoin := &RequestToJoin{
@ -2404,57 +2373,73 @@ func (m *Manager) HandleCommunityRequestToJoin(signer *ecdsa.PublicKey, receiver
State: RequestToJoinStatePending,
RevealedAccounts: request.RevealedAccounts,
}
requestToJoin.CalculateID()
existingRequestToJoin, err := m.persistence.GetRequestToJoin(requestToJoin.ID)
if err != nil && err != sql.ErrNoRows {
return nil, err
return nil, nil, err
}
if existingRequestToJoin == nil || existingRequestToJoin.State == RequestToJoinStateCanceled {
if err := m.persistence.SaveRequestToJoin(requestToJoin); err != nil {
return nil, err
if existingRequestToJoin == nil {
err = m.SaveRequestToJoin(requestToJoin)
if err != nil {
return nil, nil, err
}
} else {
retainDeclined, err := existingRequestToJoin.ShouldRetainDeclined(request.Clock)
if err != nil {
return nil, nil, err
}
if retainDeclined {
return nil, nil, ErrCommunityRequestAlreadyRejected
}
switch existingRequestToJoin.State {
case RequestToJoinStatePending, RequestToJoinStateDeclined, RequestToJoinStateCanceled:
// Another request have been received, save request back to pending state
err = m.SaveRequestToJoin(requestToJoin)
if err != nil {
return nil, nil, err
}
}
}
if community.IsControlNode() {
// request to join was already processed by an admin and waits to get
// confirmation for its decision
//
// we're only interested in immediately declining any declined/pending
// requests here, because if it's accepted/pending, we still need to perform
// some checks
if existingRequestToJoin != nil && existingRequestToJoin.State == RequestToJoinStateDeclinedPending {
requestToJoin.State = RequestToJoinStateDeclined
return requestToJoin, nil
}
if len(request.RevealedAccounts) > 0 {
// verify if revealed addresses indeed belong to requester
for _, revealedAccount := range request.RevealedAccounts {
recoverParams := account.RecoverParams{
Message: types.EncodeHex(crypto.Keccak256(crypto.CompressPubkey(signer), community.ID(), requestToJoin.ID)),
Signature: types.EncodeHex(revealedAccount.Signature),
}
matching, err := m.accountsManager.CanRecover(recoverParams, types.HexToAddress(revealedAccount.Address))
if err != nil {
return nil, err
}
if !matching {
// if ownership of only one wallet address cannot be verified,
// we mark the request as cancelled and stop
requestToJoin.State = RequestToJoinStateDeclined
return requestToJoin, nil
}
// verify if revealed addresses indeed belong to requester
for _, revealedAccount := range request.RevealedAccounts {
recoverParams := account.RecoverParams{
Message: types.EncodeHex(crypto.Keccak256(crypto.CompressPubkey(signer), community.ID(), requestToJoin.ID)),
Signature: types.EncodeHex(revealedAccount.Signature),
}
// Save revealed addresses + signatures so they can later be added
// to the control node's local table of known revealed addresses
err = m.persistence.SaveRequestToJoinRevealedAddresses(requestToJoin.ID, requestToJoin.RevealedAccounts)
matching, err := m.accountsManager.CanRecover(recoverParams, types.HexToAddress(revealedAccount.Address))
if err != nil {
return nil, err
return nil, nil, err
}
if !matching {
// if ownership of only one wallet address cannot be verified,
// we mark the request as cancelled and stop
requestToJoin.State = RequestToJoinStateDeclined
return community, requestToJoin, nil
}
}
// Save revealed addresses + signatures so they can later be added
// to the control node's local table of known revealed addresses
err = m.persistence.SaveRequestToJoinRevealedAddresses(requestToJoin.ID, requestToJoin.RevealedAccounts)
if err != nil {
return nil, nil, err
}
if existingRequestToJoin != nil {
// request to join was already processed by privileged user
// and waits to get confirmation for its decision
if existingRequestToJoin.State == RequestToJoinStateDeclinedPending {
requestToJoin.State = RequestToJoinStateDeclined
return community, requestToJoin, nil
} else if existingRequestToJoin.State == RequestToJoinStateAcceptedPending {
requestToJoin.State = RequestToJoinStateAccepted
return community, requestToJoin, nil
}
}
@ -2462,23 +2447,15 @@ func (m *Manager) HandleCommunityRequestToJoin(signer *ecdsa.PublicKey, receiver
// 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
acceptAutomatically := community.AcceptRequestToJoinAutomatically() || community.HasMember(signer)
// If the request to join was already accepted by another admin,
// we mark it as accepted so it won't be in pending state, even if the community
// is not set to auto-accept
acceptedByAdmin := existingRequestToJoin != nil && existingRequestToJoin.State == RequestToJoinStateAcceptedPending
if acceptAutomatically || acceptedByAdmin {
err = m.markRequestToJoinAsAccepted(signer, community)
if err != nil {
return nil, err
}
if acceptAutomatically {
// Don't check permissions here,
// it will be done further in the processing pipeline.
requestToJoin.State = RequestToJoinStateAccepted
return requestToJoin, nil
return community, requestToJoin, nil
}
}
return requestToJoin, nil
return community, requestToJoin, nil
}
func (m *Manager) HandleCommunityEditSharedAddresses(signer *ecdsa.PublicKey, request *protobuf.CommunityEditSharedAddresses) error {

View File

@ -74,6 +74,19 @@ func (r *RequestToJoin) Empty() bool {
return len(r.ID)+len(r.PublicKey)+int(r.Clock)+len(r.ENSName)+len(r.ChatID)+len(r.CommunityID)+int(r.State) == 0
}
func (r *RequestToJoin) ShouldRetainDeclined(clock uint64) (bool, error) {
if r.State != RequestToJoinStateDeclined {
return false, nil
}
declineExpiryClock, err := AddTimeoutToRequestToJoinClock(r.Clock)
if err != nil {
return false, err
}
return clock < declineExpiryClock, nil
}
func AddTimeoutToRequestToJoinClock(clock uint64) (uint64, error) {
requestToJoinClock, err := strconv.ParseInt(fmt.Sprint(clock), 10, 64)
if err != nil {

View File

@ -1500,16 +1500,7 @@ func (m *Messenger) CancelRequestToJoinCommunity(request *requests.CancelRequest
return response, nil
}
func (m *Messenger) AcceptRequestToJoinCommunity(request *requests.AcceptRequestToJoinCommunity) (*MessengerResponse, error) {
if err := request.Validate(); err != nil {
return nil, err
}
requestToJoin, err := m.communitiesManager.GetRequestToJoin(request.ID)
if err != nil {
return nil, err
}
func (m *Messenger) acceptRequestToJoinCommunity(requestToJoin *communities.RequestToJoin) (*MessengerResponse, error) {
community, err := m.communitiesManager.AcceptRequestToJoin(requestToJoin)
if err != nil {
return nil, err
@ -1577,12 +1568,11 @@ func (m *Messenger) AcceptRequestToJoinCommunity(request *requests.AcceptRequest
response.AddCommunity(community)
response.AddRequestToJoinCommunity(requestToJoin)
// Activity Center notification
notification, err := m.persistence.GetActivityCenterNotificationByID(request.ID)
// Update existing notification
notification, err := m.persistence.GetActivityCenterNotificationByID(requestToJoin.ID)
if err != nil {
return nil, err
}
if notification != nil {
notification.MembershipStatus = ActivityCenterMembershipStatusAccepted
if community.HasPermissionToSendCommunityEvents() {
@ -1602,17 +1592,21 @@ func (m *Messenger) AcceptRequestToJoinCommunity(request *requests.AcceptRequest
return response, nil
}
func (m *Messenger) DeclineRequestToJoinCommunity(request *requests.DeclineRequestToJoinCommunity) (*MessengerResponse, error) {
func (m *Messenger) AcceptRequestToJoinCommunity(request *requests.AcceptRequestToJoinCommunity) (*MessengerResponse, error) {
if err := request.Validate(); err != nil {
return nil, err
}
dbRequest, err := m.communitiesManager.GetRequestToJoin(request.ID)
requestToJoin, err := m.communitiesManager.GetRequestToJoin(request.ID)
if err != nil {
return nil, err
}
community, err := m.communitiesManager.DeclineRequestToJoin(dbRequest)
return m.acceptRequestToJoinCommunity(requestToJoin)
}
func (m *Messenger) declineRequestToJoinCommunity(requestToJoin *communities.RequestToJoin) (*MessengerResponse, error) {
community, err := m.communitiesManager.DeclineRequestToJoin(requestToJoin)
if err != nil {
return nil, err
}
@ -1620,9 +1614,9 @@ func (m *Messenger) DeclineRequestToJoinCommunity(request *requests.DeclineReque
if community.IsControlNode() {
// Notify privileged members that request to join was rejected
// Send request to join without revealed addresses
dbRequest.RevealedAccounts = make([]*protobuf.RevealedAccount, 0)
requestToJoin.RevealedAccounts = make([]*protobuf.RevealedAccount, 0)
declinedRequestsToJoin := make(map[string]*protobuf.CommunityRequestToJoin)
declinedRequestsToJoin[dbRequest.PublicKey] = dbRequest.ToCommunityRequestToJoinProtobuf()
declinedRequestsToJoin[requestToJoin.PublicKey] = requestToJoin.ToCommunityRequestToJoinProtobuf()
syncMsg := &protobuf.CommunityPrivilegedUserSyncMessage{
Type: protobuf.CommunityPrivilegedUserSyncMessage_CONTROL_NODE_REJECT_REQUEST_TO_JOIN,
@ -1654,25 +1648,16 @@ func (m *Messenger) DeclineRequestToJoinCommunity(request *requests.DeclineReque
}
}
// Activity Center notification
notification, err := m.persistence.GetActivityCenterNotificationByID(request.ID)
response := &MessengerResponse{}
response.AddCommunity(community)
response.AddRequestToJoinCommunity(requestToJoin)
// Update existing notification
notification, err := m.persistence.GetActivityCenterNotificationByID(requestToJoin.ID)
if err != nil {
return nil, err
}
response := &MessengerResponse{}
dbRequest, err = m.communitiesManager.GetRequestToJoin(request.ID)
response.AddRequestToJoinCommunity(dbRequest)
if notification != nil {
if err != nil {
return nil, err
}
community, err := m.communitiesManager.GetByID(dbRequest.CommunityID)
if err != nil {
return nil, err
}
notification.MembershipStatus = ActivityCenterMembershipStatusDeclined
if community.HasPermissionToSendCommunityEvents() {
notification.MembershipStatus = ActivityCenterMembershipStatusDeclinedPending
@ -1691,6 +1676,19 @@ func (m *Messenger) DeclineRequestToJoinCommunity(request *requests.DeclineReque
return response, nil
}
func (m *Messenger) DeclineRequestToJoinCommunity(request *requests.DeclineRequestToJoinCommunity) (*MessengerResponse, error) {
if err := request.Validate(); err != nil {
return nil, err
}
requestToJoin, err := m.communitiesManager.GetRequestToJoin(request.ID)
if err != nil {
return nil, err
}
return m.declineRequestToJoinCommunity(requestToJoin)
}
func (m *Messenger) LeaveCommunity(communityID types.HexBytes) (*MessengerResponse, error) {
notifications, err := m.persistence.DismissAllActivityCenterNotificationsFromCommunity(communityID.String(), m.getCurrentTimeInMillis())
if err != nil {

View File

@ -1465,76 +1465,22 @@ func (m *Messenger) HandleCommunityCancelRequestToJoin(state *ReceivedMessageSta
// HandleCommunityRequestToJoin handles an community request to join
func (m *Messenger) HandleCommunityRequestToJoin(state *ReceivedMessageState, requestToJoinProto *protobuf.CommunityRequestToJoin, statusMessage *v1protocol.StatusMessage) error {
if requestToJoinProto.CommunityId == nil {
return ErrInvalidCommunityID
}
timeNow := uint64(time.Now().Unix())
requestTimeOutClock, err := communities.AddTimeoutToRequestToJoinClock(requestToJoinProto.Clock)
if err != nil {
return err
}
if timeNow >= requestTimeOutClock {
return errors.New("request is expired")
}
signer := state.CurrentMessageState.PublicKey
receiver := statusMessage.Dst
requestToJoin, err := m.communitiesManager.HandleCommunityRequestToJoin(signer, receiver, requestToJoinProto)
if err != nil {
return err
}
// not interested, stop further processing
if requestToJoin == nil {
return nil
}
if requestToJoin.State == communities.RequestToJoinStateAccepted {
accept := &requests.AcceptRequestToJoinCommunity{
ID: requestToJoin.ID,
}
_, err = m.AcceptRequestToJoinCommunity(accept)
if err != nil {
if err == communities.ErrNoPermissionToJoin {
// only control node will end up here as it's the only one that
// performed token permission checks
requestToJoin.State = communities.RequestToJoinStateDeclined
} else {
return err
}
}
}
if requestToJoin.State == communities.RequestToJoinStateDeclined {
cancel := &requests.DeclineRequestToJoinCommunity{
ID: requestToJoin.ID,
}
_, err = m.DeclineRequestToJoinCommunity(cancel)
if err != nil {
return err
}
return nil
}
community, err := m.communitiesManager.GetByID(requestToJoinProto.CommunityId)
community, requestToJoin, err := m.communitiesManager.HandleCommunityRequestToJoin(signer, statusMessage.Dst, requestToJoinProto)
if err != nil {
return err
}
contactID := contactIDFromPublicKey(signer)
switch requestToJoin.State {
case communities.RequestToJoinStatePending:
contact, _ := state.AllContacts.Load(contactIDFromPublicKey(signer))
if len(requestToJoinProto.DisplayName) != 0 {
contact.DisplayName = requestToJoinProto.DisplayName
state.ModifiedContacts.Store(contact.ID, true)
state.AllContacts.Store(contact.ID, contact)
state.ModifiedContacts.Store(contact.ID, true)
}
contact, _ := state.AllContacts.Load(contactID)
if len(requestToJoinProto.DisplayName) != 0 {
contact.DisplayName = requestToJoinProto.DisplayName
state.ModifiedContacts.Store(contact.ID, true)
state.AllContacts.Store(contact.ID, contact)
state.ModifiedContacts.Store(contact.ID, true)
}
if requestToJoin.State == communities.RequestToJoinStatePending {
if state.Response.RequestsToJoinCommunity == nil {
state.Response.RequestsToJoinCommunity = make([]*communities.RequestToJoin, 0)
}
@ -1553,32 +1499,52 @@ func (m *Messenger) HandleCommunityRequestToJoin(state *ReceivedMessageState, re
Deleted: false,
UpdatedAt: m.getCurrentTimeInMillis(),
}
err = m.addActivityCenterNotification(state.Response, notification)
if err != nil {
m.logger.Error("failed to save notification", zap.Error(err))
return err
}
} else {
// Activity Center notification, updating existing for accepted/declined
notification, err := m.persistence.GetActivityCenterNotificationByID(requestToJoin.ID)
if err != nil {
case communities.RequestToJoinStateDeclined:
response, err := m.declineRequestToJoinCommunity(requestToJoin)
if err == nil {
err := state.Response.Merge(response)
if err != nil {
return err
}
} else {
return err
}
if notification != nil {
if requestToJoin.State == communities.RequestToJoinStateAccepted {
notification.MembershipStatus = ActivityCenterMembershipStatusAccepted
} else {
notification.MembershipStatus = ActivityCenterMembershipStatusDeclined
}
notification.UpdatedAt = m.getCurrentTimeInMillis()
err = m.addActivityCenterNotification(state.Response, notification)
case communities.RequestToJoinStateAccepted:
response, err := m.acceptRequestToJoinCommunity(requestToJoin)
if err == nil {
err := state.Response.Merge(response) // new member has been added
if err != nil {
m.logger.Error("failed to save notification", zap.Error(err))
return err
}
} else if err == communities.ErrNoPermissionToJoin {
// only control node will end up here as it's the only one that
// performed token permission checks
response, err = m.declineRequestToJoinCommunity(requestToJoin)
if err == nil {
err := state.Response.Merge(response)
if err != nil {
return err
}
} else {
return err
}
} else {
return err
}
case communities.RequestToJoinStateCanceled:
// cancellation is handled by separate message
fallthrough
case communities.RequestToJoinStateAcceptedPending, communities.RequestToJoinStateDeclinedPending:
// request can be marked as pending only manually
return errors.New("invalid request state")
}
return nil