From 8b5d888cc27b465df19f8d9db85b56bc0e4aaac1 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Fri, 26 Jun 2026 15:13:15 +0400 Subject: [PATCH] wip --- src/Simplex/Chat/Library/Commands.hs | 6 ++++-- src/Simplex/Chat/Library/Internal.hs | 22 ++++++++++++++++++---- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index eada7e5a1b..535a566ae8 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -2770,8 +2770,9 @@ processChatCommand cxt nm = \case (errs1, changed1) <- changeRoleInvitedMems user gInfo invitedMems let doBumpRoster = useRelays' gInfo && memberRole' (membership gInfo) == GROwner && (isRosterRole newRole || anyPrivilegedTarget) rosterVer <- if doBumpRoster then Just <$> reserveRosterVersion gInfo else pure Nothing + -- roster (with the change projected in) before the delta, so a relay stores the blob at this version before forwarding the delta + forM_ rosterVer $ \v -> broadcastRoster user gInfo v (RDRoleChanged newRole currentMems) `catchAllErrors` eToView (errs2, changed2, acis, msgSigned) <- changeRoleCurrentMems user g rosterVer currentMems - forM_ rosterVer $ \v -> broadcastRoster user gInfo v `catchAllErrors` eToView unless (null acis) $ toView $ CEvtNewChatItems user acis let errs = errs1 <> errs2 unless (null errs) $ toView $ CEvtChatErrors errs @@ -2898,13 +2899,14 @@ processChatCommand cxt nm = \case let recipients = filter memberCurrent members let doBumpRoster = useRelays' gInfo && memberRole' (membership gInfo) == GROwner && anyPrivilegedRemoved rosterVer <- if doBumpRoster then Just <$> reserveRosterVersion gInfo else pure Nothing + -- roster (excluding the removed members) before the delta, so a relay stores the blob at this version before forwarding the delta + forM_ rosterVer $ \v -> broadcastRoster user gInfo v (RDRemoved currentMems) `catchAllErrors` eToView (errs2, deleted2, acis2, signed2) <- deleteMemsSend user gInfo Nothing rosterVer recipients currentMems (errs3, deleted3, acis3, signed3) <- foldM (\acc m -> deletePendingMember acc user gInfo [m] m) ([], [], [], False) pendingApprvMems let moderators = filter (\GroupMember {memberRole} -> memberRole >= GRModerator) members (errs4, deleted4, acis4, signed4) <- foldM (\acc m -> deletePendingMember acc user gInfo (m : moderators) m) ([], [], [], False) pendingRvwMems - forM_ rosterVer $ \v -> broadcastRoster user gInfo v `catchAllErrors` eToView let acis = acis2 <> acis3 <> acis4 errs = errs1 <> errs2 <> errs3 <> errs4 deleted = deleted1 <> deleted2 <> deleted3 <> deleted4 diff --git a/src/Simplex/Chat/Library/Internal.hs b/src/Simplex/Chat/Library/Internal.hs index c7f44d125d..f84359ee6a 100644 --- a/src/Simplex/Chat/Library/Internal.hs +++ b/src/Simplex/Chat/Library/Internal.hs @@ -2271,20 +2271,34 @@ sendGroupMessage' user gInfo members chatMsgEvent = -- TODO after restoring from a stale backup (relays accept only strictly-greater versions) -- Persist the next roster version before sending the events that carry it (so a recipient never advances -- past a version the owner hasn't recorded). The matching blob is broadcast separately, by broadcastRoster, --- after the change is applied to the owner's members - so the served roster excludes demoted/removed members. +-- with the change projected onto the served roster - so it excludes demoted/removed members. reserveRosterVersion :: GroupInfo -> CM VersionRoster reserveRosterVersion gInfo = do let rosterVer = maybe (VersionRoster 0) (\(VersionRoster n) -> VersionRoster (n + 1)) (rosterVersion gInfo) withStore' $ \db -> setGroupRosterVersion db gInfo rosterVer pure rosterVer -broadcastRoster :: User -> GroupInfo -> VersionRoster -> CM () -broadcastRoster user gInfo rosterVer = do +-- The roster change being broadcast, projected onto the current roster members in broadcastRoster. This lets the +-- roster blob be built (and sent) before the change is applied to the owner's own member records, so the owner +-- never demotes/removes a member locally before the change has been propagated to relays. +data RosterDelta + = RDRoleChanged GroupMemberRole [GroupMember] -- these members now hold this role + | RDRemoved [GroupMember] -- these members are removed from the group + +applyRosterDelta :: RosterDelta -> [GroupMember] -> [GroupMember] +applyRosterDelta delta current = case delta of + RDRoleChanged role changed -> map (\m -> (m :: GroupMember) {memberRole = role}) changed <> without changed + RDRemoved removed -> without removed + where + without ms = let ids = S.fromList (map groupMemberId' ms) in filter ((`S.notMember` ids) . groupMemberId') current + +broadcastRoster :: User -> GroupInfo -> VersionRoster -> RosterDelta -> CM () +broadcastRoster user gInfo rosterVer delta = do cxt <- chatStoreCxt (relays, rosterMems) <- withStore' $ \db -> (,) <$> getGroupRelayMembers db cxt user gInfo <*> getGroupRosterMembers db cxt user gInfo forM_ (L.nonEmpty relays) $ \relays' -> - sendRoster user gInfo (L.toList relays') rosterVer (buildGroupRoster rosterMems) + sendRoster user gInfo (L.toList relays') rosterVer (buildGroupRoster $ applyRosterDelta delta rosterMems) -- Send the current roster (no version bump) to a newly added relay so it can serve joiners. sendGroupRosterToRelay :: User -> GroupInfo -> GroupMember -> CM ()