feature: view only channel reactions (#4820)

* CommunityMember channel role

* make generate
This commit is contained in:
Igor Sirotin 2024-03-01 17:15:38 +00:00 committed by GitHub
parent 84713384bb
commit bdb2b261a6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 961 additions and 774 deletions

View File

@ -177,7 +177,9 @@ func (o *Community) MarshalPublicAPIJSON() ([]byte, error) {
communityItem.Encrypted = o.Encrypted()
}
for id, c := range o.config.CommunityDescription.Chats {
canPost, err := o.CanPost(o.config.MemberIdentity, id)
// NOTE: Here `CanPost` is only set for ChatMessage. But it can be different for reactions/pin/etc.
// Consider adding more properties to `CommunityChat` to reflect that.
canPost, err := o.CanPost(o.config.MemberIdentity, id, protobuf.ApplicationMetadataMessage_CHAT_MESSAGE)
if err != nil {
return nil, err
}
@ -311,7 +313,9 @@ func (o *Community) MarshalJSON() ([]byte, error) {
communityItem.Categories[id] = category
}
for id, c := range o.config.CommunityDescription.Chats {
canPost, err := o.CanPost(o.config.MemberIdentity, id)
// NOTE: Here `CanPost` is only set for ChatMessage. But it can be different for reactions/pin/etc.
// Consider adding more properties to `CommunityChat` to reflect that.
canPost, err := o.CanPost(o.config.MemberIdentity, id, protobuf.ApplicationMetadataMessage_CHAT_MESSAGE)
if err != nil {
return nil, err
}
@ -1847,7 +1851,7 @@ func (o *Community) VerifyGrantSignature(data []byte) (*protobuf.Grant, error) {
return grant, nil
}
func (o *Community) CanPost(pk *ecdsa.PublicKey, chatID string) (bool, error) {
func (o *Community) CanPost(pk *ecdsa.PublicKey, chatID string, messageType protobuf.ApplicationMetadataMessage_Type) (bool, error) {
if o.config.CommunityDescription.Chats == nil {
o.config.Logger.Debug("Community.CanPost: no-chats")
return false, nil
@ -1886,14 +1890,24 @@ func (o *Community) CanPost(pk *ecdsa.PublicKey, chatID string) (bool, error) {
return false, nil
}
// Need to also be a chat member to post
if !o.IsMemberInChat(pk, chatID) {
o.config.Logger.Debug("Community.CanPost: not a chat member", zap.String("chat-id", chatID))
return false, nil
}
member, isChatMember := chat.Members[common.PubkeyToHex(pk)]
// all conditions satisfied, user can post after all
return true, nil
switch messageType {
case protobuf.ApplicationMetadataMessage_PIN_MESSAGE:
pinAllowed := o.IsPrivilegedMember(pk) || o.AllowsAllMembersToPinMessage()
return isChatMember && pinAllowed, nil
case protobuf.ApplicationMetadataMessage_EMOJI_REACTION:
if !isChatMember {
return false, nil
}
isPoster := member.ChannelRole == protobuf.CommunityMember_CHANNEL_ROLE_POSTER
isViewer := member.ChannelRole == protobuf.CommunityMember_CHANNEL_ROLE_VIEWER
return isPoster || (isViewer && chat.ViewersCanPostReactions), nil
default:
return isChatMember, nil
}
}
func (o *Community) BuildGrant(key *ecdsa.PublicKey, chatID string) ([]byte, error) {
@ -1968,8 +1982,8 @@ func (o *Community) isMember() bool {
return o.hasMember(o.config.MemberIdentity)
}
func (o *Community) CanMemberIdentityPost(chatID string) (bool, error) {
return o.CanPost(o.config.MemberIdentity, chatID)
func (o *Community) CanMemberIdentityPost(chatID string, messageType protobuf.ApplicationMetadataMessage_Type) (bool, error) {
return o.CanPost(o.config.MemberIdentity, chatID, messageType)
}
// CanJoin returns whether a user can join the community, only if it's
@ -2052,7 +2066,9 @@ func (o *Community) AddMember(publicKey *ecdsa.PublicKey, roles []protobuf.Commu
return changes, nil
}
func (o *Community) AddMemberToChat(chatID string, publicKey *ecdsa.PublicKey, roles []protobuf.CommunityMember_Roles) (*CommunityChanges, error) {
func (o *Community) AddMemberToChat(chatID string, publicKey *ecdsa.PublicKey,
roles []protobuf.CommunityMember_Roles, channelRole protobuf.CommunityMember_ChannelRole) (*CommunityChanges, error) {
o.mutex.Lock()
defer o.mutex.Unlock()
@ -2072,7 +2088,8 @@ func (o *Community) AddMemberToChat(chatID string, publicKey *ecdsa.PublicKey, r
chat.Members = make(map[string]*protobuf.CommunityMember)
}
chat.Members[memberKey] = &protobuf.CommunityMember{
Roles: roles,
Roles: roles,
ChannelRole: channelRole,
}
changes.ChatsModified[chatID] = &CommunityChatChanges{
ChatModified: chat,

View File

@ -761,7 +761,7 @@ func (s *CommunityEncryptionKeyActionSuite) TestChannelLevelKeyActions() {
for _, member := range tc.originMembers {
_, err := origin.AddMember(member, []protobuf.CommunityMember_Roles{})
s.Require().NoError(err)
_, err = origin.AddMemberToChat(channelID, member, []protobuf.CommunityMember_Roles{})
_, err = origin.AddMemberToChat(channelID, member, []protobuf.CommunityMember_Roles{}, protobuf.CommunityMember_CHANNEL_ROLE_POSTER)
s.Require().NoError(err)
}
@ -772,7 +772,7 @@ func (s *CommunityEncryptionKeyActionSuite) TestChannelLevelKeyActions() {
for _, member := range tc.modifiedMembers {
_, err := modified.AddMember(member, []protobuf.CommunityMember_Roles{})
s.Require().NoError(err)
_, err = modified.AddMemberToChat(channelID, member, []protobuf.CommunityMember_Roles{})
_, err = modified.AddMemberToChat(channelID, member, []protobuf.CommunityMember_Roles{}, protobuf.CommunityMember_CHANNEL_ROLE_POSTER)
s.Require().NoError(err)
}
@ -972,7 +972,7 @@ func (s *CommunityEncryptionKeyActionSuite) TestControlNodeChange() {
s.Require().NoError(err)
}
for _, member := range tc.channelMembers {
_, err = origin.AddMemberToChat(channelID, member, []protobuf.CommunityMember_Roles{})
_, err = origin.AddMemberToChat(channelID, member, []protobuf.CommunityMember_Roles{}, protobuf.CommunityMember_CHANNEL_ROLE_POSTER)
s.Require().NoError(err)
}

View File

@ -452,9 +452,6 @@ func (s *CommunitySuite) TestCanPost() {
notMember := &s.member3.PublicKey
member := &s.member1.PublicKey
// MEMBERSHIP-NO-MEMBERSHIP-Member-> User can post
// MEMBERSHIP-NO-MEMEBRESHIP->NON member -> User can't post
testCases := []struct {
name string
config Config
@ -463,7 +460,7 @@ func (s *CommunitySuite) TestCanPost() {
canPost bool
}{
{
name: "no-membership org with no-membeship chat",
name: "no-membership org with no-membership chat",
config: s.configNoMembershipOrgNoMembershipChat(),
member: notMember,
canPost: false,
@ -481,7 +478,7 @@ func (s *CommunitySuite) TestCanPost() {
canPost: true,
},
{
name: "monsier creator can always post of course",
name: "creator can always post of course",
config: s.configOnRequestOrgNoMembershipChat(),
member: &s.identity.PublicKey,
canPost: true,
@ -494,7 +491,7 @@ func (s *CommunitySuite) TestCanPost() {
org, err := New(tc.config, &TimeSourceStub{}, &DescriptionEncryptorMock{})
s.Require().NoError(err)
canPost, err := org.CanPost(tc.member, testChatID1)
canPost, err := org.CanPost(tc.member, testChatID1, protobuf.ApplicationMetadataMessage_CHAT_MESSAGE)
s.Require().Equal(tc.err, err)
s.Require().Equal(tc.canPost, canPost)
})

View File

@ -1046,8 +1046,12 @@ func (m *Manager) ReevaluateMembers(community *Community) (map[protobuf.Communit
isMemberAlreadyInChannel := community.IsMemberInChat(memberPubKey, channelID)
if response.ViewOnlyPermissions.Satisfied || response.ViewAndPostPermissions.Satisfied {
channelRole := protobuf.CommunityMember_CHANNEL_ROLE_VIEWER
if response.ViewAndPostPermissions.Satisfied {
channelRole = protobuf.CommunityMember_CHANNEL_ROLE_POSTER
}
if !isMemberAlreadyInChannel {
_, err := community.AddMemberToChat(channelID, memberPubKey, []protobuf.CommunityMember_Roles{})
_, err := community.AddMemberToChat(channelID, memberPubKey, []protobuf.CommunityMember_Roles{}, channelRole)
if err != nil {
return nil, err
}
@ -2315,27 +2319,37 @@ func (m *Manager) accountsSatisfyPermissionsToJoin(community *Community, account
return true, protobuf.CommunityMember_ROLE_NONE, nil
}
func (m *Manager) accountsSatisfyPermissionsToJoinChannels(community *Community, accounts []*protobuf.RevealedAccount) (map[string]*protobuf.CommunityChat, error) {
result := make(map[string]*protobuf.CommunityChat)
func (m *Manager) accountsSatisfyPermissionsToJoinChannels(community *Community, accounts []*protobuf.RevealedAccount) (map[string]*protobuf.CommunityChat, map[string]*protobuf.CommunityChat, error) {
accountsAndChainIDs := revealedAccountsToAccountsAndChainIDsCombination(accounts)
for channelID, channel := range community.config.CommunityDescription.Chats {
channelViewOnlyPermissions := community.ChannelTokenPermissionsByType(community.IDString()+channelID, protobuf.CommunityTokenPermission_CAN_VIEW_CHANNEL)
channelViewAndPostPermissions := community.ChannelTokenPermissionsByType(community.IDString()+channelID, protobuf.CommunityTokenPermission_CAN_VIEW_AND_POST_CHANNEL)
channelPermissions := append(channelViewOnlyPermissions, channelViewAndPostPermissions...)
viewChats, err := m.accountsSatisfyPermissionTypeToJoinChannels(community, accountsAndChainIDs, protobuf.CommunityTokenPermission_CAN_VIEW_CHANNEL)
if err != nil {
return nil, nil, err
}
if len(channelPermissions) > 0 {
permissionResponse, err := m.PermissionChecker.CheckPermissions(channelPermissions, accountsAndChainIDs, true)
postChats, err := m.accountsSatisfyPermissionTypeToJoinChannels(community, accountsAndChainIDs, protobuf.CommunityTokenPermission_CAN_VIEW_AND_POST_CHANNEL)
if err != nil {
return nil, nil, err
}
return viewChats, postChats, nil
}
func (m *Manager) accountsSatisfyPermissionTypeToJoinChannels(community *Community, accounts []*AccountChainIDsCombination, permissionType protobuf.CommunityTokenPermission_Type) (map[string]*protobuf.CommunityChat, error) {
result := make(map[string]*protobuf.CommunityChat)
for channelID, channel := range community.config.CommunityDescription.Chats {
permissions := community.ChannelTokenPermissionsByType(community.IDString()+channelID, permissionType)
if len(permissions) > 0 {
permissionResponse, err := m.PermissionChecker.CheckPermissions(permissions, accounts, true)
if err != nil {
return nil, err
}
if permissionResponse.Satisfied {
result[channelID] = channel
if !permissionResponse.Satisfied {
continue
}
} else {
result[channelID] = channel
}
result[channelID] = channel
}
return result, nil
@ -2377,13 +2391,20 @@ func (m *Manager) AcceptRequestToJoin(dbRequest *RequestToJoin) (*Community, err
return nil, err
}
channels, err := m.accountsSatisfyPermissionsToJoinChannels(community, revealedAccounts)
viewChannels, postChannels, err := m.accountsSatisfyPermissionsToJoinChannels(community, revealedAccounts)
if err != nil {
return nil, err
}
for channelID := range channels {
_, err = community.AddMemberToChat(channelID, pk, memberRoles)
for channelID := range viewChannels {
_, err = community.AddMemberToChat(channelID, pk, memberRoles, protobuf.CommunityMember_CHANNEL_ROLE_VIEWER)
if err != nil {
return nil, err
}
}
for channelID := range postChannels {
_, err = community.AddMemberToChat(channelID, pk, memberRoles, protobuf.CommunityMember_CHANNEL_ROLE_POSTER)
if err != nil {
return nil, err
}
@ -3460,12 +3481,12 @@ func (m *Manager) RequestsToJoinForCommunityAwaitingAddresses(id types.HexBytes)
return m.persistence.RequestsToJoinForCommunityAwaitingAddresses(id)
}
func (m *Manager) CanPost(pk *ecdsa.PublicKey, communityID string, chatID string) (bool, error) {
func (m *Manager) CanPost(pk *ecdsa.PublicKey, communityID string, chatID string, messageType protobuf.ApplicationMetadataMessage_Type) (bool, error) {
community, err := m.GetByIDString(communityID)
if err != nil {
return false, err
}
return community.CanPost(pk, chatID)
return community.CanPost(pk, chatID, messageType)
}
func (m *Manager) IsEncrypted(communityID string) (bool, error) {
@ -4794,7 +4815,7 @@ func (m *Manager) ReevaluatePrivilegedMember(community *Community, tokenPermissi
// Make sure privileged user is added to every channel
for channelID := range community.Chats() {
if !community.IsMemberInChat(memberPubKey, channelID) {
_, err := community.AddMemberToChat(channelID, memberPubKey, []protobuf.CommunityMember_Roles{privilegedRole})
_, err := community.AddMemberToChat(channelID, memberPubKey, []protobuf.CommunityMember_Roles{privilegedRole}, protobuf.CommunityMember_CHANNEL_ROLE_POSTER)
if err != nil {
return alreadyHasPrivilegedRole, err
}
@ -5002,7 +5023,7 @@ func (m *Manager) promoteSelfToControlNode(community *Community, clock uint64) (
}
for channelID := range community.Chats() {
_, err = community.AddMemberToChat(channelID, &m.identity.PublicKey, ownerRole)
_, err = community.AddMemberToChat(channelID, &m.identity.PublicKey, ownerRole, protobuf.CommunityMember_CHANNEL_ROLE_POSTER)
if err != nil {
return false, err
}

View File

@ -568,15 +568,15 @@ func waitOnCommunitiesEvent(user *Messenger, condition func(*communities.Subscri
go func() {
defer close(errCh)
subscription := user.communitiesManager.Subscribe()
for {
select {
case sub, more := <-user.communitiesManager.Subscribe():
case sub, more := <-subscription:
if !more {
errCh <- errors.New("channel closed when waiting for communities event")
return
}
if condition(sub) {
return
}

View File

@ -2,6 +2,7 @@ package protocol
import (
"bytes"
"context"
"crypto/ecdsa"
"errors"
"math/big"
@ -159,11 +160,7 @@ func (s *MessengerCommunitiesTokenPermissionsSuite) SetupTest() {
_, err = s.alice.Start()
s.Require().NoError(err)
s.mockedBalances = make(map[uint64]map[gethcommon.Address]map[gethcommon.Address]*hexutil.Big)
s.mockedBalances[testChainID1] = make(map[gethcommon.Address]map[gethcommon.Address]*hexutil.Big)
s.mockedBalances[testChainID1][gethcommon.HexToAddress(aliceAddress1)] = make(map[gethcommon.Address]*hexutil.Big)
s.mockedBalances[testChainID1][gethcommon.HexToAddress(aliceAddress2)] = make(map[gethcommon.Address]*hexutil.Big)
s.mockedBalances[testChainID1][gethcommon.HexToAddress(bobAddress)] = make(map[gethcommon.Address]*hexutil.Big)
s.resetMockedBalances()
}
@ -231,6 +228,14 @@ func (s *MessengerCommunitiesTokenPermissionsSuite) makeAddressSatisfyTheCriteri
s.mockedBalances[chainID][walletAddress][contractAddress] = (*hexutil.Big)(balance)
}
func (s *MessengerCommunitiesTokenPermissionsSuite) resetMockedBalances() {
s.mockedBalances = make(map[uint64]map[gethcommon.Address]map[gethcommon.Address]*hexutil.Big)
s.mockedBalances[testChainID1] = make(map[gethcommon.Address]map[gethcommon.Address]*hexutil.Big)
s.mockedBalances[testChainID1][gethcommon.HexToAddress(aliceAddress1)] = make(map[gethcommon.Address]*hexutil.Big)
s.mockedBalances[testChainID1][gethcommon.HexToAddress(aliceAddress2)] = make(map[gethcommon.Address]*hexutil.Big)
s.mockedBalances[testChainID1][gethcommon.HexToAddress(bobAddress)] = make(map[gethcommon.Address]*hexutil.Big)
}
func (s *MessengerCommunitiesTokenPermissionsSuite) waitOnKeyDistribution(condition func(*CommunityAndKeyActions) bool) <-chan error {
testCommunitiesKeyDistributor, ok := s.owner.communitiesKeyDistributor.(*TestCommunitiesKeyDistributor)
s.Require().True(ok)
@ -1002,9 +1007,26 @@ func (s *MessengerCommunitiesTokenPermissionsSuite) TestJoinCommunityAsAdminWith
s.Require().Equal(bobAddress, revealedAccounts[0].Address)
}
func (s *MessengerCommunitiesTokenPermissionsSuite) TestViewChannelPermissions() {
func (s *MessengerCommunitiesTokenPermissionsSuite) testViewChannelPermissions(viewersCanAddReactions bool) {
community, chat := s.createCommunity()
// setup channel reactions permissions
editedChat := &protobuf.CommunityChat{
Identity: &protobuf.ChatIdentity{
DisplayName: chat.Name,
Description: chat.Description,
Emoji: chat.Emoji,
Color: chat.Color,
},
Permissions: &protobuf.CommunityPermissions{
Access: protobuf.CommunityPermissions_AUTO_ACCEPT,
},
ViewersCanPostReactions: viewersCanAddReactions,
}
_, err := s.owner.EditCommunityChat(community.ID(), chat.ID, editedChat)
s.Require().NoError(err)
// bob joins the community
s.advertiseCommunityTo(community, s.bob)
s.joinCommunity(community, s.bob, bobPassword, []string{})
@ -1016,12 +1038,8 @@ func (s *MessengerCommunitiesTokenPermissionsSuite) TestViewChannelPermissions()
response, err := WaitOnMessengerResponse(
s.bob,
func(r *MessengerResponse) bool {
for _, message := range r.messages {
if message.Text == msg.Text {
return true
}
}
return false
_, ok := r.messages[msg.ID]
return ok
},
"no messages",
)
@ -1029,6 +1047,15 @@ func (s *MessengerCommunitiesTokenPermissionsSuite) TestViewChannelPermissions()
s.Require().Len(response.Messages(), 1)
s.Require().Equal(msg.Text, response.Messages()[0].Text)
waitOnBobToBeKickedFromChannel := waitOnCommunitiesEvent(s.owner, func(sub *communities.Subscription) bool {
channel, ok := sub.Community.Chats()[chat.CommunityChatID()]
return ok && len(channel.Members) == 1
})
waitOnChannelToBeRekeyedOnceBobIsKicked := s.waitOnKeyDistribution(func(sub *CommunityAndKeyActions) bool {
action, ok := sub.keyActions.ChannelKeysActions[chat.CommunityChatID()]
return ok && (action.ActionType == communities.EncryptionKeyRekey || action.ActionType == communities.EncryptionKeyAdd)
})
// setup view channel permission
channelPermissionRequest := requests.CreateCommunityTokenPermission{
CommunityID: community.ID(),
@ -1045,24 +1072,6 @@ func (s *MessengerCommunitiesTokenPermissionsSuite) TestViewChannelPermissions()
ChatIds: []string{chat.ID},
}
waitOnBobToBeKickedFromChannel := waitOnCommunitiesEvent(s.owner, func(sub *communities.Subscription) bool {
for channelID, channel := range sub.Community.Chats() {
if channelID == chat.CommunityChatID() && len(channel.Members) == 1 {
return true
}
}
return false
})
waitOnChannelToBeRekeyedOnceBobIsKicked := s.waitOnKeyDistribution(func(sub *CommunityAndKeyActions) bool {
for channelID, action := range sub.keyActions.ChannelKeysActions {
// We both listen for Rekey or Add, since the first time around the community goes from non-encrypted to encrypted, and that's an Add
if channelID == chat.CommunityChatID() && (action.ActionType == communities.EncryptionKeyRekey || action.ActionType == communities.EncryptionKeyAdd) {
return true
}
}
return false
})
response, err = s.owner.CreateCommunityTokenPermission(&channelPermissionRequest)
s.Require().NoError(err)
s.Require().Len(response.Communities(), 1)
@ -1080,15 +1089,14 @@ func (s *MessengerCommunitiesTokenPermissionsSuite) TestViewChannelPermissions()
_, err = WaitOnMessengerResponse(
s.bob,
func(r *MessengerResponse) bool {
community, err := s.bob.GetCommunityByID(community.ID())
c, err := s.bob.GetCommunityByID(community.ID())
if err != nil {
return false
}
if community == nil {
if c == nil {
return false
}
channel := community.Chats()[chat.CommunityChatID()]
channel := c.Chats()[chat.CommunityChatID()]
return channel != nil && len(channel.Members) == 0
},
"no community that satisfies criteria",
@ -1097,19 +1105,15 @@ func (s *MessengerCommunitiesTokenPermissionsSuite) TestViewChannelPermissions()
// make bob satisfy channel criteria
s.makeAddressSatisfyTheCriteria(testChainID1, bobAddress, channelPermissionRequest.TokenCriteria[0])
defer s.resetMockedBalances() // reset mocked balances, this test in run with different test cases
waitOnChannelKeyToBeDistributedToBob := s.waitOnKeyDistribution(func(sub *CommunityAndKeyActions) bool {
for channelID, action := range sub.keyActions.ChannelKeysActions {
if channelID == chat.CommunityChatID() && action.ActionType == communities.EncryptionKeySendToMembers {
for memberPubKey := range action.Members {
if memberPubKey == common.PubkeyToHex(&s.bob.identity.PublicKey) {
return true
}
}
}
action, ok := sub.keyActions.ChannelKeysActions[chat.CommunityChatID()]
if !ok || action.ActionType != communities.EncryptionKeySendToMembers {
return false
}
return false
_, ok = action.Members[common.PubkeyToHex(&s.bob.identity.PublicKey)]
return ok
})
// force owner to reevaluate channel members
@ -1129,18 +1133,64 @@ func (s *MessengerCommunitiesTokenPermissionsSuite) TestViewChannelPermissions()
response, err = WaitOnMessengerResponse(
s.bob,
func(r *MessengerResponse) bool {
for _, message := range r.messages {
if message.Text == msg.Text {
return true
}
}
return false
_, ok := r.messages[msg.ID]
return ok
},
"no messages",
)
s.Require().NoError(err)
s.Require().Len(response.Messages(), 1)
s.Require().Equal(msg.Text, response.Messages()[0].Text)
// bob can/can't post reactions
response, err = s.bob.SendEmojiReaction(context.Background(), chat.ID, msg.ID, protobuf.EmojiReaction_THUMBS_UP)
if !viewersCanAddReactions {
s.Require().Error(err)
} else {
s.Require().NoError(err)
s.Require().Len(response.emojiReactions, 1)
reactionMessage := response.EmojiReactions()[0]
response, err = WaitOnMessengerResponse(
s.owner,
func(r *MessengerResponse) bool {
_, ok := r.emojiReactions[reactionMessage.ID()]
return ok
},
"no reactions received",
)
if viewersCanAddReactions {
s.Require().NoError(err)
s.Require().Len(response.EmojiReactions(), 1)
s.Require().Equal(response.EmojiReactions()[0].Type, protobuf.EmojiReaction_THUMBS_UP)
} else {
s.Require().Error(err)
s.Require().Len(response.EmojiReactions(), 0)
}
}
}
func (s *MessengerCommunitiesTokenPermissionsSuite) TestViewChannelPermissions() {
testCases := []struct {
name string
viewersCanPostReactions bool
}{
{
name: "viewers are allowed to post reactions",
viewersCanPostReactions: true,
},
{
name: "viewers are forbidden to post reactions",
viewersCanPostReactions: false,
},
}
for _, tc := range testCases {
s.T().Run(tc.name, func(*testing.T) {
s.testViewChannelPermissions(tc.viewersCanPostReactions)
})
}
}
func (s *MessengerCommunitiesTokenPermissionsSuite) testReevaluateMemberPrivilegedRoleInOpenCommunity(permissionType protobuf.CommunityTokenPermission_Type) {

View File

@ -2227,6 +2227,7 @@ func (m *Messenger) dispatchMessage(ctx context.Context, rawMessage common.RawMe
if err != nil {
return rawMessage, err
}
case ChatTypeCommunityChat:
community, err := m.communitiesManager.GetByIDString(chat.CommunityID)
if err != nil {
@ -2234,14 +2235,18 @@ func (m *Messenger) dispatchMessage(ctx context.Context, rawMessage common.RawMe
}
rawMessage.PubsubTopic = community.PubsubTopic()
canPost, err := m.communitiesManager.CanPost(&m.identity.PublicKey, chat.CommunityID, chat.CommunityChatID())
canPost, err := m.communitiesManager.CanPost(&m.identity.PublicKey, chat.CommunityID, chat.CommunityChatID(), rawMessage.MessageType)
if err != nil {
return rawMessage, err
}
if !canPost {
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")
m.logger.Error("can't post on chat",
zap.String("chatID", chat.ID),
zap.String("chatName", chat.Name),
zap.Any("messageType", rawMessage.MessageType),
)
return rawMessage, fmt.Errorf("can't post message type '%d' on chat '%s'", rawMessage.MessageType, chat.ID)
}
logger.Debug("sending community chat message", zap.String("chatName", chat.Name))
@ -5688,143 +5693,6 @@ func generateAliasAndIdenticon(pk string) (string, string, error) {
}
func (m *Messenger) SendEmojiReaction(ctx context.Context, chatID, messageID string, emojiID protobuf.EmojiReaction_Type) (*MessengerResponse, error) {
var response MessengerResponse
chat, ok := m.allChats.Load(chatID)
if !ok {
return nil, ErrChatNotFound
}
clock, _ := chat.NextClockAndTimestamp(m.getTimesource())
emojiR := &EmojiReaction{
EmojiReaction: &protobuf.EmojiReaction{
Clock: clock,
MessageId: messageID,
ChatId: chatID,
Type: emojiID,
},
LocalChatID: chatID,
From: types.EncodeHex(crypto.FromECDSAPub(&m.identity.PublicKey)),
}
encodedMessage, err := m.encodeChatEntity(chat, emojiR)
if err != nil {
return nil, err
}
_, err = m.dispatchMessage(ctx, common.RawMessage{
LocalChatID: chatID,
Payload: encodedMessage,
SkipGroupMessageWrap: true,
MessageType: protobuf.ApplicationMetadataMessage_EMOJI_REACTION,
// Don't resend using datasync, that would create quite a lot
// of traffic if clicking too eagelry
ResendAutomatically: false,
})
if err != nil {
return nil, err
}
response.AddEmojiReaction(emojiR)
response.AddChat(chat)
err = m.persistence.SaveEmojiReaction(emojiR)
if err != nil {
return nil, errors.Wrap(err, "Can't save emoji reaction in db")
}
return &response, nil
}
func (m *Messenger) EmojiReactionsByChatID(chatID string, cursor string, limit int) ([]*EmojiReaction, error) {
chat, err := m.persistence.Chat(chatID)
if err != nil {
return nil, err
}
if chat.Timeline() {
var chatIDs = []string{"@" + contactIDFromPublicKey(&m.identity.PublicKey)}
m.allContacts.Range(func(contactID string, contact *Contact) (shouldContinue bool) {
if contact.added() {
chatIDs = append(chatIDs, "@"+contact.ID)
}
return true
})
return m.persistence.EmojiReactionsByChatIDs(chatIDs, cursor, limit)
}
return m.persistence.EmojiReactionsByChatID(chatID, cursor, limit)
}
func (m *Messenger) EmojiReactionsByChatIDMessageID(chatID string, messageID string) ([]*EmojiReaction, error) {
_, err := m.persistence.Chat(chatID)
if err != nil {
return nil, err
}
return m.persistence.EmojiReactionsByChatIDMessageID(chatID, messageID)
}
func (m *Messenger) SendEmojiReactionRetraction(ctx context.Context, emojiReactionID string) (*MessengerResponse, error) {
emojiR, err := m.persistence.EmojiReactionByID(emojiReactionID)
if err != nil {
return nil, err
}
// Check that the sender is the key owner
pk := types.EncodeHex(crypto.FromECDSAPub(&m.identity.PublicKey))
if emojiR.From != pk {
return nil, errors.Errorf("identity mismatch, "+
"emoji reactions can only be retracted by the reaction sender, "+
"emoji reaction sent by '%s', current identity '%s'",
emojiR.From, pk,
)
}
// Get chat and clock
chat, ok := m.allChats.Load(emojiR.GetChatId())
if !ok {
return nil, ErrChatNotFound
}
clock, _ := chat.NextClockAndTimestamp(m.getTimesource())
// Update the relevant fields
emojiR.Clock = clock
emojiR.Retracted = true
encodedMessage, err := m.encodeChatEntity(chat, emojiR)
if err != nil {
return nil, err
}
// Send the marshalled EmojiReactionRetraction protobuf
_, err = m.dispatchMessage(ctx, common.RawMessage{
LocalChatID: emojiR.GetChatId(),
Payload: encodedMessage,
SkipGroupMessageWrap: true,
MessageType: protobuf.ApplicationMetadataMessage_EMOJI_REACTION,
// Don't resend using datasync, that would create quite a lot
// of traffic if clicking too eagelry
ResendAutomatically: false,
})
if err != nil {
return nil, err
}
// Update MessengerResponse
response := MessengerResponse{}
emojiR.Retracted = true
response.AddEmojiReaction(emojiR)
response.AddChat(chat)
// Persist retraction state for emoji reaction
err = m.persistence.SaveEmojiReaction(emojiR)
if err != nil {
return nil, err
}
return &response, nil
}
func (m *Messenger) encodeChatEntity(chat *Chat, message common.ChatEntity) ([]byte, error) {
var encodedMessage []byte
var err error

View File

@ -0,0 +1,149 @@
package protocol
import (
"context"
"github.com/pkg/errors"
"github.com/status-im/status-go/eth-node/crypto"
"github.com/status-im/status-go/eth-node/types"
"github.com/status-im/status-go/protocol/common"
"github.com/status-im/status-go/protocol/protobuf"
)
func (m *Messenger) SendEmojiReaction(ctx context.Context, chatID, messageID string, emojiID protobuf.EmojiReaction_Type) (*MessengerResponse, error) {
var response MessengerResponse
chat, ok := m.allChats.Load(chatID)
if !ok {
return nil, ErrChatNotFound
}
clock, _ := chat.NextClockAndTimestamp(m.getTimesource())
emojiR := &EmojiReaction{
EmojiReaction: &protobuf.EmojiReaction{
Clock: clock,
MessageId: messageID,
ChatId: chatID,
Type: emojiID,
},
LocalChatID: chatID,
From: types.EncodeHex(crypto.FromECDSAPub(&m.identity.PublicKey)),
}
encodedMessage, err := m.encodeChatEntity(chat, emojiR)
if err != nil {
return nil, err
}
_, err = m.dispatchMessage(ctx, common.RawMessage{
LocalChatID: chatID,
Payload: encodedMessage,
SkipGroupMessageWrap: true,
MessageType: protobuf.ApplicationMetadataMessage_EMOJI_REACTION,
// Don't resend using datasync, that would create quite a lot
// of traffic if clicking too eagelry
ResendAutomatically: false,
})
if err != nil {
return nil, err
}
response.AddEmojiReaction(emojiR)
response.AddChat(chat)
err = m.persistence.SaveEmojiReaction(emojiR)
if err != nil {
return nil, errors.Wrap(err, "Can't save emoji reaction in db")
}
return &response, nil
}
func (m *Messenger) EmojiReactionsByChatID(chatID string, cursor string, limit int) ([]*EmojiReaction, error) {
chat, err := m.persistence.Chat(chatID)
if err != nil {
return nil, err
}
if chat.Timeline() {
var chatIDs = []string{"@" + contactIDFromPublicKey(&m.identity.PublicKey)}
m.allContacts.Range(func(contactID string, contact *Contact) (shouldContinue bool) {
if contact.added() {
chatIDs = append(chatIDs, "@"+contact.ID)
}
return true
})
return m.persistence.EmojiReactionsByChatIDs(chatIDs, cursor, limit)
}
return m.persistence.EmojiReactionsByChatID(chatID, cursor, limit)
}
func (m *Messenger) EmojiReactionsByChatIDMessageID(chatID string, messageID string) ([]*EmojiReaction, error) {
_, err := m.persistence.Chat(chatID)
if err != nil {
return nil, err
}
return m.persistence.EmojiReactionsByChatIDMessageID(chatID, messageID)
}
func (m *Messenger) SendEmojiReactionRetraction(ctx context.Context, emojiReactionID string) (*MessengerResponse, error) {
emojiR, err := m.persistence.EmojiReactionByID(emojiReactionID)
if err != nil {
return nil, err
}
// Check that the sender is the key owner
pk := types.EncodeHex(crypto.FromECDSAPub(&m.identity.PublicKey))
if emojiR.From != pk {
return nil, errors.Errorf("identity mismatch, "+
"emoji reactions can only be retracted by the reaction sender, "+
"emoji reaction sent by '%s', current identity '%s'",
emojiR.From, pk,
)
}
// Get chat and clock
chat, ok := m.allChats.Load(emojiR.GetChatId())
if !ok {
return nil, ErrChatNotFound
}
clock, _ := chat.NextClockAndTimestamp(m.getTimesource())
// Update the relevant fields
emojiR.Clock = clock
emojiR.Retracted = true
encodedMessage, err := m.encodeChatEntity(chat, emojiR)
if err != nil {
return nil, err
}
// Send the marshalled EmojiReactionRetraction protobuf
_, err = m.dispatchMessage(ctx, common.RawMessage{
LocalChatID: emojiR.GetChatId(),
Payload: encodedMessage,
SkipGroupMessageWrap: true,
MessageType: protobuf.ApplicationMetadataMessage_EMOJI_REACTION,
// Don't resend using datasync, that would create quite a lot
// of traffic if clicking too eagelry
ResendAutomatically: false,
})
if err != nil {
return nil, err
}
// Update MessengerResponse
response := MessengerResponse{}
emojiR.Retracted = true
response.AddEmojiReaction(emojiR)
response.AddChat(chat)
// Persist retraction state for emoji reaction
err = m.persistence.SaveEmojiReaction(emojiR)
if err != nil {
return nil, err
}
return &response, nil
}

View File

@ -393,7 +393,26 @@ func (m *Messenger) handleCommandMessage(state *ReceivedMessageState, message *c
if err := message.PrepareContent(common.PubkeyToHex(&m.identity.PublicKey)); err != nil {
return fmt.Errorf("failed to prepare content: %v", err)
}
chat, err := m.matchChatEntity(message)
// Get Application layer messageType from commandState
// Currently this is not really used in `matchChatEntity`, but I did want to pass UNKNOWN there.
var messageType protobuf.ApplicationMetadataMessage_Type
switch message.CommandParameters.CommandState {
case common.CommandStateRequestAddressForTransaction:
messageType = protobuf.ApplicationMetadataMessage_REQUEST_ADDRESS_FOR_TRANSACTION
case common.CommandStateRequestAddressForTransactionAccepted:
messageType = protobuf.ApplicationMetadataMessage_ACCEPT_REQUEST_ADDRESS_FOR_TRANSACTION
case common.CommandStateRequestAddressForTransactionDeclined:
messageType = protobuf.ApplicationMetadataMessage_DECLINE_REQUEST_ADDRESS_FOR_TRANSACTION
case common.CommandStateRequestTransaction:
messageType = protobuf.ApplicationMetadataMessage_REQUEST_TRANSACTION
case common.CommandStateRequestTransactionDeclined:
messageType = protobuf.ApplicationMetadataMessage_DECLINE_REQUEST_TRANSACTION
default:
messageType = protobuf.ApplicationMetadataMessage_UNKNOWN
}
chat, err := m.matchChatEntity(message, messageType)
if err != nil {
return err
}
@ -862,7 +881,7 @@ func (m *Messenger) handlePinMessage(pinner *Contact, whisperTimestamp uint64, r
Alias: pinner.Alias,
}
chat, err := m.matchChatEntity(pinMessage)
chat, err := m.matchChatEntity(pinMessage, protobuf.ApplicationMetadataMessage_PIN_MESSAGE)
if err != nil {
return err // matchChatEntity returns a descriptive error message
}
@ -2127,7 +2146,7 @@ func (m *Messenger) handleChatMessage(state *ReceivedMessageState, forceSeen boo
}
}
chat, err := m.matchChatEntity(receivedMessage)
chat, err := m.matchChatEntity(receivedMessage, protobuf.ApplicationMetadataMessage_CHAT_MESSAGE)
if err != nil {
return err // matchChatEntity returns a descriptive error message
}
@ -2684,7 +2703,7 @@ func (m *Messenger) HandleDeclineRequestTransaction(messageState *ReceivedMessag
return m.handleCommandMessage(messageState, oldMessage)
}
func (m *Messenger) matchChatEntity(chatEntity common.ChatEntity) (*Chat, error) {
func (m *Messenger) matchChatEntity(chatEntity common.ChatEntity, messageType protobuf.ApplicationMetadataMessage_Type) (*Chat, error) {
if chatEntity.GetSigPubKey() == nil {
m.logger.Error("public key can't be empty")
return nil, errors.New("received a chatEntity with empty public key")
@ -2753,7 +2772,7 @@ func (m *Messenger) matchChatEntity(chatEntity common.ChatEntity) (*Chat, error)
return nil, errors.New("not an community chat")
}
canPost, err := m.communitiesManager.CanPost(chatEntity.GetSigPubKey(), chat.CommunityID, chat.CommunityChatID())
canPost, err := m.communitiesManager.CanPost(chatEntity.GetSigPubKey(), chat.CommunityID, chat.CommunityChatID(), messageType)
if err != nil {
return nil, err
}
@ -2762,21 +2781,8 @@ func (m *Messenger) matchChatEntity(chatEntity common.ChatEntity) (*Chat, error)
return nil, errors.New("user can't post in community")
}
_, isPinMessage := chatEntity.(*common.PinMessage)
if isPinMessage {
community, err := m.communitiesManager.GetByIDString(chat.CommunityID)
if err != nil {
return nil, err
}
hasPermission := community.IsPrivilegedMember(chatEntity.GetSigPubKey())
pinMessageAllowed := community.AllowsAllMembersToPinMessage()
if !hasPermission && !pinMessageAllowed {
return nil, errors.New("user can't pin message")
}
}
return chat, nil
case chatEntity.GetMessageType() == protobuf.MessageType_PRIVATE_GROUP:
// In the case of a group chatEntity, ChatID is the same for all messages belonging to a group.
// It needs to be verified if the signature public key belongs to the chat.
@ -2853,7 +2859,7 @@ func (m *Messenger) HandleEmojiReaction(state *ReceivedMessageState, pbEmojiR *p
return nil
}
chat, err := m.matchChatEntity(emojiReaction)
chat, err := m.matchChatEntity(emojiReaction, protobuf.ApplicationMetadataMessage_EMOJI_REACTION)
if err != nil {
return err // matchChatEntity returns a descriptive error message
}

View File

@ -31,24 +31,6 @@ func (m *Messenger) sendPinMessage(ctx context.Context, message *common.PinMessa
return nil, errors.New("chat not found")
}
if chat.CommunityChat() {
community, err := m.communitiesManager.GetByIDString(chat.CommunityID)
if err != nil {
return nil, err
}
hasPermission := community.IsPrivilegedMember(&m.identity.PublicKey)
pinMessageAllowed := community.AllowsAllMembersToPinMessage()
canPost, err := community.CanPost(&m.identity.PublicKey, chat.CommunityChatID())
if err != nil {
return nil, err
}
if !canPost && !pinMessageAllowed && !hasPermission {
return nil, errors.New("can't pin message")
}
}
err := m.handleStandaloneChatIdentity(chat)
if err != nil {
return nil, err

File diff suppressed because it is too large Load Diff

View File

@ -23,9 +23,16 @@ message CommunityMember {
ROLE_ADMIN = 4;
ROLE_TOKEN_MASTER = 5;
}
enum ChannelRole {
// We make POSTER the first role to be the default one.
// This is for backwards compatibility. Older protobufs won't have this field and will default to 0.
CHANNEL_ROLE_POSTER = 0;
CHANNEL_ROLE_VIEWER = 1;
}
repeated Roles roles = 1;
repeated RevealedAccount revealed_accounts = 2 [deprecated = true];
uint64 last_update_clock = 3;
ChannelRole channel_role = 4;
}
message CommunityTokenMetadata {
@ -121,6 +128,7 @@ message CommunityChat {
ChatIdentity identity = 3;
string category_id = 4;
int32 position = 5;
bool viewers_can_post_reactions = 6;
}
message CommunityCategory {

View File

@ -77,8 +77,12 @@ type Chat struct {
FirstMessageTimestamp uint32 `json:"firstMessageTimestamp,omitempty"`
Highlight bool `json:"highlight,omitempty"`
PinnedMessages *PinnedMessages `json:"pinnedMessages,omitempty"`
CanPost bool `json:"canPost"`
Base64Image string `json:"image,omitempty"`
// Deprecated: CanPost is deprecated in favor of CanPostMessages/CanPostReactions/etc.
// For now CanPost will equal to CanPostMessages.
CanPost bool `json:"canPost"`
CanPostMessages bool `json:"canPostMessages"`
CanPostReactions bool `json:"canPostReactions"`
Base64Image string `json:"image,omitempty"`
}
type ChannelGroup struct {
@ -482,7 +486,12 @@ func (chat *Chat) populateCommunityFields(community *communities.Community) erro
return nil
}
canPost, err := community.CanMemberIdentityPost(chat.ID)
canPostMessages, err := community.CanMemberIdentityPost(chat.ID, protobuf.ApplicationMetadataMessage_CHAT_MESSAGE)
if err != nil {
return err
}
canPostReactions, err := community.CanMemberIdentityPost(chat.ID, protobuf.ApplicationMetadataMessage_EMOJI_REACTION)
if err != nil {
return err
}
@ -493,7 +502,9 @@ func (chat *Chat) populateCommunityFields(community *communities.Community) erro
chat.Emoji = commChat.Identity.Emoji
chat.Name = commChat.Identity.DisplayName
chat.Description = commChat.Identity.Description
chat.CanPost = canPost
chat.CanPost = canPostMessages
chat.CanPostMessages = canPostMessages
chat.CanPostReactions = canPostReactions
return nil
}