fix(messenger_communities): block messages and reactions to token gated or spectated communities (#4064)

Which specifies that if a user is not a community member & a
chat member, he can't post, react or pin messages in that chat.

Notes:
- also fix&cleanup associated failing tests.
- refactor Community.CanPost() to reflect the new requirement.
- grant code is not fully implemented and is to be removed later.

Fixes https://github.com/status-im/status-desktop/issues/11915
This commit is contained in:
Shinnok 2023-10-25 17:26:18 +03:00 committed by GitHub
parent 9d59d889f6
commit 3805662a18
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 106 additions and 234 deletions

View File

@ -1722,72 +1722,54 @@ func (o *Community) VerifyGrantSignature(data []byte) (*protobuf.Grant, error) {
func (o *Community) CanPost(pk *ecdsa.PublicKey, chatID string, grantBytes []byte) (bool, error) { func (o *Community) CanPost(pk *ecdsa.PublicKey, chatID string, grantBytes []byte) (bool, error) {
if o.config.CommunityDescription.Chats == nil { if o.config.CommunityDescription.Chats == nil {
o.config.Logger.Debug("canPost, no-chats") o.config.Logger.Debug("Community.CanPost: no-chats")
return false, nil return false, nil
} }
chat, ok := o.config.CommunityDescription.Chats[chatID] chat, ok := o.config.CommunityDescription.Chats[chatID]
if !ok { if !ok {
o.config.Logger.Debug("canPost, no chat with id", zap.String("chat-id", chatID)) o.config.Logger.Debug("Community.CanPost: no chat with id", zap.String("chat-id", chatID))
return false, nil return false, nil
} }
// creator can always post // community creator can always post, return immediately
if common.IsPubKeyEqual(pk, o.config.ID) { if common.IsPubKeyEqual(pk, o.config.ID) {
return true, nil return true, nil
} }
// if banned cannot post
if o.isBanned(pk) { if o.isBanned(pk) {
o.config.Logger.Debug("Community.CanPost: user is banned", zap.String("chat-id", chatID))
return false, nil return false, nil
} }
// If both the chat & the org have no permissions, the user is allowed to post if o.config.CommunityDescription.Members == nil {
if o.config.CommunityDescription.Permissions.Access == protobuf.CommunityPermissions_NO_MEMBERSHIP && chat.Permissions.Access == protobuf.CommunityPermissions_NO_MEMBERSHIP { o.config.Logger.Debug("Community.CanPost: no members in org", zap.String("chat-id", chatID))
return true, nil return false, nil
} }
if chat.Permissions.Access != protobuf.CommunityPermissions_NO_MEMBERSHIP { // If community member, also check chat membership next
if chat.Members == nil { _, ok = o.config.CommunityDescription.Members[common.PubkeyToHex(pk)]
o.config.Logger.Debug("canPost, no members in chat", zap.String("chat-id", chatID)) if !ok {
o.config.Logger.Debug("Community.CanPost: not a community member", zap.String("chat-id", chatID))
return false, nil
}
if chat.Members == nil {
o.config.Logger.Debug("Community.CanPost: no members in chat", zap.String("chat-id", chatID))
return false, nil
}
// Need to also be a chat member to post
if !o.IsMemberInChat(pk, chatID) {
if grantBytes == nil {
o.config.Logger.Debug("Community.CanPost: not a chat member:", zap.String("chat-id", chatID))
return false, nil return false, nil
} }
_, ok := chat.Members[common.PubkeyToHex(pk)]
// If member, we stop here
if ok {
return true, nil
}
// If not a member, and not grant, we return
if !ok && grantBytes == nil {
o.config.Logger.Debug("canPost, not a member in chat", zap.String("chat-id", chatID))
return false, nil
}
// Otherwise we verify the grant
return o.canPostWithGrant(pk, chatID, grantBytes) return o.canPostWithGrant(pk, chatID, grantBytes)
} }
// Chat has no membership, check org permissions // all conditions satisfied, user can post after all
if o.config.CommunityDescription.Members == nil { return true, nil
o.config.Logger.Debug("canPost, no members in org", zap.String("chat-id", chatID))
return false, nil
}
// If member, they can post
_, ok = o.config.CommunityDescription.Members[common.PubkeyToHex(pk)]
if ok {
return true, nil
}
// Not a member and no grant, can't post
if !ok && grantBytes == nil {
o.config.Logger.Debug("canPost, not a member in org", zap.String("chat-id", chatID), zap.String("pubkey", common.PubkeyToHex(pk)))
return false, nil
}
return o.canPostWithGrant(pk, chatID, grantBytes)
} }
func (o *Community) canPostWithGrant(pk *ecdsa.PublicKey, chatID string, grantBytes []byte) (bool, error) { func (o *Community) canPostWithGrant(pk *ecdsa.PublicKey, chatID string, grantBytes []byte) (bool, error) {

View File

@ -511,7 +511,7 @@ func (s *CommunitySuite) TestCanPost() {
name: "no-membership org with no-membeship chat", name: "no-membership org with no-membeship chat",
config: s.configNoMembershipOrgNoMembershipChat(), config: s.configNoMembershipOrgNoMembershipChat(),
member: notMember, member: notMember,
canPost: true, canPost: false,
}, },
{ {
name: "no-membership org with invitation only chat-not-a-member", name: "no-membership org with invitation only chat-not-a-member",
@ -529,7 +529,7 @@ func (s *CommunitySuite) TestCanPost() {
name: "no-membership org with invitation only chat-not-a-member valid grant", name: "no-membership org with invitation only chat-not-a-member valid grant",
config: s.configNoMembershipOrgInvitationOnlyChat(), config: s.configNoMembershipOrgInvitationOnlyChat(),
member: notMember, member: notMember,
canPost: true, canPost: false,
grant: validGrant, grant: validGrant,
}, },
{ {
@ -555,7 +555,7 @@ func (s *CommunitySuite) TestCanPost() {
name: "membership org with no-membership chat not-a-member valid grant", name: "membership org with no-membership chat not-a-member valid grant",
config: s.configOnRequestOrgNoMembershipChat(), config: s.configOnRequestOrgNoMembershipChat(),
member: notMember, member: notMember,
canPost: true, canPost: false,
grant: validGrant, grant: validGrant,
}, },
{ {

View File

@ -442,18 +442,29 @@ func (s *MessengerCommunitiesSuite) TestCommunityContactCodeAdvertisement() {
func (s *MessengerCommunitiesSuite) TestPostToCommunityChat() { func (s *MessengerCommunitiesSuite) TestPostToCommunityChat() {
community, chat := s.createCommunity() community, chat := s.createCommunity()
s.advertiseCommunityTo(community, s.owner, s.alice)
s.joinCommunity(community, s.owner, s.alice)
ctx := context.Background()
chatID := chat.ID chatID := chat.ID
inputMessage := common.NewMessage() inputMessage := common.NewMessage()
inputMessage.ChatId = chatID inputMessage.ChatId = chatID
inputMessage.ContentType = protobuf.ChatMessage_TEXT_PLAIN inputMessage.ContentType = protobuf.ChatMessage_TEXT_PLAIN
inputMessage.Text = "some text" inputMessage.Text = "some text"
ctx := context.Background()
s.advertiseCommunityTo(community, s.owner, s.alice)
// Send message without even spectating fails
_, err := s.alice.SendChatMessage(ctx, inputMessage) _, err := s.alice.SendChatMessage(ctx, inputMessage)
s.Require().Error(err)
// Sending a message without joining fails
_, err = s.alice.SpectateCommunity(community.ID())
s.Require().NoError(err)
_, err = s.alice.SendChatMessage(ctx, inputMessage)
s.Require().Error(err)
// Sending should work now
s.joinCommunity(community, s.owner, s.alice)
_, err = s.alice.SendChatMessage(ctx, inputMessage)
s.Require().NoError(err) s.Require().NoError(err)
var response *MessengerResponse var response *MessengerResponse

View File

@ -2137,10 +2137,8 @@ func (m *Messenger) dispatchMessage(ctx context.Context, rawMessage common.RawMe
return rawMessage, err return rawMessage, err
} }
// We allow emoji reactions by anyone if !canPost {
if rawMessage.MessageType != protobuf.ApplicationMetadataMessage_EMOJI_REACTION && !canPost {
m.logger.Error("can't post on chat", zap.String("chat-id", chat.ID), zap.String("chat-name", chat.Name)) m.logger.Error("can't post on chat", zap.String("chat-id", chat.ID), zap.String("chat-name", chat.Name))
return rawMessage, errors.New("can't post on chat") return rawMessage, errors.New("can't post on chat")
} }

View File

@ -12,6 +12,7 @@ import (
"github.com/status-im/status-go/eth-node/crypto" "github.com/status-im/status-go/eth-node/crypto"
"github.com/status-im/status-go/eth-node/types" "github.com/status-im/status-go/eth-node/types"
"github.com/status-im/status-go/protocol/common" "github.com/status-im/status-go/protocol/common"
"github.com/status-im/status-go/protocol/communities"
"github.com/status-im/status-go/protocol/protobuf" "github.com/status-im/status-go/protocol/protobuf"
"github.com/status-im/status-go/protocol/requests" "github.com/status-im/status-go/protocol/requests"
"github.com/status-im/status-go/protocol/tt" "github.com/status-im/status-go/protocol/tt"
@ -23,6 +24,19 @@ func TestMessengerActivityCenterMessageSuite(t *testing.T) {
suite.Run(t, new(MessengerActivityCenterMessageSuite)) suite.Run(t, new(MessengerActivityCenterMessageSuite))
} }
func (s *MessengerActivityCenterMessageSuite) createCommunity(owner *Messenger) (*communities.Community, *Chat) {
return createCommunity(&s.Suite, owner)
}
func (s *MessengerActivityCenterMessageSuite) advertiseCommunityTo(community *communities.Community, owner *Messenger, user *Messenger) {
advertiseCommunityTo(&s.Suite, community, owner, user)
}
func (s *MessengerActivityCenterMessageSuite) joinCommunity(community *communities.Community, owner *Messenger, user *Messenger) {
request := &requests.RequestToJoinCommunity{CommunityID: community.ID()}
joinCommunity(&s.Suite, community, owner, user, request)
}
type MessengerActivityCenterMessageSuite struct { type MessengerActivityCenterMessageSuite struct {
suite.Suite suite.Suite
m *Messenger // main instance of Messenger m *Messenger // main instance of Messenger
@ -117,75 +131,35 @@ func (s *MessengerActivityCenterMessageSuite) TestDeleteOneToOneChat() {
} }
func (s *MessengerActivityCenterMessageSuite) TestEveryoneMentionTag() { func (s *MessengerActivityCenterMessageSuite) TestEveryoneMentionTag() {
description := &requests.CreateCommunity{
Membership: protobuf.CommunityPermissions_NO_MEMBERSHIP,
Name: "status",
Color: "#ffffff",
Description: "status community description",
}
alice := s.m alice := s.m
bob := s.newMessenger() bob := s.newMessenger()
_, err := bob.Start() _, err := bob.Start()
s.Require().NoError(err) s.Require().NoError(err)
defer bob.Shutdown() // nolint: errcheck defer bob.Shutdown() // nolint: errcheck
// Create an community chat // Create a community
response, err := bob.CreateCommunity(description, true) community, chat := s.createCommunity(bob)
s.Require().NoError(err)
s.Require().Len(response.Communities(), 1)
community := response.Communities()[0]
s.Require().NotNil(community) s.Require().NotNil(community)
s.Require().NotNil(chat)
chat := CreateOneToOneChat(common.PubkeyToHex(&alice.identity.PublicKey), &alice.identity.PublicKey, bob.transport)
// bob sends a community message
inputMessage := common.NewMessage()
inputMessage.ChatId = chat.ID
inputMessage.Text = "some text"
inputMessage.CommunityID = community.IDString()
err = bob.SaveChat(chat)
s.Require().NoError(err)
_, err = bob.SendChatMessage(context.Background(), inputMessage)
s.Require().NoError(err)
_, err = WaitOnMessengerResponse(
alice,
func(r *MessengerResponse) bool { return len(r.Communities()) == 1 },
"no messages",
)
s.Require().NoError(err)
// Alice joins the community // Alice joins the community
response, err = alice.JoinCommunity(context.Background(), community.ID(), false) s.advertiseCommunityTo(community, bob, alice)
s.Require().NoError(err) s.joinCommunity(community, bob, alice)
s.Require().NotNil(response)
s.Require().Len(response.Communities(), 1)
s.Require().True(response.Communities()[0].Joined())
s.Require().Len(response.Chats(), 1)
defaultCommunityChatID := response.Chats()[0].ID // alice sends a community message
inputMessage := common.NewMessage()
// bob sends a community message inputMessage.ChatId = chat.ID
inputMessage = common.NewMessage() inputMessage.ContentType = protobuf.ChatMessage_TEXT_PLAIN
inputMessage.ChatId = defaultCommunityChatID
inputMessage.Text = "Good news, @" + common.EveryoneMentionTag + " !" inputMessage.Text = "Good news, @" + common.EveryoneMentionTag + " !"
inputMessage.CommunityID = community.IDString()
response, err = alice.SendChatMessage(context.Background(), inputMessage) response, err := alice.SendChatMessage(context.Background(), inputMessage)
s.Require().NoError(err) s.Require().NoError(err)
s.Require().Len(response.Messages(), 1) s.Require().Len(response.Messages(), 1)
s.Require().True(response.Messages()[0].Mentioned) s.Require().True(response.Messages()[0].Mentioned)
response, err = WaitOnMessengerResponse( response, err = WaitOnMessengerResponse(
bob, bob,
func(r *MessengerResponse) bool { return len(r.Messages()) == 1 }, func(r *MessengerResponse) bool { return len(r.Messages()) >= 1 },
"no messages", "no messages",
) )
@ -199,14 +173,6 @@ func (s *MessengerActivityCenterMessageSuite) TestEveryoneMentionTag() {
} }
func (s *MessengerActivityCenterMessageSuite) TestReplyWithImage() { func (s *MessengerActivityCenterMessageSuite) TestReplyWithImage() {
description := &requests.CreateCommunity{
Membership: protobuf.CommunityPermissions_NO_MEMBERSHIP,
Name: "status",
Color: "#ffffff",
Description: "status community description",
}
alice := s.m alice := s.m
bob := s.newMessenger() bob := s.newMessenger()
_, err := bob.Start() _, err := bob.Start()
@ -218,73 +184,36 @@ func (s *MessengerActivityCenterMessageSuite) TestReplyWithImage() {
s.Require().NoError(err) s.Require().NoError(err)
s.Require().NotNil(mediaServer) s.Require().NotNil(mediaServer)
s.Require().NoError(mediaServer.Start()) s.Require().NoError(mediaServer.Start())
alice.httpServer = mediaServer alice.httpServer = mediaServer
// Create an community chat // Create a community
response, err := bob.CreateCommunity(description, true) community, chat := s.createCommunity(bob)
s.Require().NoError(err)
s.Require().Len(response.Communities(), 1)
community := response.Communities()[0]
s.Require().NotNil(community) s.Require().NotNil(community)
s.Require().NotNil(chat)
chat := CreateOneToOneChat(common.PubkeyToHex(&alice.identity.PublicKey), &alice.identity.PublicKey, bob.transport)
// bob sends a community message
inputMessage := common.NewMessage()
inputMessage.ChatId = chat.ID
inputMessage.Text = "some text"
inputMessage.CommunityID = community.IDString()
err = bob.SaveChat(chat)
s.Require().NoError(err)
_, err = bob.SendChatMessage(context.Background(), inputMessage)
s.Require().NoError(err)
_, err = WaitOnMessengerResponse(
alice,
func(r *MessengerResponse) bool { return len(r.Communities()) == 1 },
"no messages",
)
s.Require().NoError(err)
// Alice joins the community // Alice joins the community
response, err = alice.JoinCommunity(context.Background(), community.ID(), false) s.advertiseCommunityTo(community, bob, alice)
s.Require().NoError(err) s.joinCommunity(community, bob, alice)
s.Require().NotNil(response)
s.Require().Len(response.Communities(), 1)
s.Require().True(response.Communities()[0].Joined())
s.Require().Len(response.Chats(), 1)
defaultCommunityChat := response.Chats()[0] // Alice sends a community message
inputMessage := common.NewMessage()
defaultCommunityChatID := defaultCommunityChat.ID inputMessage.ChatId = chat.ID
inputMessage.ContentType = protobuf.ChatMessage_TEXT_PLAIN
// bob sends a community message
inputMessage = common.NewMessage()
inputMessage.ChatId = defaultCommunityChatID
inputMessage.Text = "test message" inputMessage.Text = "test message"
inputMessage.CommunityID = community.IDString()
response, err = alice.SendChatMessage(context.Background(), inputMessage) response, err := alice.SendChatMessage(context.Background(), inputMessage)
s.Require().NoError(err) s.Require().NoError(err)
s.Require().Len(response.Messages(), 1) s.Require().Len(response.Messages(), 1)
response, err = WaitOnMessengerResponse( response, err = WaitOnMessengerResponse(
bob, bob,
func(r *MessengerResponse) bool { return len(r.Messages()) == 1 }, func(r *MessengerResponse) bool { return len(r.Messages()) == 1 },
"no messages", "no messages",
) )
s.Require().NoError(err) s.Require().NoError(err)
s.Require().Len(response.Messages(), 1) s.Require().Len(response.Messages(), 1)
// bob sends a community message // bob sends a reply with an image
inputMessage, err = buildImageWithAlbumIDMessage(*defaultCommunityChat, "0x34") inputMessage, err = buildImageWithAlbumIDMessage(*chat, "0x34")
s.Require().NoError(err) s.Require().NoError(err)
inputMessage.Text = "test message reply" inputMessage.Text = "test message reply"
@ -292,25 +221,22 @@ func (s *MessengerActivityCenterMessageSuite) TestReplyWithImage() {
response, err = bob.SendChatMessage(context.Background(), inputMessage) response, err = bob.SendChatMessage(context.Background(), inputMessage)
s.Require().NoError(err) s.Require().NoError(err)
s.Require().Len(response.Messages(), 2) s.Require().Len(response.Messages(), 2)
response, err = WaitOnMessengerResponse( response, err = WaitOnMessengerResponse(
alice, alice,
func(r *MessengerResponse) bool { return len(r.Messages()) == 2 }, func(r *MessengerResponse) bool { return len(r.Messages()) == 2 },
"no messages", "no messages",
) )
s.Require().NoError(err) s.Require().NoError(err)
s.Require().Len(response.ActivityCenterNotifications(), 1) s.Require().Len(response.ActivityCenterNotifications(), 1)
// verify the new message
var newMessage *common.Message var newMessage *common.Message
for _, m := range response.Messages() { for _, m := range response.Messages() {
if m.Text == "test message reply" { if m.Text == "test message reply" {
newMessage = m newMessage = m
} }
} }
s.Require().NotNil(newMessage) s.Require().NotNil(newMessage)
s.Require().Equal(protobuf.ChatMessage_IMAGE, newMessage.ContentType) s.Require().Equal(protobuf.ChatMessage_IMAGE, newMessage.ContentType)
s.Require().NotEmpty(newMessage.ImageLocalURL) s.Require().NotEmpty(newMessage.ImageLocalURL)
@ -331,57 +257,19 @@ func (s *MessengerActivityCenterMessageSuite) TestReplyWithImage() {
} }
func (s *MessengerActivityCenterMessageSuite) TestMuteCommunityActivityCenterNotifications() { func (s *MessengerActivityCenterMessageSuite) TestMuteCommunityActivityCenterNotifications() {
description := &requests.CreateCommunity{
Membership: protobuf.CommunityPermissions_NO_MEMBERSHIP,
Name: "status",
Color: "#ffffff",
Description: "status community description",
}
alice := s.m alice := s.m
bob := s.newMessenger() bob := s.newMessenger()
_, err := bob.Start() _, err := bob.Start()
s.Require().NoError(err) s.Require().NoError(err)
// Create an community chat // Create a community
response, err := bob.CreateCommunity(description, true) community, chat := s.createCommunity(bob)
s.Require().NoError(err)
s.Require().Len(response.Communities(), 1)
community := response.Communities()[0]
s.Require().NotNil(community) s.Require().NotNil(community)
s.Require().NotNil(chat)
chat := CreateOneToOneChat(common.PubkeyToHex(&alice.identity.PublicKey), &alice.identity.PublicKey, bob.transport)
// bob sends a community message
inputMessage := common.NewMessage()
inputMessage.ChatId = chat.ID
inputMessage.Text = "some text"
inputMessage.CommunityID = community.IDString()
err = bob.SaveChat(chat)
s.Require().NoError(err)
_, err = bob.SendChatMessage(context.Background(), inputMessage)
s.Require().NoError(err)
_, err = WaitOnMessengerResponse(
alice,
func(r *MessengerResponse) bool { return len(r.Communities()) == 1 },
"no messages",
)
s.Require().NoError(err)
// Alice joins the community // Alice joins the community
response, err = alice.JoinCommunity(context.Background(), community.ID(), true) s.advertiseCommunityTo(community, bob, alice)
s.Require().NoError(err) s.joinCommunity(community, bob, alice)
s.Require().NotNil(response)
s.Require().Len(response.Communities(), 1)
s.Require().True(response.Communities()[0].Joined())
s.Require().Len(response.Chats(), 1)
defaultCommunityChatID := response.Chats()[0].ID
// Bob mutes the community // Bob mutes the community
time, err := bob.MuteAllCommunityChats(&requests.MuteCommunity{ time, err := bob.MuteAllCommunityChats(&requests.MuteCommunity{
@ -396,16 +284,14 @@ func (s *MessengerActivityCenterMessageSuite) TestMuteCommunityActivityCenterNot
s.Require().True(bobCommunity.Muted()) s.Require().True(bobCommunity.Muted())
// alice sends a community message // alice sends a community message
inputMessage = common.NewMessage() inputMessage := common.NewMessage()
inputMessage.ChatId = defaultCommunityChatID inputMessage.ChatId = chat.ID
inputMessage.ContentType = protobuf.ChatMessage_TEXT_PLAIN
inputMessage.Text = "Good news, @" + common.EveryoneMentionTag + " !" inputMessage.Text = "Good news, @" + common.EveryoneMentionTag + " !"
inputMessage.CommunityID = community.IDString()
response, err = alice.SendChatMessage(context.Background(), inputMessage) response, err := alice.SendChatMessage(context.Background(), inputMessage)
s.Require().NoError(err) s.Require().NoError(err)
s.Require().Len(response.Messages(), 1) s.Require().Len(response.Messages(), 1)
s.Require().True(response.Messages()[0].Mentioned) s.Require().True(response.Messages()[0].Mentioned)
response, err = WaitOnMessengerResponse( response, err = WaitOnMessengerResponse(
@ -414,10 +300,9 @@ func (s *MessengerActivityCenterMessageSuite) TestMuteCommunityActivityCenterNot
"no messages", "no messages",
) )
// Bob still receives it, but no AC notif
s.Require().NoError(err) s.Require().NoError(err)
s.Require().Len(response.Messages(), 1) s.Require().Len(response.Messages(), 1)
s.Require().True(response.Messages()[0].Mentioned) s.Require().True(response.Messages()[0].Mentioned)
s.Require().Len(response.ActivityCenterNotifications(), 0) s.Require().Len(response.ActivityCenterNotifications(), 0)
} }

View File

@ -2625,31 +2625,27 @@ func (m *Messenger) matchChatEntity(chatEntity common.ChatEntity) (*Chat, error)
return nil, errors.New("not an community chat") return nil, errors.New("not an community chat")
} }
var emojiReaction bool
var pinMessage bool
// We allow emoji reactions from anyone
switch chatEntity.(type) {
case *EmojiReaction:
emojiReaction = true
case *common.PinMessage:
pinMessage = true
}
canPost, err := m.communitiesManager.CanPost(chatEntity.GetSigPubKey(), chat.CommunityID, chat.CommunityChatID(), chatEntity.GetGrant()) canPost, err := m.communitiesManager.CanPost(chatEntity.GetSigPubKey(), chat.CommunityID, chat.CommunityChatID(), chatEntity.GetGrant())
if err != nil { if err != nil {
return nil, err return nil, err
} }
community, err := m.communitiesManager.GetByIDString(chat.CommunityID) if !canPost {
if err != nil { return nil, errors.New("user can't post in community")
return nil, err
} }
hasPermission := community.IsPrivilegedMember(chatEntity.GetSigPubKey()) _, isPinMessage := chatEntity.(*common.PinMessage)
pinMessageAllowed := community.AllowsAllMembersToPinMessage() if isPinMessage {
community, err := m.communitiesManager.GetByIDString(chat.CommunityID)
if err != nil {
return nil, err
}
if (pinMessage && !hasPermission && !pinMessageAllowed) || (!emojiReaction && !canPost) { hasPermission := community.IsPrivilegedMember(chatEntity.GetSigPubKey())
return nil, errors.New("user can't post") pinMessageAllowed := community.AllowsAllMembersToPinMessage()
if !hasPermission && !pinMessageAllowed {
return nil, errors.New("user can't pin message")
}
} }
return chat, nil return chat, nil