This commit is contained in:
spaced4ndy
2026-05-29 13:04:14 +04:00
parent 5524b7b45f
commit 88217a6156
2 changed files with 70 additions and 40 deletions
+26 -18
View File
@@ -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 =
+44 -22
View File
@@ -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"