From 252b1e90dcc4dc06fbedd72ee1823bae57e1395c Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Mon, 29 Jun 2026 15:20:52 +0400 Subject: [PATCH] wip --- src/Simplex/Chat/Library/Subscriber.hs | 56 ++++++++++++++++++-------- src/Simplex/Chat/Protocol.hs | 7 ++++ 2 files changed, 46 insertions(+), 17 deletions(-) diff --git a/src/Simplex/Chat/Library/Subscriber.hs b/src/Simplex/Chat/Library/Subscriber.hs index 883f6c985b..0dea4b58f3 100644 --- a/src/Simplex/Chat/Library/Subscriber.hs +++ b/src/Simplex/Chat/Library/Subscriber.hs @@ -1089,11 +1089,11 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = XGrpMemIntro memInfo memRestrictions_ -> Nothing <$ xGrpMemIntro gInfo' m'' memInfo memRestrictions_ XGrpMemInv memId introInv -> Nothing <$ xGrpMemInv gInfo' m'' memId introInv XGrpMemFwd memInfo introInv -> Nothing <$ xGrpMemFwd gInfo' m'' memInfo introInv - XGrpMemRole memId memRole memberKey rosterVer -> fmap ctx <$> xGrpMemRole gInfo' m'' memId memRole memberKey rosterVer msg brokerTs + XGrpMemRole memId memRole memberKey rosterVer -> fmap ctx <$> xGrpMemRole gInfo' Nothing m'' memId memRole memberKey rosterVer msg brokerTs XGrpMemRestrict memId memRestrictions -> fmap ctx <$> xGrpMemRestrict gInfo' m'' memId memRestrictions msg brokerTs XGrpMemCon memId -> Nothing <$ xGrpMemCon gInfo' m'' memId XGrpMemDel memId withMessages rosterVer -> case encoding @e of - SJson -> fmap ctx <$> xGrpMemDel gInfo' m'' memId withMessages rosterVer verifiedMsg msg brokerTs False + SJson -> fmap ctx <$> xGrpMemDel gInfo' Nothing m'' memId withMessages rosterVer verifiedMsg msg brokerTs False SBinary -> pure Nothing XGrpLeave -> fmap ctx <$> xGrpLeave gInfo' m'' msg brokerTs XGrpDel -> Just (DeliveryTaskContext (DJSGroup {jobSpec = DJRelayRemoved}) False) <$ xGrpDel gInfo' m'' msg brokerTs @@ -1101,6 +1101,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = XGrpPrefs ps' -> fmap ctx <$> xGrpPrefs gInfo' m'' ps' msg XGrpRoster gr -> fmap ctx <$> xGrpRoster gInfo' m'' m'' gr verifiedMsg sharedMsgId_ brokerTs XGrpRosterAck ackVer ackErr -> Nothing <$ xGrpRosterAck gInfo' m'' ackVer ackErr + XGrpRosterRequest reqVer -> Nothing <$ xGrpRosterRequest gInfo' m'' reqVer -- TODO [knocking] why don't we forward these messages? XGrpDirectInv connReq mContent_ msgScope -> memberCanSend (Just m'') msgScope $ Nothing <$ xGrpDirectInv gInfo' m'' conn' connReq mContent_ msg brokerTs XGrpMsgForward fwd msg' -> Nothing <$ xGrpMsgForward gInfo' Nothing m'' fwd (ParsedMsg Nothing Nothing msg') brokerTs @@ -3244,29 +3245,41 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = -- batch), then advance it in the same transaction; a strictly lower version is a replay and is ignored. -- Only an owner sender may advance it: a non-owner signed event is rejected by the action that follows, -- but must not bump roster_version first, or every later owner roster at a lower version is dropped. - applyAtRosterVersion :: GroupInfo -> GroupMember -> Maybe VersionRoster -> CM (Maybe DeliveryJobScope) -> CM (Maybe DeliveryJobScope) - applyAtRosterVersion gInfo sender rosterVer_ action + applyAtRosterVersion :: GroupInfo -> Maybe GroupMember -> GroupMember -> Maybe VersionRoster -> CM (Maybe DeliveryJobScope) -> CM (Maybe DeliveryJobScope) + applyAtRosterVersion gInfo fwdRelay_ sender rosterVer_ action | not (useRelays' gInfo) = action | otherwise = case rosterVer_ of Nothing -> action Just _ | memberRole' sender /= GROwner -> action Just v -> do - accept <- withStore' $ \db -> do + (accept, cur) <- withStore' $ \db -> do cur <- getGroupRosterVersion db gInfo let fresh = maybe True (v >=) cur when fresh $ setGroupRosterVersion db gInfo v - pure fresh + pure (fresh, cur) if accept - then action + then (requestRosterOnGap gInfo fwdRelay_ cur v `catchAllErrors` eToView) >> action else messageWarning "x.grp.mem: roster version not newer than current, ignoring" $> Nothing - xGrpMemRole :: GroupInfo -> GroupMember -> MemberId -> GroupMemberRole -> Maybe MemberKey -> Maybe VersionRoster -> RcvMessage -> UTCTime -> CM (Maybe DeliveryJobScope) - xGrpMemRole gInfo@GroupInfo {membership} m@GroupMember {memberRole = senderRole} memId memRole memberKey_ rosterVer_ msg@RcvMessage {msgSigned} brokerTs + -- A subscriber that skipped versions (cur known and v > cur+1) asks the relay that forwarded this delta + -- to re-serve the full roster, recovering the privileged set and keys carried by the missed versions. + -- The request carries the pre-gap cur (the relay serves only what it holds newer). Best-effort: a failed + -- request must not block applying the delta. Relays and the direct path don't request (fwdRelay_ is Nothing). + requestRosterOnGap :: GroupInfo -> Maybe GroupMember -> Maybe VersionRoster -> VersionRoster -> CM () + requestRosterOnGap gInfo fwdRelay_ cur_ v + | isUserGrpFwdRelay gInfo = pure () + | otherwise = case (fwdRelay_, cur_) of + (Just relay, Just cur@(VersionRoster c)) + | v > VersionRoster (c + 1) -> void $ sendGroupMessage' user gInfo [relay] (XGrpRosterRequest cur) + _ -> pure () + + xGrpMemRole :: GroupInfo -> Maybe GroupMember -> GroupMember -> MemberId -> GroupMemberRole -> Maybe MemberKey -> Maybe VersionRoster -> RcvMessage -> UTCTime -> CM (Maybe DeliveryJobScope) + xGrpMemRole gInfo@GroupInfo {membership} fwdRelay_ m@GroupMember {memberRole = senderRole} memId memRole memberKey_ rosterVer_ msg@RcvMessage {msgSigned} brokerTs | membershipMemId == memId = - applyAtRosterVersion gInfo m rosterVer_ $ + applyAtRosterVersion gInfo fwdRelay_ m rosterVer_ $ let gInfo' = gInfo {membership = membership {memberRole = memRole}} in changeMemberRole gInfo' membership False (\db -> updateGroupMemberRole db user membership memRole) (RGEUserRole memRole) True - | otherwise = applyAtRosterVersion gInfo m rosterVer_ $ do + | otherwise = applyAtRosterVersion gInfo fwdRelay_ m rosterVer_ $ do defaultRole <- unknownMemberRole gInfo -- an owner-signed event with a key TOFU-creates an unknown member only for a roster role; else a plain lookup let allowCreate = useRelays' gInfo && senderRole == GROwner && isRosterRole memRole && isJust memberKey_ @@ -3483,6 +3496,15 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = messageError $ "x.grp.roster.ack: relay could not save roster, marked rejected: " <> e _ -> pure () + -- A relay re-serves the full roster to a subscriber that detected a version gap, but only when it holds + -- something newer than the requester's pre-gap version (rate-limiting same-version requests). serveRoster + -- is the join/resume path: signed header + inline blob chunks to the one requester; a no-op without a roster. + xGrpRosterRequest :: GroupInfo -> GroupMember -> VersionRoster -> CM () + xGrpRosterRequest gInfo m reqVer = + when (isUserGrpFwdRelay gInfo) $ do + cur <- withStore' $ \db -> getGroupRosterVersion db gInfo + when (maybe True (> reqVer) cur) $ serveRoster user gInfo m + checkHostRole :: GroupMember -> GroupMemberRole -> CM () checkHostRole GroupMember {memberRole, localDisplayName} memRole = when (memberRole < GRAdmin || memberRole < memRole) $ throwChatError (CEGroupContactRole localDisplayName) @@ -3527,11 +3549,11 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = withStore $ \db -> setMemberVectorRelationConnected db sendingMem refMem MRSubjectConnected withStore $ \db -> setMemberVectorRelationConnected db refMem sendingMem MRReferencedConnected - xGrpMemDel :: GroupInfo -> GroupMember -> MemberId -> Bool -> Maybe VersionRoster -> VerifiedMsg 'Json -> RcvMessage -> UTCTime -> Bool -> CM (Maybe DeliveryJobScope) - xGrpMemDel gInfo@GroupInfo {membership} m@GroupMember {memberRole = senderRole} memId withMessages rosterVer_ verifiedMsg msg@RcvMessage {msgSigned} brokerTs forwarded = do + xGrpMemDel :: GroupInfo -> Maybe GroupMember -> GroupMember -> MemberId -> Bool -> Maybe VersionRoster -> VerifiedMsg 'Json -> RcvMessage -> UTCTime -> Bool -> CM (Maybe DeliveryJobScope) + xGrpMemDel gInfo@GroupInfo {membership} fwdRelay_ m@GroupMember {memberRole = senderRole} memId withMessages rosterVer_ verifiedMsg msg@RcvMessage {msgSigned} brokerTs forwarded = do let GroupMember {memberId = membershipMemId} = membership if membershipMemId == memId - then applyAtRosterVersion gInfo m rosterVer_ $ checkRole membership $ do + then applyAtRosterVersion gInfo fwdRelay_ m rosterVer_ $ checkRole membership $ do deleteGroupLinkIfExists user gInfo -- TODO [relays] possible improvement is to immediately delete rcv queues if isUserGrpFwdRelay unless (isUserGrpFwdRelay gInfo) $ deleteGroupConnections user gInfo False @@ -3543,7 +3565,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = deleteMemberItem msg gInfo RGEUserDeleted toView $ CEvtDeletedMemberUser user gInfo {membership = membership'} m withMessages msgSigned pure $ Just DJSGroup {jobSpec = DJRelayRemoved} - else applyAtRosterVersion gInfo m rosterVer_ $ + else applyAtRosterVersion gInfo fwdRelay_ m rosterVer_ $ withStore' (\db -> runExceptT $ getGroupMemberByMemberId db cxt user gInfo memId) >>= \case Left _ -> do messageError "x.grp.mem.del with unknown member ID" @@ -3809,9 +3831,9 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = XInfo p -> withAuthor XInfo_ $ \author -> void $ xInfoMember gInfo author p rcvMsg msgTs XGrpRelayNew rl -> withAuthor XGrpRelayNew_ $ \author -> void $ xGrpRelayNew gInfo author rl XGrpMemNew memInfo msgScope -> withAuthor XGrpMemNew_ $ \author -> void $ xGrpMemNew gInfo author memInfo msgScope rcvMsg msgTs - XGrpMemRole memId memRole memberKey rosterVer -> withAuthor XGrpMemRole_ $ \author -> void $ xGrpMemRole gInfo author memId memRole memberKey rosterVer rcvMsg msgTs + XGrpMemRole memId memRole memberKey rosterVer -> withAuthor XGrpMemRole_ $ \author -> void $ xGrpMemRole gInfo (Just m) author memId memRole memberKey rosterVer rcvMsg msgTs XGrpMemRestrict memId memRestrictions -> withAuthor XGrpMemRestrict_ $ \author -> void $ xGrpMemRestrict gInfo author memId memRestrictions rcvMsg msgTs - XGrpMemDel memId withMessages rosterVer -> withAuthor XGrpMemDel_ $ \author -> void $ xGrpMemDel gInfo author memId withMessages rosterVer verifiedMsg rcvMsg msgTs True + XGrpMemDel memId withMessages rosterVer -> withAuthor XGrpMemDel_ $ \author -> void $ xGrpMemDel gInfo (Just m) author memId withMessages rosterVer verifiedMsg rcvMsg msgTs True XGrpLeave -> withAuthor XGrpLeave_ $ \author -> void $ xGrpLeave gInfo author rcvMsg msgTs XGrpDel -> withAuthor XGrpDel_ $ \author -> void $ xGrpDel gInfo author rcvMsg msgTs XGrpInfo p' -> withAuthor XGrpInfo_ $ \author -> void $ xGrpInfo gInfo author p' rcvMsg msgTs diff --git a/src/Simplex/Chat/Protocol.hs b/src/Simplex/Chat/Protocol.hs index 223fe492a9..6b2b67c4e3 100644 --- a/src/Simplex/Chat/Protocol.hs +++ b/src/Simplex/Chat/Protocol.hs @@ -525,6 +525,7 @@ data ChatMsgEvent (e :: MsgEncoding) where XGrpDirectInv :: ConnReqInvitation -> Maybe MsgContent -> Maybe MsgScope -> ChatMsgEvent 'Json XGrpRoster :: GroupRoster -> ChatMsgEvent 'Json XGrpRosterAck :: VersionRoster -> Maybe Text -> ChatMsgEvent 'Json + XGrpRosterRequest :: VersionRoster -> ChatMsgEvent 'Json XGrpMsgForward :: GrpMsgForward -> ChatMessage 'Json -> ChatMsgEvent 'Json XInfoProbe :: Probe -> ChatMsgEvent 'Json XInfoProbeCheck :: ProbeHash -> ChatMsgEvent 'Json @@ -1099,6 +1100,7 @@ data CMEventTag (e :: MsgEncoding) where XGrpDirectInv_ :: CMEventTag 'Json XGrpRoster_ :: CMEventTag 'Json XGrpRosterAck_ :: CMEventTag 'Json + XGrpRosterRequest_ :: CMEventTag 'Json XGrpMsgForward_ :: CMEventTag 'Json XInfoProbe_ :: CMEventTag 'Json XInfoProbeCheck_ :: CMEventTag 'Json @@ -1161,6 +1163,7 @@ instance MsgEncodingI e => StrEncoding (CMEventTag e) where XGrpDirectInv_ -> "x.grp.direct.inv" XGrpRoster_ -> "x.grp.roster" XGrpRosterAck_ -> "x.grp.roster.ack" + XGrpRosterRequest_ -> "x.grp.roster.request" XGrpMsgForward_ -> "x.grp.msg.forward" XInfoProbe_ -> "x.info.probe" XInfoProbeCheck_ -> "x.info.probe.check" @@ -1224,6 +1227,7 @@ instance StrEncoding ACMEventTag where "x.grp.direct.inv" -> XGrpDirectInv_ "x.grp.roster" -> XGrpRoster_ "x.grp.roster.ack" -> XGrpRosterAck_ + "x.grp.roster.request" -> XGrpRosterRequest_ "x.grp.msg.forward" -> XGrpMsgForward_ "x.info.probe" -> XInfoProbe_ "x.info.probe.check" -> XInfoProbeCheck_ @@ -1283,6 +1287,7 @@ toCMEventTag msg = case msg of XGrpDirectInv {} -> XGrpDirectInv_ XGrpRoster _ -> XGrpRoster_ XGrpRosterAck {} -> XGrpRosterAck_ + XGrpRosterRequest _ -> XGrpRosterRequest_ XGrpMsgForward {} -> XGrpMsgForward_ XInfoProbe _ -> XInfoProbe_ XInfoProbeCheck _ -> XInfoProbeCheck_ @@ -1444,6 +1449,7 @@ appJsonToCM AppMessageJson {v, msgId, event, params} = do XGrpDirectInv_ -> XGrpDirectInv <$> p "connReq" <*> opt "content" <*> opt "scope" XGrpRoster_ -> XGrpRoster <$> (GroupRoster <$> p "version" <*> p "fileInv") XGrpRosterAck_ -> XGrpRosterAck <$> p "version" <*> opt "error" + XGrpRosterRequest_ -> XGrpRosterRequest <$> p "version" XGrpMsgForward_ -> do fwdSender <- opt "memberId" >>= \case Just memberId -> FwdMember memberId . fromMaybe "" <$> opt "memberName" @@ -1518,6 +1524,7 @@ chatToAppMessage chatMsg@ChatMessage {chatVRange, msgId, chatMsgEvent} = case en XGrpDirectInv connReq content scope -> o $ ("content" .=? content) $ ("scope" .=? scope) ["connReq" .= connReq] XGrpRoster GroupRoster {version, fileInv} -> o ["version" .= version, "fileInv" .= fileInv] XGrpRosterAck version err -> o $ ("error" .=? err) ["version" .= version] + XGrpRosterRequest version -> o ["version" .= version] XGrpMsgForward GrpMsgForward {fwdSender, fwdBrokerTs} msg -> o $ encodeFwdSender fwdSender ["msg" .= msg, "msgTs" .= fwdBrokerTs] where encodeFwdSender = \case