mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-05-10 21:37:09 +00:00
core, ui: relay management - add, remove relays, synchronization to relay list (#6917)
This commit is contained in:
@@ -117,6 +117,9 @@ defaultChatConfig =
|
||||
deliveryWorkerDelay = 0,
|
||||
deliveryBucketSize = 10000,
|
||||
channelSubscriberRole = GRObserver,
|
||||
relayChecksInitialDelay = 30 * 1000000, -- 30 seconds
|
||||
relayChecksInterval = 30 * 60, -- 30 minutes
|
||||
relayInactiveTTL = nominalDay,
|
||||
relayRequestRetryInterval = RetryInterval {initialInterval = 5_000000, increaseAfter = 0, maxInterval = 600_000000},
|
||||
relayRequestExpiry = (10, nominalDay),
|
||||
deviceNameForRemote = "",
|
||||
|
||||
@@ -159,6 +159,9 @@ data ChatConfig = ChatConfig
|
||||
deliveryWorkerDelay :: Int64, -- microseconds
|
||||
deliveryBucketSize :: Int,
|
||||
channelSubscriberRole :: GroupMemberRole, -- TODO [relays] starting role should be communicated in protocol from owner to relays
|
||||
relayChecksInitialDelay :: Int64,
|
||||
relayChecksInterval :: NominalDiffTime,
|
||||
relayInactiveTTL :: NominalDiffTime,
|
||||
relayRequestRetryInterval :: RetryInterval,
|
||||
relayRequestExpiry :: (Int, NominalDiffTime),
|
||||
highlyAvailable :: Bool,
|
||||
@@ -521,6 +524,7 @@ data ChatCommand
|
||||
-- TODO [relays] starting role should be communicated in protocol from owner to relays (see channelSubscriberRole config)
|
||||
| APINewPublicGroup {userId :: UserId, incognito :: IncognitoEnabled, relayIds :: NonEmpty Int64, groupProfile :: GroupProfile}
|
||||
| APIGetGroupRelays {groupId :: GroupId}
|
||||
| APIAddGroupRelays {groupId :: GroupId, relayIds :: NonEmpty Int64}
|
||||
| NewPublicGroup IncognitoEnabled (NonEmpty Int64) GroupProfile
|
||||
| AddMember GroupName ContactName GroupMemberRole
|
||||
| JoinGroup {groupName :: GroupName, enableNtfs :: MsgFilter}
|
||||
@@ -732,6 +736,8 @@ data ChatResponse
|
||||
| CRPublicGroupCreated {user :: User, groupInfo :: GroupInfo, groupLink :: GroupLink, groupRelays :: [GroupRelay]}
|
||||
| CRPublicGroupCreationFailed {user :: User, addRelayResults :: [AddRelayResult]}
|
||||
| CRGroupRelays {user :: User, groupInfo :: GroupInfo, groupRelays :: [GroupRelay]}
|
||||
| CRGroupRelaysAdded {user :: User, groupInfo :: GroupInfo, groupLink :: GroupLink, groupRelays :: [GroupRelay]}
|
||||
| CRGroupRelaysAddFailed {user :: User, addRelayResults :: [AddRelayResult]}
|
||||
| CRGroupMembers {user :: User, group :: Group}
|
||||
| CRMemberSupportChats {user :: User, groupInfo :: GroupInfo, members :: [GroupMember]}
|
||||
-- | CRGroupConversationsArchived {user :: User, groupInfo :: GroupInfo, archivedGroupConversations :: [GroupConversation]}
|
||||
|
||||
@@ -1778,11 +1778,14 @@ processChatCommand vr nm = \case
|
||||
gInfo@GroupInfo {groupProfile = p, groupSummary = GroupSummary {publicMemberCount = localCount}} <- withFastStore $ \db -> getGroupInfo db vr user groupId
|
||||
case p of
|
||||
GroupProfile {publicGroup = Just PublicGroupProfile {groupLink = sLnk}} | useRelays' gInfo -> do
|
||||
(_, cData) <- getShortLinkConnReq nm user sLnk
|
||||
(_, cData@(ContactLinkData _ UserContactData {relays = currentRelayLinks})) <- getShortLinkConnReq' nm user sLnk
|
||||
groupSLinkData_ <- liftIO $ decodeLinkUserData cData
|
||||
gInfo' <- case groupSLinkData_ of
|
||||
Just sLinkData -> fst <$> updateGroupFromLinkData user gInfo sLinkData
|
||||
_ -> pure gInfo
|
||||
when (memberRole' (membership gInfo) /= GROwner && memberCurrent (membership gInfo)) $
|
||||
withGroupLock "syncSubscriberRelays" groupId $
|
||||
syncSubscriberRelays user gInfo' currentRelayLinks
|
||||
pure $ CRGroupInfo user gInfo'
|
||||
_ -> throwCmdError "group link data not available"
|
||||
APIGroupMemberInfo gId gMemberId -> withUser $ \user -> do
|
||||
@@ -2135,7 +2138,7 @@ processChatCommand vr nm = \case
|
||||
_ -> Nothing
|
||||
void $ createLinkOwnerMember db vr user gInfo' ctId_ (MemberId ownerId) ownerKey
|
||||
pure gInfo'
|
||||
rs <- mapConcurrently (connectToRelay gInfo') relays
|
||||
rs <- mapConcurrently (connectToRelay user gInfo') relays
|
||||
let relayFailed = \case (_, _, Left _) -> True; _ -> False
|
||||
(failed, succeeded) = partition relayFailed rs
|
||||
if null succeeded
|
||||
@@ -2162,23 +2165,6 @@ processChatCommand vr nm = \case
|
||||
isTempErr = \case
|
||||
(_, _, Left ChatErrorAgent {agentError = e}) -> temporaryOrHostError e
|
||||
_ -> False
|
||||
connectToRelay gInfo' relayLink = do
|
||||
gVar <- asks random
|
||||
-- Save relayLink to re-use relay member record on retry (check by relayLink)
|
||||
relayMember <- withFastStore $ \db -> getCreateRelayForMember db vr gVar user gInfo' relayLink
|
||||
r <- tryAllErrors $ do
|
||||
(fd@FixedLinkData {rootKey = relayKey, linkEntityId}, cData) <- getShortLinkConnReq nm user relayLink
|
||||
relayLinkData_ <- liftIO $ decodeLinkUserData cData
|
||||
case (relayLinkData_, linkEntityId) of
|
||||
(Just RelayShortLinkData {relayProfile = p}, Just entityId) ->
|
||||
withFastStore $ \db -> updateRelayMemberData db user relayMember (MemberId entityId) (MemberKey relayKey) p
|
||||
_ -> throwChatError $ CEException "relay link: no relay link data or entity id"
|
||||
let cReq = linkConnReq fd
|
||||
relayLinkToConnect = CCLink cReq (Just relayLink)
|
||||
void $ connectViaContact user (Just $ PCEGroup gInfo' relayMember) incognito relayLinkToConnect Nothing Nothing
|
||||
-- Re-read member to get updated activeConn and updated data (from updateRelayMemberData)
|
||||
relayMember' <- withFastStore $ \db -> getGroupMember db vr user groupId (groupMemberId' relayMember)
|
||||
pure (relayLink, relayMember', r)
|
||||
retryRelayConnectionAsync gInfo' relayLink relayMember@GroupMember {activeConn} = do
|
||||
forM_ activeConn $ \conn -> do
|
||||
deleteAgentConnectionAsync $ aConnId conn
|
||||
@@ -2547,6 +2533,37 @@ processChatCommand vr nm = \case
|
||||
relays <- liftIO $ getGroupRelays db gInfo
|
||||
pure (gInfo, relays)
|
||||
pure $ CRGroupRelays user gInfo relays
|
||||
APIAddGroupRelays groupId relayIds -> withUser $ \user -> withGroupLock "addGroupRelays" groupId $ do
|
||||
(gInfo, existingRelays) <- withFastStore $ \db -> do
|
||||
gi <- getGroupInfo db vr user groupId
|
||||
rs <- liftIO $ getGroupRelays db gi
|
||||
pure (gi, rs)
|
||||
assertUserGroupRole gInfo GROwner
|
||||
unless (useRelays' gInfo) $ throwCmdError "group does not use relays"
|
||||
let existingRelayIds = map (\GroupRelay {userChatRelay = UserChatRelay {chatRelayId = DBEntityId rId}} -> rId) existingRelays
|
||||
when (any (`elem` existingRelayIds) relayIds) $ throwCmdError "some relays are already in the group"
|
||||
gLink@GroupLink {connLinkContact = ccLink} <- withFastStore $ \db -> getGroupLink db user gInfo
|
||||
sLnk <- case connShortLink' ccLink of
|
||||
Just sl -> pure sl
|
||||
Nothing -> throwChatError $ CEException "group link has no short link"
|
||||
relays <- withFastStore $ \db -> mapM (getChatRelayById db user) (L.toList relayIds)
|
||||
results <- addRelays user gInfo sLnk relays
|
||||
case partitionEithers (map snd results) of
|
||||
([], _) -> do
|
||||
relays' <- withFastStore $ \db -> liftIO $ getGroupRelays db gInfo
|
||||
pure $ CRGroupRelaysAdded user gInfo gLink relays'
|
||||
(errors@(e : _), _) -> do
|
||||
if all isTempErr errors
|
||||
then throwError e
|
||||
else do
|
||||
let toRelayResult (r, Left e') = AddRelayResult r (Just e')
|
||||
toRelayResult (r, Right _) = AddRelayResult r Nothing
|
||||
pure $ CRGroupRelaysAddFailed user (map toRelayResult results)
|
||||
where
|
||||
isTempErr :: ChatError -> Bool
|
||||
isTempErr = \case
|
||||
ChatErrorAgent {agentError = e} -> temporaryOrHostError e
|
||||
_ -> False
|
||||
APIAddMember groupId contactId memRole -> withUser $ \user -> withGroupLock "addMember" groupId $ do
|
||||
-- TODO for large groups: no need to load all members to determine if contact is a member
|
||||
(group, contact) <- withFastStore $ \db -> (,) <$> getGroup db vr user groupId <*> getContact db vr user contactId
|
||||
@@ -3577,6 +3594,44 @@ processChatCommand vr nm = \case
|
||||
ct' <- withStore $ \db -> getContact db vr user contactId
|
||||
pure $ CRSentInvitationToContact user ct' incognitoProfile
|
||||
_ -> throwCmdError "contact already has connection"
|
||||
connectToRelay :: User -> GroupInfo -> ShortLinkContact -> CM (ShortLinkContact, GroupMember, Either ChatError ())
|
||||
connectToRelay user gInfo relayLink = do
|
||||
gVar <- asks random
|
||||
-- Save relayLink to re-use relay member record on retry (check by relayLink)
|
||||
relayMember <- withFastStore $ \db -> getCreateRelayForMember db vr gVar user gInfo relayLink
|
||||
r <- tryAllErrors $ do
|
||||
(fd@FixedLinkData {rootKey = relayKey, linkEntityId}, cData) <- getShortLinkConnReq nm user relayLink
|
||||
relayLinkData_ <- liftIO $ decodeLinkUserData cData
|
||||
case (relayLinkData_, linkEntityId) of
|
||||
(Just RelayShortLinkData {relayProfile = p}, Just entityId) ->
|
||||
withFastStore $ \db -> updateRelayMemberData db user relayMember (MemberId entityId) (MemberKey relayKey) p
|
||||
_ -> throwChatError $ CEException "relay link: no relay link data or entity id"
|
||||
let cReq = linkConnReq fd
|
||||
relayLinkToConnect = CCLink cReq (Just relayLink)
|
||||
void $ connectViaContact user (Just $ PCEGroup gInfo relayMember) (incognitoMembership gInfo) relayLinkToConnect Nothing Nothing
|
||||
relayMember' <- withFastStore $ \db -> getGroupMember db vr user (groupId' gInfo) (groupMemberId' relayMember)
|
||||
pure (relayLink, relayMember', r)
|
||||
syncSubscriberRelays :: User -> GroupInfo -> [ShortLinkContact] -> CM ()
|
||||
syncSubscriberRelays user gInfo currentRelayLinks = void . tryAllErrors $ do
|
||||
localRelayMembers <- withFastStore' $ \db -> getGroupRelayMembers db vr user gInfo
|
||||
let activeRelayMembers = filter memberCurrent localRelayMembers
|
||||
memberRelayLink GroupMember {relayLink = rl} = rl
|
||||
localRelayLinks = mapMaybe memberRelayLink activeRelayMembers
|
||||
newRelayLinks = filter (`notElem` localRelayLinks) currentRelayLinks
|
||||
forM_ newRelayLinks $ \rlnk -> void . tryAllErrors $
|
||||
connectToRelay user gInfo rlnk
|
||||
forM_ localRelayMembers $ \m ->
|
||||
case memberRelayLink m of
|
||||
-- Remove relay if its link is no longer in the current link data.
|
||||
-- Inactive relays (e.g. left) are only cleaned up when no active relays remain,
|
||||
-- as that is the only case where the owner's relay removal can't be forwarded.
|
||||
Just rlnk | rlnk `notElem` currentRelayLinks,
|
||||
memberCurrent m || null activeRelayMembers ->
|
||||
void . tryAllErrors $ do
|
||||
deleteMemberConnection m
|
||||
deleteOrUpdateMemberRecord user gInfo m
|
||||
_ -> pure ()
|
||||
|
||||
prepareContact :: User -> ConnReqContact -> PQSupport -> CM (ConnId, VersionChat)
|
||||
prepareContact user cReq pqSup = do
|
||||
-- 0) toggle disabled - PQSupportOff
|
||||
@@ -4727,12 +4782,42 @@ deleteInProgressGroup user gInfo = do
|
||||
withFastStore' $ \db -> deleteGroup db user gInfo
|
||||
|
||||
runRelayGroupLinkChecks :: User -> CM ()
|
||||
runRelayGroupLinkChecks _user = do
|
||||
-- TODO [relays] relay: periodically check presence of relay link in group links of served groups
|
||||
-- TODO - retrieve group link data
|
||||
-- TODO - if relay link is present, update relay status to RSActive
|
||||
-- TODO - if relay link is absent and status was RSActive -> update to new "Removed" status?
|
||||
pure ()
|
||||
runRelayGroupLinkChecks user = do
|
||||
initialDelay <- asks (relayChecksInitialDelay . config)
|
||||
liftIO $ threadDelay' initialDelay
|
||||
interval <- asks (relayChecksInterval . config)
|
||||
forever $ do
|
||||
flip catchAllErrors eToView $ do
|
||||
lift waitChatStartedAndActivated
|
||||
checkRelayServedGroups
|
||||
checkRelayInactiveGroups
|
||||
liftIO $ threadDelay' $ diffToMicroseconds interval
|
||||
where
|
||||
checkRelayServedGroups = do
|
||||
vr <- chatVersionRange
|
||||
relayGroups <- withStore' $ \db -> getRelayServedGroups db vr user
|
||||
forM_ relayGroups $ \gInfo@GroupInfo {groupProfile = gp} -> flip catchAllErrors eToView $ do
|
||||
case publicGroup gp of
|
||||
Just PublicGroupProfile {groupLink = sLnk} -> do
|
||||
(_, ContactLinkData _ UserContactData {relays = relayLinks}) <-
|
||||
getShortLinkConnReq' NRMBackground user sLnk
|
||||
gLink_ <- withStore' $ \db -> runExceptT $ getGroupLink db user gInfo
|
||||
case gLink_ of
|
||||
Right GroupLink {connLinkContact = CCLink _ (Just ourLink)} ->
|
||||
if ourLink `elem` relayLinks
|
||||
then do
|
||||
-- TODO [relays] emit event to UI when relay own status promoted to RSActive
|
||||
-- CEvtGroupRelayUpdated requires GroupRelay (owner-side), not available on relay side
|
||||
void $ withStore' $ \db -> updateRelayOwnStatusFromTo db gInfo RSAccepted RSActive
|
||||
else void $ withStore' $ \db -> updateRelayOwnStatusFromTo db gInfo RSActive RSInactive
|
||||
_ -> pure ()
|
||||
_ -> pure ()
|
||||
checkRelayInactiveGroups = do
|
||||
vr <- chatVersionRange
|
||||
ttl <- asks (relayInactiveTTL . config)
|
||||
inactiveGroups <- withStore' $ \db -> getRelayInactiveGroups db vr user ttl
|
||||
forM_ inactiveGroups $ \gInfo -> flip catchAllErrors eToView $
|
||||
deleteGroupConnections user gInfo False
|
||||
|
||||
expireChatItems :: User -> Int64 -> Bool -> CM ()
|
||||
expireChatItems user@User {userId} globalTTL sync = do
|
||||
@@ -5026,6 +5111,7 @@ chatCommandP =
|
||||
("/public group" <|> "/pg") *> (NewPublicGroup <$> incognitoP <* " relays=" <*> strP <* A.space <* char_ '#' <*> channelProfile),
|
||||
"/_public group " *> (APINewPublicGroup <$> A.decimal <*> incognitoOnOffP <*> _strP <* A.space <*> jsonP),
|
||||
"/_get relays #" *> (APIGetGroupRelays <$> A.decimal),
|
||||
"/_add relays #" *> (APIAddGroupRelays <$> A.decimal <*> _strP),
|
||||
("/add " <|> "/a ") *> char_ '#' *> (AddMember <$> displayNameP <* A.space <* char_ '@' <*> displayNameP <*> (memberRole <|> pure GRMember)),
|
||||
("/join " <|> "/j ") *> char_ '#' *> (JoinGroup <$> displayNameP <*> (" mute" $> MFNone <|> pure MFAll)),
|
||||
"/accept member " *> char_ '#' *> (AcceptMember <$> displayNameP <* A.space <* char_ '@' <*> displayNameP <*> (memberRole <|> pure GRMember)),
|
||||
|
||||
@@ -1808,9 +1808,12 @@ deleteOrUpdateMemberRecord user gInfo m =
|
||||
deleteOrUpdateMemberRecordIO :: DB.Connection -> User -> GroupInfo -> GroupMember -> IO GroupInfo
|
||||
deleteOrUpdateMemberRecordIO db user@User {userId} gInfo m = do
|
||||
(gInfo', m') <- deleteSupportChatIfExists db user gInfo m
|
||||
checkGroupMemberHasItems db user m' >>= \case
|
||||
Just _ -> updateGroupMemberStatus db userId m' GSMemRemoved
|
||||
Nothing -> deleteGroupMember db user m'
|
||||
if isRelay m'
|
||||
then deleteGroupMember db user m'
|
||||
else
|
||||
checkGroupMemberHasItems db user m' >>= \case
|
||||
Just _ -> updateGroupMemberStatus db userId m' GSMemRemoved
|
||||
Nothing -> deleteGroupMember db user m'
|
||||
pure gInfo'
|
||||
|
||||
updateMemberRecordDeleted :: User -> GroupInfo -> GroupMember -> GroupMemberStatus -> CM GroupInfo
|
||||
@@ -1818,8 +1821,15 @@ updateMemberRecordDeleted user@User {userId} gInfo m newStatus =
|
||||
withStore' $ \db -> do
|
||||
(gInfo', m') <- deleteSupportChatIfExists db user gInfo m
|
||||
updateGroupMemberStatus db userId m' newStatus
|
||||
deactivateRelay_ db m
|
||||
pure gInfo'
|
||||
|
||||
deactivateRelay_ :: DB.Connection -> GroupMember -> IO ()
|
||||
deactivateRelay_ db m =
|
||||
when (isRelay m) $ do
|
||||
relay_ <- runExceptT $ getGroupRelayByGMId db (groupMemberId' m)
|
||||
forM_ relay_ $ \relay -> void $ updateRelayStatus db relay RSInactive
|
||||
|
||||
deleteSupportChatIfExists :: DB.Connection -> User -> GroupInfo -> GroupMember -> IO (GroupInfo, GroupMember)
|
||||
deleteSupportChatIfExists db user gInfo m = do
|
||||
gInfo' <-
|
||||
|
||||
@@ -931,7 +931,11 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
newDeliveryTasks <- reverse <$> foldM (processAChatMsg gInfo' scopeInfo m' tags eInfo) [] aChatMsgs
|
||||
shouldDelConns <-
|
||||
if isUserGrpFwdRelay gInfo' && not (blockedByAdmin m)
|
||||
then createDeliveryTasks gInfo' m' newDeliveryTasks
|
||||
then
|
||||
let tasks
|
||||
| relayOwnStatus gInfo' == Just RSInactive = filter relayRemovedNewTask newDeliveryTasks
|
||||
| otherwise = newDeliveryTasks
|
||||
in createDeliveryTasks gInfo' m' tasks
|
||||
else pure False
|
||||
withRcpt <- checkSendRcpt $ rights aChatMsgs
|
||||
pure (withRcpt, shouldDelConns)
|
||||
@@ -1039,6 +1043,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
where
|
||||
aChatMsgHasReceipt (APMsg _ (ParsedMsg _ _ ChatMessage {chatMsgEvent})) =
|
||||
hasDeliveryReceipt (toCMEventTag chatMsgEvent)
|
||||
relayRemovedNewTask :: NewMessageDeliveryTask -> Bool
|
||||
relayRemovedNewTask NewMessageDeliveryTask {taskContext = DeliveryTaskContext {jobScope}} = isRelayRemoved jobScope
|
||||
createDeliveryTasks :: GroupInfo -> GroupMember -> [NewMessageDeliveryTask] -> CM ShouldDeleteGroupConns
|
||||
createDeliveryTasks gInfo'@GroupInfo {groupId = gId} m' newDeliveryTasks = do
|
||||
let relayRemovedTask_ = find (\NewMessageDeliveryTask {taskContext = DeliveryTaskContext {jobScope}} -> isRelayRemoved jobScope) newDeliveryTasks
|
||||
@@ -1306,8 +1312,6 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
pure (gInfo, gLink, relays', changed)
|
||||
toView $ CEvtGroupLinkDataUpdated user gInfo gLink relays relaysChanged
|
||||
where
|
||||
-- TODO [relays] owner: on relay deletion (link absent from relayLinks)
|
||||
-- TODO move status RSActive to new "Removed" status / remove relay record
|
||||
updateRelay :: DB.Connection -> GroupRelay -> ([GroupRelay], Bool) -> IO ([GroupRelay], Bool)
|
||||
updateRelay db relay@GroupRelay {relayLink, relayStatus} (acc, changed) =
|
||||
case relayLink of
|
||||
@@ -1315,6 +1319,16 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
| rLink `elem` relayLinks && relayStatus == RSAccepted -> do
|
||||
relay' <- updateRelayStatus db relay RSActive
|
||||
pure (relay' : acc, True)
|
||||
| rLink `elem` relayLinks -> pure (relay : acc, changed)
|
||||
| relayStatus == RSActive -> do
|
||||
-- Relay link absent from link data — deactivate.
|
||||
-- RSAccepted relays are not deactivated: their own link data update
|
||||
-- may not have been processed yet (race with concurrent relay connections).
|
||||
-- TODO [relays] multi-owner: Another owner removing a relay updates link data on
|
||||
-- TODO the SMP server, but this owner won't receive a LINK callback for it
|
||||
-- TODO (LINK only fires in response to own setConnShortLink calls).
|
||||
relay' <- updateRelayStatus db relay RSInactive
|
||||
pure (relay' : acc, True)
|
||||
_ -> pure (relay : acc, changed)
|
||||
_ -> throwChatError $ CECommandError "LINK event expected for a group link only"
|
||||
_ -> throwChatError $ CECommandError "unexpected cmdFunction"
|
||||
@@ -3096,10 +3110,12 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
deleteGroupLinkIfExists user gInfo
|
||||
-- TODO [relays] possible improvement is to immediately delete rcv queues if isUserGrpFwdRelay
|
||||
unless (isUserGrpFwdRelay gInfo) $ deleteGroupConnections user gInfo False
|
||||
withStore' $ \db -> updateGroupMemberStatus db userId membership GSMemRemoved
|
||||
withStore' $ \db -> do
|
||||
updateGroupMemberStatus db userId membership GSMemRemoved
|
||||
when (isJust $ relayOwnStatus gInfo) $ updateRelayOwnStatus_ db gInfo RSInactive
|
||||
let membership' = membership {memberStatus = GSMemRemoved}
|
||||
when withMessages $ deleteMessages gInfo membership' SMDSnd
|
||||
deleteMemberItem gInfo RGEUserDeleted
|
||||
deleteMemberItem msg gInfo RGEUserDeleted
|
||||
toView $ CEvtDeletedMemberUser user gInfo {membership = membership'} m withMessages msgSigned
|
||||
pure $ Just DJSGroup {jobSpec = DJRelayRemoved}
|
||||
else
|
||||
@@ -3127,7 +3143,11 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
let wasDeleted = memberStatus == GSMemRemoved || memberStatus == GSMemLeft
|
||||
deletedMember' = deletedMember {memberStatus = GSMemRemoved}
|
||||
when withMessages $ deleteMessages gInfo'' deletedMember' SMDRcv
|
||||
unless wasDeleted $ deleteMemberItem gInfo'' $ RGEMemberDeleted groupMemberId (fromLocalProfile memberProfile)
|
||||
-- Clear forwardedByMember if it references the deleted member,
|
||||
-- as the member record was already deleted above.
|
||||
let RcvMessage {forwardedByMember = fwdBy} = msg
|
||||
msg' = if fwdBy == Just groupMemberId then (msg :: RcvMessage) {forwardedByMember = Nothing} else msg
|
||||
unless wasDeleted $ deleteMemberItem msg' gInfo'' $ RGEMemberDeleted groupMemberId (fromLocalProfile memberProfile)
|
||||
toView $ CEvtDeletedMember user gInfo'' m deletedMember' withMessages msgSigned
|
||||
pure deliveryScope
|
||||
where
|
||||
@@ -3135,9 +3155,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
| senderRole < GRAdmin || senderRole < memberRole =
|
||||
messageError "x.grp.mem.del with insufficient member permissions" $> Nothing
|
||||
| otherwise = a
|
||||
deleteMemberItem gi gEvent = do
|
||||
deleteMemberItem msg' gi gEvent = do
|
||||
(gi', m', scopeInfo) <- mkGroupChatScope gi m
|
||||
(ci, cInfo) <- saveRcvChatItemNoParse user (CDGroupRcv gi' scopeInfo m') msg brokerTs (CIRcvGroupEvent gEvent)
|
||||
(ci, cInfo) <- saveRcvChatItemNoParse user (CDGroupRcv gi' scopeInfo m') msg' brokerTs (CIRcvGroupEvent gEvent)
|
||||
groupMsgToView cInfo ci
|
||||
deleteMessages :: MsgDirectionI d => GroupInfo -> GroupMember -> SMsgDirection d -> CM ()
|
||||
deleteMessages gInfo' delMem msgDir
|
||||
@@ -3168,10 +3188,6 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
deleteMemberConnection m
|
||||
-- member record is not deleted to allow creation of "member left" chat item
|
||||
gInfo' <- updateMemberRecordDeleted user gInfo m GSMemLeft
|
||||
when (isRelay m) $
|
||||
withStore' $ \db -> do
|
||||
relay_ <- runExceptT $ getGroupRelayByGMId db (groupMemberId' m)
|
||||
forM_ relay_ $ \relay -> void $ updateRelayStatus db relay RSInactive
|
||||
gInfo'' <- updatePublicGroupData user gInfo'
|
||||
unless (muteEventInChannel gInfo'' m) $ do
|
||||
(gInfo''', m', scopeInfo) <- mkGroupChatScope gInfo'' m
|
||||
@@ -3526,19 +3542,24 @@ runDeliveryTaskWorker a deliveryKey Worker {doWork} = do
|
||||
processDeliveryTask :: MessageDeliveryTask -> CM ()
|
||||
processDeliveryTask task@MessageDeliveryTask {jobScope} =
|
||||
case jobScopeImpliedSpec jobScope of
|
||||
DJDeliveryJob _includePending ->
|
||||
withWorkItems a doWork (withStore' $ \db -> getNextDeliveryTasks db gInfo task) $ \nextTasks -> do
|
||||
let (body, taskIds, largeTaskIds) = batchDeliveryTasks1 vr maxEncodedMsgLength nextTasks
|
||||
withStore' $ \db -> do
|
||||
createMsgDeliveryJob db gInfo jobScope (singleSenderGMId_ nextTasks) body
|
||||
forM_ taskIds $ \taskId -> updateDeliveryTaskStatus db taskId DTSProcessed
|
||||
forM_ largeTaskIds $ \taskId -> setDeliveryTaskErrStatus db taskId "large"
|
||||
lift . void $ getDeliveryJobWorker True deliveryKey
|
||||
DJDeliveryJob _includePending
|
||||
| relayOwnStatus gInfo == Just RSInactive -> do
|
||||
logWarn "delivery task worker: relay inactive"
|
||||
withStore' $ \db -> setDeliveryTaskErrStatus db (deliveryTaskId task) "relay inactive"
|
||||
| otherwise ->
|
||||
withWorkItems a doWork (withStore' $ \db -> getNextDeliveryTasks db gInfo task) $ \nextTasks -> do
|
||||
let (body, taskIds, largeTaskIds) = batchDeliveryTasks1 vr maxEncodedMsgLength nextTasks
|
||||
withStore' $ \db -> do
|
||||
createMsgDeliveryJob db gInfo jobScope (singleSenderGMId_ nextTasks) body
|
||||
forM_ taskIds $ \taskId -> updateDeliveryTaskStatus db taskId DTSProcessed
|
||||
forM_ largeTaskIds $ \taskId -> setDeliveryTaskErrStatus db taskId "large"
|
||||
lift . void $ getDeliveryJobWorker True deliveryKey
|
||||
where
|
||||
singleSenderGMId_ :: NonEmpty MessageDeliveryTask -> Maybe GroupMemberId
|
||||
singleSenderGMId_ (MessageDeliveryTask {senderGMId = senderGMId'} :| ts)
|
||||
| all (\MessageDeliveryTask {senderGMId} -> senderGMId == senderGMId') ts = Just senderGMId'
|
||||
| otherwise = Nothing
|
||||
-- DJRelayRemoved is allowed when RSInactive - it forwards XGrpMemDel about relay's own deletion
|
||||
DJRelayRemoved
|
||||
| workerScope /= DWSGroup ->
|
||||
throwChatError $ CEInternalError "delivery task worker: relay removed task in wrong worker scope"
|
||||
@@ -3591,9 +3612,14 @@ runDeliveryJobWorker a deliveryKey Worker {doWork} = do
|
||||
processDeliveryJob :: MessageDeliveryJob -> CM ()
|
||||
processDeliveryJob job =
|
||||
case jobScopeImpliedSpec jobScope of
|
||||
DJDeliveryJob _includePending -> do
|
||||
sendBodyToMembers
|
||||
withStore' $ \db -> updateDeliveryJobStatus db jobId DJSComplete
|
||||
DJDeliveryJob _includePending
|
||||
| relayOwnStatus gInfo == Just RSInactive -> do
|
||||
logWarn "delivery job worker: relay inactive"
|
||||
withStore' $ \db -> setDeliveryJobErrStatus db (deliveryJobId job) "relay inactive"
|
||||
| otherwise -> do
|
||||
sendBodyToMembers
|
||||
withStore' $ \db -> updateDeliveryJobStatus db jobId DJSComplete
|
||||
-- DJRelayRemoved is allowed when RSInactive - it forwards XGrpMemDel about relay's own deletion
|
||||
DJRelayRemoved
|
||||
| workerScope /= DWSGroup ->
|
||||
throwChatError $ CEInternalError "delivery job worker: relay removed job in wrong worker scope"
|
||||
|
||||
@@ -94,6 +94,9 @@ module Simplex.Chat.Store.Groups
|
||||
setGroupInProgressDone,
|
||||
createRelayRequestGroup,
|
||||
updateRelayOwnStatusFromTo,
|
||||
updateRelayOwnStatus_,
|
||||
getRelayServedGroups,
|
||||
getRelayInactiveGroups,
|
||||
createNewContactMemberAsync,
|
||||
createJoiningMember,
|
||||
getMemberJoinRequest,
|
||||
@@ -188,7 +191,7 @@ import Data.Maybe (catMaybes, fromMaybe, isJust, isNothing)
|
||||
import Data.Ord (Down (..))
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
import Data.Time.Clock (UTCTime (..), getCurrentTime)
|
||||
import Data.Time.Clock (NominalDiffTime, UTCTime (..), addUTCTime, getCurrentTime)
|
||||
import Data.Text.Encoding (encodeUtf8)
|
||||
import Simplex.Chat.Messages
|
||||
import Simplex.Chat.Operators
|
||||
@@ -1585,7 +1588,29 @@ updateRelayOwnStatusFromTo db gInfo@GroupInfo {groupId} fromStatus toStatus = do
|
||||
updateRelayOwnStatus_ :: DB.Connection -> GroupInfo -> RelayStatus -> IO ()
|
||||
updateRelayOwnStatus_ db GroupInfo {groupId} relayStatus = do
|
||||
currentTs <- getCurrentTime
|
||||
DB.execute db "UPDATE groups SET relay_own_status = ?, updated_at = ? WHERE group_id = ?" (relayStatus, currentTs, groupId)
|
||||
let inactiveAt_ = if relayStatus == RSInactive then Just currentTs else Nothing
|
||||
DB.execute db "UPDATE groups SET relay_own_status = ?, relay_inactive_at = ?, updated_at = ? WHERE group_id = ?" (relayStatus, inactiveAt_, currentTs, groupId)
|
||||
|
||||
getRelayServedGroups :: DB.Connection -> VersionRangeChat -> User -> IO [GroupInfo]
|
||||
getRelayServedGroups db vr User {userId, userContactId} = do
|
||||
map (toGroupInfo vr userContactId [])
|
||||
<$> DB.query
|
||||
db
|
||||
( groupInfoQuery
|
||||
<> " WHERE g.user_id = ? AND mu.contact_id = ? AND g.relay_own_status IN (?, ?)"
|
||||
)
|
||||
(userId, userContactId, RSAccepted, RSActive)
|
||||
|
||||
getRelayInactiveGroups :: DB.Connection -> VersionRangeChat -> User -> NominalDiffTime -> IO [GroupInfo]
|
||||
getRelayInactiveGroups db vr User {userId, userContactId} ttl = do
|
||||
cutoffTs <- addUTCTime (- ttl) <$> getCurrentTime
|
||||
map (toGroupInfo vr userContactId [])
|
||||
<$> DB.query
|
||||
db
|
||||
( groupInfoQuery
|
||||
<> " WHERE g.user_id = ? AND mu.contact_id = ? AND g.relay_own_status = ? AND g.relay_inactive_at IS NOT NULL AND g.relay_inactive_at <= ?"
|
||||
)
|
||||
(userId, userContactId, RSInactive, cutoffTs)
|
||||
|
||||
createNewContactMemberAsync :: DB.Connection -> TVar ChaChaDRG -> User -> GroupInfo -> Contact -> GroupMemberRole -> (CommandId, ConnId) -> VersionChat -> VersionRangeChat -> SubscriptionMode -> ExceptT StoreError IO ()
|
||||
createNewContactMemberAsync db gVar user@User {userId, userContactId} GroupInfo {groupId, membership} Contact {contactId, localDisplayName, profile} memberRole (cmdId, agentConnId) chatV peerChatVRange subMode =
|
||||
|
||||
@@ -29,6 +29,7 @@ import Simplex.Chat.Store.Postgres.Migrations.M20260122_has_link
|
||||
import Simplex.Chat.Store.Postgres.Migrations.M20260222_chat_relays
|
||||
import Simplex.Chat.Store.Postgres.Migrations.M20260403_item_viewed
|
||||
import Simplex.Chat.Store.Postgres.Migrations.M20260429_relay_request_retries
|
||||
import Simplex.Chat.Store.Postgres.Migrations.M20260507_relay_inactive_at
|
||||
import Simplex.Messaging.Agent.Store.Shared (Migration (..))
|
||||
|
||||
schemaMigrations :: [(String, Text, Maybe Text)]
|
||||
@@ -57,7 +58,8 @@ schemaMigrations =
|
||||
("20260122_has_link", m20260122_has_link, Just down_m20260122_has_link),
|
||||
("20260222_chat_relays", m20260222_chat_relays, Just down_m20260222_chat_relays),
|
||||
("20260403_item_viewed", m20260403_item_viewed, Just down_m20260403_item_viewed),
|
||||
("20260429_relay_request_retries", m20260429_relay_request_retries, Just down_m20260429_relay_request_retries)
|
||||
("20260429_relay_request_retries", m20260429_relay_request_retries, Just down_m20260429_relay_request_retries),
|
||||
("20260507_relay_inactive_at", m20260507_relay_inactive_at, Just down_m20260507_relay_inactive_at)
|
||||
]
|
||||
|
||||
-- | The list of migrations in ascending order by date
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
|
||||
module Simplex.Chat.Store.Postgres.Migrations.M20260507_relay_inactive_at where
|
||||
|
||||
import Data.Text (Text)
|
||||
import Text.RawString.QQ (r)
|
||||
|
||||
m20260507_relay_inactive_at :: Text
|
||||
m20260507_relay_inactive_at =
|
||||
[r|
|
||||
ALTER TABLE groups ADD COLUMN relay_inactive_at TIMESTAMPTZ;
|
||||
|]
|
||||
|
||||
down_m20260507_relay_inactive_at :: Text
|
||||
down_m20260507_relay_inactive_at =
|
||||
[r|
|
||||
ALTER TABLE groups DROP COLUMN relay_inactive_at;
|
||||
|]
|
||||
@@ -962,7 +962,8 @@ CREATE TABLE test_chat_schema.groups (
|
||||
public_member_count bigint,
|
||||
relay_request_retries bigint DEFAULT 0 NOT NULL,
|
||||
relay_request_delay bigint DEFAULT 0 NOT NULL,
|
||||
relay_request_execute_at timestamp with time zone DEFAULT '1970-01-01 01:00:00+01'::timestamp with time zone NOT NULL
|
||||
relay_request_execute_at timestamp with time zone DEFAULT '1970-01-01 04:00:00+04'::timestamp with time zone NOT NULL,
|
||||
relay_inactive_at timestamp with time zone
|
||||
);
|
||||
|
||||
|
||||
|
||||
@@ -152,6 +152,7 @@ import Simplex.Chat.Store.SQLite.Migrations.M20260122_has_link
|
||||
import Simplex.Chat.Store.SQLite.Migrations.M20260222_chat_relays
|
||||
import Simplex.Chat.Store.SQLite.Migrations.M20260403_item_viewed
|
||||
import Simplex.Chat.Store.SQLite.Migrations.M20260429_relay_request_retries
|
||||
import Simplex.Chat.Store.SQLite.Migrations.M20260507_relay_inactive_at
|
||||
import Simplex.Messaging.Agent.Store.Shared (Migration (..))
|
||||
|
||||
schemaMigrations :: [(String, Query, Maybe Query)]
|
||||
@@ -303,7 +304,8 @@ schemaMigrations =
|
||||
("20260122_has_link", m20260122_has_link, Just down_m20260122_has_link),
|
||||
("20260222_chat_relays", m20260222_chat_relays, Just down_m20260222_chat_relays),
|
||||
("20260403_item_viewed", m20260403_item_viewed, Just down_m20260403_item_viewed),
|
||||
("20260429_relay_request_retries", m20260429_relay_request_retries, Just down_m20260429_relay_request_retries)
|
||||
("20260429_relay_request_retries", m20260429_relay_request_retries, Just down_m20260429_relay_request_retries),
|
||||
("20260507_relay_inactive_at", m20260507_relay_inactive_at, Just down_m20260507_relay_inactive_at)
|
||||
]
|
||||
|
||||
-- | The list of migrations in ascending order by date
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
|
||||
module Simplex.Chat.Store.SQLite.Migrations.M20260507_relay_inactive_at where
|
||||
|
||||
import Database.SQLite.Simple (Query)
|
||||
import Database.SQLite.Simple.QQ (sql)
|
||||
|
||||
m20260507_relay_inactive_at :: Query
|
||||
m20260507_relay_inactive_at =
|
||||
[sql|
|
||||
ALTER TABLE groups ADD COLUMN relay_inactive_at TEXT;
|
||||
|]
|
||||
|
||||
down_m20260507_relay_inactive_at :: Query
|
||||
down_m20260507_relay_inactive_at =
|
||||
[sql|
|
||||
ALTER TABLE groups DROP COLUMN relay_inactive_at;
|
||||
|]
|
||||
@@ -273,6 +273,16 @@ Query:
|
||||
Plan:
|
||||
SEARCH connections USING PRIMARY KEY (conn_id=?)
|
||||
|
||||
Query:
|
||||
SELECT user_id FROM users u
|
||||
WHERE u.deleted = ?
|
||||
AND NOT EXISTS (SELECT c.conn_id FROM connections c WHERE c.user_id = u.user_id)
|
||||
|
||||
Plan:
|
||||
SCAN u
|
||||
CORRELATED SCALAR SUBQUERY 1
|
||||
SEARCH c USING COVERING INDEX idx_connections_user (user_id=?)
|
||||
|
||||
Query:
|
||||
SELECT user_id FROM users u
|
||||
WHERE u.user_id = ?
|
||||
@@ -525,6 +535,21 @@ Query:
|
||||
Plan:
|
||||
SEARCH conn_confirmations USING COVERING INDEX idx_conn_confirmations_conn_id (conn_id=?)
|
||||
|
||||
Query:
|
||||
DELETE FROM encrypted_rcv_message_hashes
|
||||
WHERE encrypted_rcv_message_hash_id IN (
|
||||
SELECT encrypted_rcv_message_hash_id
|
||||
FROM encrypted_rcv_message_hashes
|
||||
WHERE created_at < ?
|
||||
ORDER BY created_at ASC
|
||||
LIMIT ?
|
||||
)
|
||||
|
||||
Plan:
|
||||
SEARCH encrypted_rcv_message_hashes USING INTEGER PRIMARY KEY (rowid=?)
|
||||
LIST SUBQUERY 1
|
||||
SEARCH encrypted_rcv_message_hashes USING COVERING INDEX idx_encrypted_rcv_message_hashes_created_at (created_at<?)
|
||||
|
||||
Query:
|
||||
INSERT INTO connections
|
||||
(user_id, conn_id, conn_mode, smp_agent_version, enable_ntfs, pq_support, duplex_handshake) VALUES (?,?,?,?,?,?,?)
|
||||
@@ -1085,6 +1110,14 @@ Query: SELECT conn_id FROM connections WHERE deleted = 0
|
||||
Plan:
|
||||
SCAN connections
|
||||
|
||||
Query: SELECT conn_id FROM connections WHERE deleted = ?
|
||||
Plan:
|
||||
SCAN connections
|
||||
|
||||
Query: SELECT conn_id FROM connections WHERE deleted_at_wait_delivery IS NOT NULL
|
||||
Plan:
|
||||
SCAN connections
|
||||
|
||||
Query: SELECT conn_id FROM connections WHERE user_id = ?
|
||||
Plan:
|
||||
SEARCH connections USING COVERING INDEX idx_connections_user (user_id=?)
|
||||
|
||||
@@ -6808,6 +6808,10 @@ Query: SELECT last_insert_rowid()
|
||||
Plan:
|
||||
SCAN CONSTANT ROW
|
||||
|
||||
Query: SELECT local_display_name FROM group_members
|
||||
Plan:
|
||||
SCAN group_members USING COVERING INDEX idx_group_members_user_id_local_display_name
|
||||
|
||||
Query: SELECT max(active_order) FROM users
|
||||
Plan:
|
||||
SEARCH users
|
||||
|
||||
@@ -176,7 +176,8 @@ CREATE TABLE groups(
|
||||
public_member_count INTEGER,
|
||||
relay_request_retries INTEGER NOT NULL DEFAULT 0,
|
||||
relay_request_delay INTEGER NOT NULL DEFAULT 0,
|
||||
relay_request_execute_at TEXT NOT NULL DEFAULT '1970-01-01 00:00:00', -- received
|
||||
relay_request_execute_at TEXT NOT NULL DEFAULT '1970-01-01 00:00:00',
|
||||
relay_inactive_at TEXT, -- received
|
||||
FOREIGN KEY(user_id, local_display_name)
|
||||
REFERENCES display_names(user_id, local_display_name)
|
||||
ON DELETE CASCADE
|
||||
|
||||
@@ -182,6 +182,8 @@ chatResponseToView hu cfg@ChatConfig {logLevel, showReactions, testView} liveIte
|
||||
CRPublicGroupCreated u g _groupLink _relays -> ttyUser u $ viewGroupCreated g testView
|
||||
CRPublicGroupCreationFailed u results -> ttyUser u $ viewPublicGroupCreationFailed results
|
||||
CRGroupRelays u g relays -> ttyUser u $ viewGroupRelays g relays
|
||||
CRGroupRelaysAdded u g _groupLink relays -> ttyUser u $ viewGroupRelays g relays
|
||||
CRGroupRelaysAddFailed u results -> ttyUser u $ viewGroupRelaysAddFailed results
|
||||
CRGroupMembers u g -> ttyUser u $ viewGroupMembers g
|
||||
CRMemberSupportChats u g ms -> ttyUser u $ viewMemberSupportChats g ms
|
||||
-- CRGroupConversationsArchived u _g _conversations -> ttyUser u []
|
||||
@@ -1239,14 +1241,18 @@ viewGroupCreated g testView =
|
||||
where
|
||||
relaysInstruction = "wait for selected relay(s) to join, then you can invite members via group link"
|
||||
|
||||
viewPublicGroupCreationFailed :: [AddRelayResult] -> [StyledString]
|
||||
viewPublicGroupCreationFailed results =
|
||||
["channel not created, results:"]
|
||||
<> map showRelayResult results
|
||||
viewRelayResults :: StyledString -> [AddRelayResult] -> [StyledString]
|
||||
viewRelayResults header results = [header] <> map showRelayResult results
|
||||
where
|
||||
showRelayResult (AddRelayResult UserChatRelay {chatRelayId = DBEntityId i} err_) =
|
||||
" relay " <> sShow i <> ": " <> maybe "ok" (plain . tshow) err_
|
||||
|
||||
viewPublicGroupCreationFailed :: [AddRelayResult] -> [StyledString]
|
||||
viewPublicGroupCreationFailed = viewRelayResults "channel not created, results:"
|
||||
|
||||
viewGroupRelaysAddFailed :: [AddRelayResult] -> [StyledString]
|
||||
viewGroupRelaysAddFailed = viewRelayResults "relays not added, results:"
|
||||
|
||||
viewCannotResendInvitation :: GroupInfo -> ContactName -> [StyledString]
|
||||
viewCannotResendInvitation g c =
|
||||
[ ttyContact c <> " is already invited to group " <> ttyGroup' g,
|
||||
|
||||
Reference in New Issue
Block a user