diff --git a/src/Simplex/Chat/Library/Subscriber.hs b/src/Simplex/Chat/Library/Subscriber.hs index 450a40a9d9..8113bf4df4 100644 --- a/src/Simplex/Chat/Library/Subscriber.hs +++ b/src/Simplex/Chat/Library/Subscriber.hs @@ -76,7 +76,7 @@ import qualified Simplex.FileTransfer.Transport as XFTP import Simplex.FileTransfer.Types (FileErrorType (..), RcvFileId, SndFileId) import Simplex.Messaging.Agent import Simplex.Messaging.Agent.Client (getAgentWorker, temporaryOrHostError, waitForUserNetwork, waitForWork, waitWhileSuspended, withWorkItems, withWork_) -import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), Worker (..)) +import Simplex.Messaging.Agent.Env.SQLite (Worker (..)) import Simplex.Messaging.Agent.Protocol import qualified Simplex.Messaging.Agent.Protocol as AP (AgentErrorType (..)) import Simplex.Messaging.Agent.RetryInterval (RetryInterval (..), nextRetryDelay) @@ -3129,7 +3129,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = | otherwise = withStore' (\db -> runExceptT $ getGroupMemberByMemberId db vr user gInfo memId) >>= \case Right member -> changeMemberRole gInfo member $ RGEMemberRole (groupMemberId' member) (fromLocalProfile $ memberProfile member) memRole - -- in relay groups the roster delivers the (privileged) member separately; no-op until then + -- in channels the roster delivers the chat item for previously-unknown privileged members Left _ | useRelays' gInfo -> pure Nothing | otherwise -> messageError "x.grp.mem.role with unknown member ID" $> Nothing @@ -3177,29 +3177,37 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = let GroupRoster {roster = entries} = validRoster rosterIds = map (\RosterMember {memberId} -> memberId) entries defaultRole <- unknownMemberRole gInfo - conflicts <- withStore $ \db -> do - conflicts <- catMaybes <$> mapM (applyRosterEntryDB db) entries + (conflicts, applied) <- withStore $ \db -> do + acc <- foldrM (applyRosterEntry db defaultRole) ([], []) entries -- absent privileged members revert to the joiner default currentPriv <- liftIO $ getGroupRosterMembers db vr user gInfo liftIO $ forM_ currentPriv $ \m -> when (memberId' m `notElem` rosterIds) $ updateGroupMemberRole db user m defaultRole - pure conflicts + pure acc forM_ conflicts $ \mid' -> messageWarning $ "x.grp.roster: member key conflict, keeping trusted key, memberId=" <> safeDecodeUtf8 (strEncode mid') - applyRosterEntryDB :: DB.Connection -> RosterMember -> ExceptT StoreError IO (Maybe MemberId) - applyRosterEntryDB db RosterMember {memberId, name, key = MemberKey pubKey, role} = - getCreateUnknownGMByMemberId db vr user gInfo memberId name role True >>= \case - Just (m, _created) -> case memberPubKey m of - Just k - | k == pubKey -> Nothing <$ liftIO (unless (memberRole' m == role) $ updateGroupMemberRole db user m role) - | otherwise -> pure (Just memberId) - Nothing -> Nothing <$ liftIO (setGroupMemberKeyRole db m pubKey role) - Nothing -> pure Nothing - signedMsgBytes :: Maybe ByteString - signedMsgBytes = case verifiedMsg of - VMSigned _ sm _ -> Just $ smpEncode sm - VMUnsigned _ -> Nothing + forM_ applied $ \(member, fromRole) -> createItems member fromRole + applyRosterEntry :: DB.Connection -> GroupMemberRole -> RosterMember -> ([MemberId], [(GroupMember, GroupMemberRole)]) -> ExceptT StoreError IO ([MemberId], [(GroupMember, GroupMemberRole)]) + applyRosterEntry db defaultRole RosterMember {memberId, name, key = MemberKey pubKey, role} (cs, as) = + applyEntry `catchAllErrors` \_ -> pure (cs, as) + where + applied m = (cs, ((m :: GroupMember) {memberRole = role}, memberRole' m) : as) + applyEntry = getCreateUnknownGMByMemberId db vr user gInfo memberId name defaultRole True >>= \case + Nothing -> pure (cs, as) + Just (m, _) -> case memberPubKey m of + Just k + | k /= pubKey -> pure (memberId : cs, as) + | memberRole' m == role -> pure (cs, as) + | otherwise -> liftIO (updateGroupMemberRole db user m role) $> applied m + Nothing -> liftIO (setGroupMemberKeyRole db m pubKey role) $> applied m + createItems :: GroupMember -> GroupMemberRole -> CM () + createItems member fromRole = do + let toRole = memberRole' member + gEvent = RGEMemberRole (groupMemberId' member) (fromLocalProfile $ memberProfile member) toRole + (gInfo', author', scopeInfo) <- mkGroupChatScope gInfo author + createInternalChatItem user (CDGroupRcv gInfo' scopeInfo author') (CIRcvGroupEvent gEvent) (Just brokerTs) + toView CEvtMemberRole {user, groupInfo = gInfo', byMember = author', member, fromRole, toRole, msgSigned = Just MSSVerified} checkHostRole :: GroupMember -> GroupMemberRole -> CM () checkHostRole GroupMember {memberRole, localDisplayName} memRole = diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs index 8443ecc0e9..4d41e5dc89 100644 --- a/tests/ChatTests/Groups.hs +++ b/tests/ChatTests/Groups.hs @@ -9409,18 +9409,23 @@ testChannelChangeRoleSigned ps = dan #$> ("/_get chat #1 count=1", chat, [(0, "changed role of cath to admin (signed)")]) eve #$> ("/_get chat #1 count=1", chat, [(0, "changed role of cath to admin (signed)")]) - -- change role of silent member; cath/eve don't know dan, so the relay-group branch - -- in xGrpMemRole silently no-ops (the roster will deliver the privileged set separately) + -- change role of silent member; cath/eve don't know dan via xGrpMemRole, but the + -- subsequent roster apply emits the chat item with dan TOFU-created at the new role threadDelay 1000000 alice ##> "/mr #team dan admin" alice <## "#team: you changed the role of dan to admin (signed)" - bob <## "#team: alice changed the role of dan from member to admin (signed)" - dan <## "#team: alice changed your role from member to admin (signed)" + concurrentlyN_ + [ bob <## "#team: alice changed the role of dan from member to admin (signed)", + dan <## "#team: alice changed your role from member to admin (signed)", + cath <## "#team: alice changed the role of dan from member to admin (signed)", + eve <## "#team: alice changed the role of dan from member to admin (signed)" + ] alice #$> ("/_get chat #1 count=1", chat, [(1, "changed role of dan to admin (signed)")]) bob #$> ("/_get chat #1 count=1", chat, [(0, "changed role of dan to admin (signed)")]) - cath #$> ("/_get chat #1 count=1", chat, [(0, "changed your role to admin (signed)")]) -- now new chat item + -- roster-emitted items don't carry the signed marker in chat content text (live event does) + cath #$> ("/_get chat #1 count=1", chat, [(0, "changed role of dan to admin")]) dan #$> ("/_get chat #1 count=1", chat, [(0, "changed your role to admin (signed)")]) - eve #$> ("/_get chat #1 count=1", chat, [(0, "changed role of cath to admin (signed)")]) -- now new chat item + eve #$> ("/_get chat #1 count=1", chat, [(0, "changed role of dan to admin")]) testChannelBlockMemberSigned :: HasCallStack => TestParams -> IO () testChannelBlockMemberSigned ps = @@ -9518,14 +9523,17 @@ testChannelModeratorActionViaRoster ps = eve <# "#team dan> hello from dan [>>]" ] - -- alice promotes cath. dan/eve don't know cath yet, so their XGrpMemRole is silently - -- skipped (channel branch in xGrpMemRole returns Nothing) until the roster arrives - -- and creates cath's record with the new role. + -- dan/eve XGrpMemRole is skipped (target unknown); roster apply creates cath + -- and emits the role-change chat item threadDelay 1000000 alice ##> "/mr #team cath moderator" alice <## "#team: you changed the role of cath to moderator (signed)" - bob <## "#team: alice changed the role of cath from member to moderator (signed)" - cath <## "#team: alice changed your role from member to moderator (signed)" + concurrentlyN_ + [ bob <## "#team: alice changed the role of cath from member to moderator (signed)", + cath <## "#team: alice changed your role from member to moderator (signed)", + dan <## "#team: alice changed the role of cath from member to moderator (signed)", + eve <## "#team: alice changed the role of cath from member to moderator (signed)" + ] -- cath (moderator) blocks dan; profile prepend carries cath's full profile to dan/eve threadDelay 1000000 @@ -9540,9 +9548,12 @@ testChannelModeratorActionViaRoster ps = dan <## "#team: bob introduced cath (Catherine) in the channel" -- frank joins after the roster update; cached roster gives him cath as moderator. - -- both alice (owner) and cath (mod) receive XGrpMemNew(frank) via introduceInChannel + -- both alice (owner) and cath (mod) receive XGrpMemNew(frank) via introduceInChannel. + -- the roster apply also emits the role-change chat item on frank's side (owner + -- profile may not be loaded yet, so the actor renders by memberId hash) threadDelay 1000000 memberJoinChannel "team" [bob] [alice, cath] shortLink fullLink frank + frank <### [EndsWith "changed the role of cath from member to moderator (signed)"] threadDelay 500000 checkMemberRole frank "cath" "moderator" where @@ -9563,12 +9574,16 @@ testChannelRemovedModeratorRefreshesRoster ps = (shortLink, fullLink) <- prepareChannel1Relay "team" alice bob forM_ [cath, dan, eve] $ \member -> memberJoinChannel "team" [bob] [alice] shortLink fullLink member - -- dan/eve don't know cath; XGrpMemRole is silently skipped (relay-group branch) + -- dan/eve XGrpMemRole is skipped; roster apply creates cath and emits the chat item threadDelay 1000000 alice ##> "/mr #team cath moderator" alice <## "#team: you changed the role of cath to moderator (signed)" - bob <## "#team: alice changed the role of cath from member to moderator (signed)" - cath <## "#team: alice changed your role from member to moderator (signed)" + concurrentlyN_ + [ bob <## "#team: alice changed the role of cath from member to moderator (signed)", + cath <## "#team: alice changed your role from member to moderator (signed)", + dan <## "#team: alice changed the role of cath from member to moderator (signed)", + eve <## "#team: alice changed the role of cath from member to moderator (signed)" + ] threadDelay 1000000 alice ##> "/rm #team cath" alice <## "#team: you removed cath from the group (signed)" @@ -9595,12 +9610,16 @@ testChannelLeftModeratorDropsFromRoster ps = (shortLink, fullLink) <- prepareChannel1Relay "team" alice bob forM_ [cath, dan, eve] $ \member -> memberJoinChannel "team" [bob] [alice] shortLink fullLink member - -- promote cath; dan/eve unknown branch silently no-ops, they pick up cath from the roster + -- promote cath; dan/eve XGrpMemRole skipped, roster apply emits the chat item threadDelay 1000000 alice ##> "/mr #team cath moderator" alice <## "#team: you changed the role of cath to moderator (signed)" - bob <## "#team: alice changed the role of cath from member to moderator (signed)" - cath <## "#team: alice changed your role from member to moderator (signed)" + concurrentlyN_ + [ bob <## "#team: alice changed the role of cath from member to moderator (signed)", + cath <## "#team: alice changed your role from member to moderator (signed)", + dan <## "#team: alice changed the role of cath from member to moderator (signed)", + eve <## "#team: alice changed the role of cath from member to moderator (signed)" + ] -- cath (moderator) leaves; owner xGrpLeave refreshes the cached roster threadDelay 1000000 cath ##> "/leave #team" @@ -9631,13 +9650,16 @@ testChannelRoleTransitionsUpdateRoster ps = (shortLink, fullLink) <- prepareChannel1Relay "team" alice bob forM_ [cath, dan, eve] $ \member -> memberJoinChannel "team" [bob] [alice] shortLink fullLink member - -- member -> moderator: dan/eve don't know cath, role event silently skipped; - -- the subsequent roster broadcast gives them cath as moderator + -- member -> moderator: dan/eve XGrpMemRole skipped; roster apply creates cath and emits chat item threadDelay 1000000 alice ##> "/mr #team cath moderator" alice <## "#team: you changed the role of cath to moderator (signed)" - bob <## "#team: alice changed the role of cath from member to moderator (signed)" - cath <## "#team: alice changed your role from member to moderator (signed)" + concurrentlyN_ + [ bob <## "#team: alice changed the role of cath from member to moderator (signed)", + cath <## "#team: alice changed your role from member to moderator (signed)", + dan <## "#team: alice changed the role of cath from member to moderator (signed)", + eve <## "#team: alice changed the role of cath from member to moderator (signed)" + ] -- moderator -> admin (within roster): dan/eve now know cath, so role event lands cleanly threadDelay 1000000 alice ##> "/mr #team cath admin"