Fix pending join requests + API to get them (#3902)
Needed for https://github.com/status-im/status-desktop/issues/11851
This commit is contained in:
parent
5272f99b59
commit
3bf0bed78d
|
@ -310,6 +310,7 @@ type Subscription struct {
|
|||
type CommunityResponse struct {
|
||||
Community *Community `json:"community"`
|
||||
Changes *CommunityChanges `json:"changes"`
|
||||
RequestsToJoin []*RequestToJoin `json:"requestsToJoin"`
|
||||
}
|
||||
|
||||
type CommunityEventsMessageInvalidClockSignal struct {
|
||||
|
@ -1422,7 +1423,7 @@ func (m *Manager) HandleCommunityEventsMessage(signer *ecdsa.PublicKey, message
|
|||
return nil, err
|
||||
}
|
||||
|
||||
err = m.handleAdditionalAdminChanges(changes.Community)
|
||||
additionalCommunityResponse, err := m.handleAdditionalAdminChanges(changes.Community)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -1451,6 +1452,7 @@ func (m *Manager) HandleCommunityEventsMessage(signer *ecdsa.PublicKey, message
|
|||
return &CommunityResponse{
|
||||
Community: changes.Community,
|
||||
Changes: changes,
|
||||
RequestsToJoin: additionalCommunityResponse.RequestsToJoin,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -1502,37 +1504,7 @@ func (m *Manager) HandleCommunityEventsMessageRejected(signer *ecdsa.PublicKey,
|
|||
return reapplyEventsMessage, nil
|
||||
}
|
||||
|
||||
func (m *Manager) HandleCommunityPrivilegedUserSyncMessage(signer *ecdsa.PublicKey, message *protobuf.CommunityPrivilegedUserSyncMessage) error {
|
||||
if signer == nil {
|
||||
return errors.New("signer can't be nil")
|
||||
}
|
||||
|
||||
community, err := m.persistence.GetByID(&m.identity.PublicKey, message.CommunityId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if community == nil {
|
||||
return ErrOrgNotFound
|
||||
}
|
||||
|
||||
if !community.IsPrivilegedMember(&m.identity.PublicKey) {
|
||||
return errors.New("user has no permissions to process privileged sync message")
|
||||
}
|
||||
|
||||
err = validateCommunityPrivilegedUserSyncMessage(message)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch message.Type {
|
||||
case protobuf.CommunityPrivilegedUserSyncMessage_CONTROL_NODE_ACCEPT_REQUEST_TO_JOIN:
|
||||
fallthrough
|
||||
case protobuf.CommunityPrivilegedUserSyncMessage_CONTROL_NODE_REJECT_REQUEST_TO_JOIN:
|
||||
if !common.IsPubKeyEqual(community.PublicKey(), signer) {
|
||||
return errors.New("accepted/requested to join sync messages can be send only by the control node")
|
||||
}
|
||||
|
||||
func (m *Manager) HandleRequestToJoinPrivilegedUserSyncMessage(message *protobuf.CommunityPrivilegedUserSyncMessage, communityID types.HexBytes) ([]*RequestToJoin, error) {
|
||||
var state RequestToJoinState
|
||||
if message.Type == protobuf.CommunityPrivilegedUserSyncMessage_CONTROL_NODE_ACCEPT_REQUEST_TO_JOIN {
|
||||
state = RequestToJoinStateAccepted
|
||||
|
@ -1540,6 +1512,7 @@ func (m *Manager) HandleCommunityPrivilegedUserSyncMessage(signer *ecdsa.PublicK
|
|||
state = RequestToJoinStateDeclined
|
||||
}
|
||||
|
||||
requestsToJoin := make([]*RequestToJoin, 0)
|
||||
for signer, requestToJoinProto := range message.RequestToJoin {
|
||||
requestToJoin := &RequestToJoin{
|
||||
PublicKey: signer,
|
||||
|
@ -1550,12 +1523,17 @@ func (m *Manager) HandleCommunityPrivilegedUserSyncMessage(signer *ecdsa.PublicK
|
|||
}
|
||||
requestToJoin.CalculateID()
|
||||
|
||||
_, err := m.saveOrUpdateRequestToJoin(signer, community.ID(), requestToJoin)
|
||||
_, err := m.saveOrUpdateRequestToJoin(signer, communityID, requestToJoin)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
requestsToJoin = append(requestsToJoin, requestToJoin)
|
||||
}
|
||||
case protobuf.CommunityPrivilegedUserSyncMessage_ADD_COMMUNITY_TOKENS:
|
||||
|
||||
return requestsToJoin, nil
|
||||
}
|
||||
|
||||
func (m *Manager) HandleAddCommunityTokenPrivilegedUserSyncMessage(message *protobuf.CommunityPrivilegedUserSyncMessage, community *Community) error {
|
||||
for _, token := range message.CommunityTokens {
|
||||
token := community_token.FromCommunityTokenProtobuf(token)
|
||||
if !community.MemberCanManageToken(community.MemberIdentity(), token) {
|
||||
|
@ -1571,38 +1549,44 @@ func (m *Manager) HandleCommunityPrivilegedUserSyncMessage(signer *ecdsa.PublicK
|
|||
return m.persistence.AddCommunityToken(token)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) handleAdditionalAdminChanges(community *Community) error {
|
||||
func (m *Manager) handleAdditionalAdminChanges(community *Community) (*CommunityResponse, error) {
|
||||
communityResponse := CommunityResponse{
|
||||
RequestsToJoin: make([]*RequestToJoin, 0),
|
||||
}
|
||||
|
||||
if !(community.IsControlNode() || community.HasPermissionToSendCommunityEvents()) {
|
||||
// we're a normal user/member node, so there's nothing for us to do here
|
||||
return nil
|
||||
return &communityResponse, nil
|
||||
}
|
||||
|
||||
for i := range community.config.EventsData.Events {
|
||||
communityEvent := &community.config.EventsData.Events[i]
|
||||
switch communityEvent.Type {
|
||||
case protobuf.CommunityEvent_COMMUNITY_REQUEST_TO_JOIN_ACCEPT:
|
||||
err := m.handleCommunityEventRequestAccepted(community, communityEvent)
|
||||
requestsToJoin, err := m.handleCommunityEventRequestAccepted(community, communityEvent)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
if requestsToJoin != nil {
|
||||
communityResponse.RequestsToJoin = append(communityResponse.RequestsToJoin, requestsToJoin...)
|
||||
}
|
||||
|
||||
case protobuf.CommunityEvent_COMMUNITY_REQUEST_TO_JOIN_REJECT:
|
||||
err := m.handleCommunityEventRequestRejected(community, communityEvent)
|
||||
requestsToJoin, err := m.handleCommunityEventRequestRejected(community, communityEvent)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
if requestsToJoin != nil {
|
||||
communityResponse.RequestsToJoin = append(communityResponse.RequestsToJoin, requestsToJoin...)
|
||||
}
|
||||
|
||||
default:
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return &communityResponse, nil
|
||||
}
|
||||
|
||||
func (m *Manager) saveOrUpdateRequestToJoin(signer string, communityID types.HexBytes, requestToJoin *RequestToJoin) (bool, error) {
|
||||
|
@ -1636,9 +1620,11 @@ func (m *Manager) saveOrUpdateRequestToJoin(signer string, communityID types.Hex
|
|||
return updated, nil
|
||||
}
|
||||
|
||||
func (m *Manager) handleCommunityEventRequestAccepted(community *Community, communityEvent *CommunityEvent) error {
|
||||
func (m *Manager) handleCommunityEventRequestAccepted(community *Community, communityEvent *CommunityEvent) ([]*RequestToJoin, error) {
|
||||
acceptedRequestsToJoin := make([]types.HexBytes, 0)
|
||||
|
||||
requestsToJoin := make([]*RequestToJoin, 0)
|
||||
|
||||
for signer, request := range communityEvent.AcceptedRequestsToJoin {
|
||||
requestToJoin := &RequestToJoin{
|
||||
PublicKey: signer,
|
||||
|
@ -1651,7 +1637,7 @@ func (m *Manager) handleCommunityEventRequestAccepted(community *Community, comm
|
|||
|
||||
existingRequestToJoin, err := m.persistence.GetRequestToJoin(requestToJoin.ID)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if community.IsControlNode() {
|
||||
|
@ -1670,7 +1656,7 @@ func (m *Manager) handleCommunityEventRequestAccepted(community *Community, comm
|
|||
|
||||
requestUpdated, err := m.saveOrUpdateRequestToJoin(signer, community.ID(), requestToJoin)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if community.IsControlNode() && requestUpdated {
|
||||
|
@ -1680,16 +1666,20 @@ func (m *Manager) handleCommunityEventRequestAccepted(community *Community, comm
|
|||
// admin nodes), so we don't want to trigger an `AcceptRequestToJoin` in such cases.
|
||||
acceptedRequestsToJoin = append(acceptedRequestsToJoin, requestToJoin.ID)
|
||||
}
|
||||
|
||||
requestsToJoin = append(requestsToJoin, requestToJoin)
|
||||
}
|
||||
if community.IsControlNode() {
|
||||
m.publish(&Subscription{AcceptedRequestsToJoin: acceptedRequestsToJoin})
|
||||
}
|
||||
return nil
|
||||
return requestsToJoin, nil
|
||||
}
|
||||
|
||||
func (m *Manager) handleCommunityEventRequestRejected(community *Community, communityEvent *CommunityEvent) error {
|
||||
func (m *Manager) handleCommunityEventRequestRejected(community *Community, communityEvent *CommunityEvent) ([]*RequestToJoin, error) {
|
||||
rejectedRequestsToJoin := make([]types.HexBytes, 0)
|
||||
|
||||
requestsToJoin := make([]*RequestToJoin, 0)
|
||||
|
||||
for signer, request := range communityEvent.RejectedRequestsToJoin {
|
||||
requestToJoin := &RequestToJoin{
|
||||
PublicKey: signer,
|
||||
|
@ -1702,7 +1692,7 @@ func (m *Manager) handleCommunityEventRequestRejected(community *Community, comm
|
|||
|
||||
existingRequestToJoin, err := m.persistence.GetRequestToJoin(requestToJoin.ID)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if community.IsControlNode() {
|
||||
|
@ -1721,17 +1711,19 @@ func (m *Manager) handleCommunityEventRequestRejected(community *Community, comm
|
|||
|
||||
requestUpdated, err := m.saveOrUpdateRequestToJoin(signer, community.ID(), requestToJoin)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
if community.IsControlNode() && requestUpdated {
|
||||
rejectedRequestsToJoin = append(rejectedRequestsToJoin, requestToJoin.ID)
|
||||
}
|
||||
|
||||
requestsToJoin = append(requestsToJoin, requestToJoin)
|
||||
}
|
||||
|
||||
if community.IsControlNode() {
|
||||
m.publish(&Subscription{RejectedRequestsToJoin: rejectedRequestsToJoin})
|
||||
}
|
||||
return nil
|
||||
return requestsToJoin, nil
|
||||
}
|
||||
|
||||
// markRequestToJoin marks all the pending requests to join as completed
|
||||
|
@ -2110,7 +2102,7 @@ func (m *Manager) HandleCommunityRequestToJoin(signer *ecdsa.PublicKey, request
|
|||
return nil, err
|
||||
}
|
||||
|
||||
if existingRequestToJoin != nil {
|
||||
if existingRequestToJoin != nil && existingRequestToJoin.State != RequestToJoinStateCanceled {
|
||||
// request to join was already processed by an admin and waits to get
|
||||
// confirmation for its decision
|
||||
//
|
||||
|
@ -4636,7 +4628,7 @@ func (m *Manager) HandleCommunityTokensMetadata(communityID string, communityTok
|
|||
return nil
|
||||
}
|
||||
|
||||
func validateCommunityPrivilegedUserSyncMessage(message *protobuf.CommunityPrivilegedUserSyncMessage) error {
|
||||
func (m *Manager) ValidateCommunityPrivilegedUserSyncMessage(message *protobuf.CommunityPrivilegedUserSyncMessage) error {
|
||||
if message == nil {
|
||||
return errors.New("invalid CommunityPrivilegedUserSyncMessage message")
|
||||
}
|
||||
|
|
|
@ -374,7 +374,7 @@ func (p *Persistence) SaveRequestToJoin(request *RequestToJoin) (err error) {
|
|||
return ErrOldRequestToJoin
|
||||
}
|
||||
|
||||
_, err = tx.Exec(`INSERT INTO communities_requests_to_join(id,public_key,clock,ens_name,chat_id,community_id,state) VALUES (?, ?, ?, ?, ?, ?, ?)`, request.ID, request.PublicKey, request.Clock, request.ENSName, request.ChatID, request.CommunityID, request.State)
|
||||
_, err = tx.Exec(`INSERT OR REPLACE INTO communities_requests_to_join(id,public_key,clock,ens_name,chat_id,community_id,state) VALUES (?, ?, ?, ?, ?, ?, ?)`, request.ID, request.PublicKey, request.Clock, request.ENSName, request.ChatID, request.CommunityID, request.State)
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
|
@ -1308,6 +1308,19 @@ func (m *Messenger) CancelRequestToJoinCommunity(request *requests.CancelRequest
|
|||
return nil, err
|
||||
}
|
||||
|
||||
if !community.AcceptRequestToJoinAutomatically() {
|
||||
// send cancelation to community admins also
|
||||
rawMessage.Payload = payload
|
||||
|
||||
privilegedMembers := community.GetPrivilegedMembers()
|
||||
for _, privilegedMember := range privilegedMembers {
|
||||
_, err := m.sender.SendPrivate(context.Background(), privilegedMember, &rawMessage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
response := &MessengerResponse{}
|
||||
response.AddCommunity(community)
|
||||
response.RequestsToJoinCommunity = append(response.RequestsToJoinCommunity, requestToJoin)
|
||||
|
@ -1354,6 +1367,8 @@ func (m *Messenger) AcceptRequestToJoinCommunity(request *requests.AcceptRequest
|
|||
return nil, err
|
||||
}
|
||||
|
||||
if community.IsControlNode() {
|
||||
// If we are the control node, we send the response to the user
|
||||
pk, err := common.HexToPubkey(requestToJoin.PublicKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -1364,7 +1379,6 @@ func (m *Messenger) AcceptRequestToJoinCommunity(request *requests.AcceptRequest
|
|||
return nil, err
|
||||
}
|
||||
|
||||
if community.IsControlNode() {
|
||||
requestToJoinResponseProto := &protobuf.CommunityRequestToJoinResponse{
|
||||
Clock: community.Clock(),
|
||||
Accepted: true,
|
||||
|
@ -1437,6 +1451,7 @@ func (m *Messenger) AcceptRequestToJoinCommunity(request *requests.AcceptRequest
|
|||
|
||||
response := &MessengerResponse{}
|
||||
response.AddCommunity(community)
|
||||
response.AddRequestToJoinCommunity(requestToJoin)
|
||||
|
||||
// Activity Center notification
|
||||
notification, err := m.persistence.GetActivityCenterNotificationByID(request.ID)
|
||||
|
@ -1522,9 +1537,10 @@ func (m *Messenger) DeclineRequestToJoinCommunity(request *requests.DeclineReque
|
|||
}
|
||||
|
||||
response := &MessengerResponse{}
|
||||
dbRequest, err = m.communitiesManager.GetRequestToJoin(request.ID)
|
||||
response.AddRequestToJoinCommunity(dbRequest)
|
||||
|
||||
if notification != nil {
|
||||
dbRequest, err := m.communitiesManager.GetRequestToJoin(request.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -2159,6 +2175,26 @@ func (m *Messenger) PendingRequestsToJoinForCommunity(id types.HexBytes) ([]*com
|
|||
return m.communitiesManager.PendingRequestsToJoinForCommunity(id)
|
||||
}
|
||||
|
||||
func (m *Messenger) AllPendingRequestsToJoinForCommunity(id types.HexBytes) ([]*communities.RequestToJoin, error) {
|
||||
pendingRequests, err := m.communitiesManager.PendingRequestsToJoinForCommunity(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
acceptedPendingRequests, err := m.communitiesManager.AcceptedPendingRequestsToJoinForCommunity(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
declinedPendingRequests, err := m.communitiesManager.DeclinedPendingRequestsToJoinForCommunity(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pendingRequests = append(pendingRequests, acceptedPendingRequests...)
|
||||
pendingRequests = append(pendingRequests, declinedPendingRequests...)
|
||||
|
||||
return pendingRequests, nil
|
||||
}
|
||||
|
||||
func (m *Messenger) DeclinedRequestsToJoinForCommunity(id types.HexBytes) ([]*communities.RequestToJoin, error) {
|
||||
return m.communitiesManager.DeclinedRequestsToJoinForCommunity(id)
|
||||
}
|
||||
|
@ -2542,6 +2578,7 @@ func (m *Messenger) handleCommunityResponse(state *ReceivedMessageState, communi
|
|||
|
||||
state.Response.AddCommunity(community)
|
||||
state.Response.CommunityChanges = append(state.Response.CommunityChanges, communityResponse.Changes)
|
||||
state.Response.AddRequestsToJoinCommunity(communityResponse.RequestsToJoin)
|
||||
|
||||
// If we haven't joined the org, nothing to do
|
||||
if !community.Joined() {
|
||||
|
@ -2608,6 +2645,41 @@ func (m *Messenger) handleCommunityResponse(state *ReceivedMessageState, communi
|
|||
return err
|
||||
}
|
||||
|
||||
for _, requestToJoin := range communityResponse.RequestsToJoin {
|
||||
// Activity Center notification
|
||||
notification, err := m.persistence.GetActivityCenterNotificationByID(requestToJoin.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if notification != nil {
|
||||
notification.MembershipStatus = ActivityCenterMembershipStatusAccepted
|
||||
switch requestToJoin.State {
|
||||
case communities.RequestToJoinStateDeclined:
|
||||
notification.MembershipStatus = ActivityCenterMembershipStatusDeclined
|
||||
case communities.RequestToJoinStateAccepted:
|
||||
notification.MembershipStatus = ActivityCenterMembershipStatusAccepted
|
||||
case communities.RequestToJoinStateAcceptedPending:
|
||||
notification.MembershipStatus = ActivityCenterMembershipStatusAcceptedPending
|
||||
case communities.RequestToJoinStateDeclinedPending:
|
||||
notification.MembershipStatus = ActivityCenterMembershipStatusDeclinedPending
|
||||
default:
|
||||
notification.MembershipStatus = ActivityCenterMembershipStatusPending
|
||||
|
||||
}
|
||||
|
||||
notification.Read = true
|
||||
notification.Accepted = true
|
||||
notification.UpdatedAt = m.getCurrentTimeInMillis()
|
||||
|
||||
err = m.addActivityCenterNotification(state.Response, notification)
|
||||
if err != nil {
|
||||
m.logger.Error("failed to save notification", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -2639,7 +2711,55 @@ func (m *Messenger) handleCommunityEventsMessageRejected(state *ReceivedMessageS
|
|||
}
|
||||
|
||||
func (m *Messenger) handleCommunityPrivilegedUserSyncMessage(state *ReceivedMessageState, signer *ecdsa.PublicKey, message protobuf.CommunityPrivilegedUserSyncMessage) error {
|
||||
return m.communitiesManager.HandleCommunityPrivilegedUserSyncMessage(signer, &message)
|
||||
if signer == nil {
|
||||
return errors.New("signer can't be nil")
|
||||
}
|
||||
|
||||
community, err := m.communitiesManager.GetByID(message.CommunityId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if community == nil {
|
||||
return errors.New("community not found")
|
||||
}
|
||||
|
||||
if !community.IsPrivilegedMember(&m.identity.PublicKey) {
|
||||
return errors.New("user has no permissions to process privileged sync message")
|
||||
}
|
||||
|
||||
isControlNodeMsg := common.IsPubKeyEqual(community.PublicKey(), signer)
|
||||
if !(isControlNodeMsg || community.IsPrivilegedMember(signer)) {
|
||||
return errors.New("user has no permissions to send privileged sync message")
|
||||
}
|
||||
|
||||
err = m.communitiesManager.ValidateCommunityPrivilegedUserSyncMessage(&message)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch message.Type {
|
||||
case protobuf.CommunityPrivilegedUserSyncMessage_CONTROL_NODE_ACCEPT_REQUEST_TO_JOIN:
|
||||
fallthrough
|
||||
case protobuf.CommunityPrivilegedUserSyncMessage_CONTROL_NODE_REJECT_REQUEST_TO_JOIN:
|
||||
if !isControlNodeMsg {
|
||||
return errors.New("accepted/requested to join sync messages can be send only by the control node")
|
||||
}
|
||||
requestsToJoin, err := m.communitiesManager.HandleRequestToJoinPrivilegedUserSyncMessage(&message, community.ID())
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
state.Response.AddRequestsToJoinCommunity(requestsToJoin)
|
||||
|
||||
case protobuf.CommunityPrivilegedUserSyncMessage_ADD_COMMUNITY_TOKENS:
|
||||
// TODO add tokens to the Response
|
||||
err = m.communitiesManager.HandleAddCommunityTokenPrivilegedUserSyncMessage(&message, community)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Messenger) handleSyncCommunity(messageState *ReceivedMessageState, syncCommunity protobuf.SyncCommunity) error {
|
||||
|
|
|
@ -561,6 +561,11 @@ func (api *PublicAPI) PendingRequestsToJoinForCommunity(id types.HexBytes) ([]*c
|
|||
return api.service.messenger.PendingRequestsToJoinForCommunity(id)
|
||||
}
|
||||
|
||||
// AllPendingRequestsToJoinForCommunity returns the all the pending requests to join, including accepted and rejected ones, for a given community
|
||||
func (api *PublicAPI) AllPendingRequestsToJoinForCommunity(id types.HexBytes) ([]*communities.RequestToJoin, error) {
|
||||
return api.service.messenger.AllPendingRequestsToJoinForCommunity(id)
|
||||
}
|
||||
|
||||
// DeclinedRequestsToJoinForCommunity returns the declined requests to join for a given community
|
||||
func (api *PublicAPI) DeclinedRequestsToJoinForCommunity(id types.HexBytes) ([]*communities.RequestToJoin, error) {
|
||||
return api.service.messenger.DeclinedRequestsToJoinForCommunity(id)
|
||||
|
|
Loading…
Reference in New Issue