Compare commits

...

35 Commits

Author SHA1 Message Date
Evgeny Poberezkin f856bb5faf Merge branch 'master' into ab/async-subs 2024-06-24 16:52:36 +01:00
Evgeny Poberezkin 41e6e6b54e revert some changes 2024-06-24 15:35:57 +01:00
Alexander Bondarenko 137656f13f Merge remote-tracking branch 'origin/master' into ab/async-subs 2024-06-12 22:56:01 +03:00
Alexander Bondarenko 5c44f0e17e Merge remote-tracking branch 'origin/master' into ab/async-subs 2024-06-03 15:46:11 +03:00
Alexander Bondarenko baced8a6da use connections from getConnectionEntity for the subscribing user 2024-06-03 15:21:16 +03:00
IC Rainbow 6548245883 request empty network statuses explicitly 2024-05-31 13:07:09 +03:00
Alexander Bondarenko 63026acf46 remove reference queries 2024-05-30 21:36:10 +03:00
Alexander Bondarenko 900aad60e9 update nix 2024-05-30 21:21:03 +03:00
IC Rainbow e5e231fe9e add user index on user contact links 2024-05-30 21:16:24 +03:00
IC Rainbow eb11e6c409 request names only when enabled 2024-05-30 20:56:03 +03:00
Alexander Bondarenko e045aa214d request names only for errored contacts 2024-05-30 20:45:43 +03:00
Alexander Bondarenko 6311a581cf clenup 2024-05-30 20:27:07 +03:00
Alexander Bondarenko 370936aaa1 hide up/down details in CLI unless requested with -c 2024-05-30 20:26:15 +03:00
Alexander Bondarenko 7b108536fd send only errors contact results 2024-05-30 20:25:41 +03:00
Alexander Bondarenko 13d779f41d stripped contactLinkSubs 2024-05-30 20:22:44 +03:00
Alexander Bondarenko ed12f34330 Merge remote-tracking branch 'origin/master' into ab/async-subs 2024-05-30 19:08:51 +03:00
Alexander Bondarenko ddd9c6f16f more races 2024-05-29 23:28:42 +03:00
Alexander Bondarenko 561c923cdb fix network status 2024-05-29 22:37:45 +03:00
Alexander Bondarenko a126215c8c fix bg conns 2024-05-29 22:25:55 +03:00
Alexander Bondarenko f21b5af568 more races 2024-05-29 22:00:47 +03:00
Alexander Bondarenko 73bd003984 fix race in test 2024-05-29 21:41:21 +03:00
Alexander Bondarenko 4d2452b03f test that light query results match the original 2024-05-29 20:29:14 +03:00
Alexander Bondarenko 4ceb0dc564 remove pendingContact details 2024-05-29 16:45:43 +03:00
Alexander Bondarenko 551a34dd1a log forked subscriber errors 2024-05-29 16:35:35 +03:00
Alexander Bondarenko 30f00c2f2e lightweight queries 2024-05-29 16:18:33 +03:00
Alexander Bondarenko b94ced6b39 restore rare entities prefetch 2024-05-29 15:12:52 +03:00
Alexander Bondarenko a1216d86fd process user links 2024-05-29 12:38:46 +03:00
Alexander Bondarenko b6cf81e680 fix ok sizes in summary 2024-05-28 22:11:27 +03:00
Alexander Bondarenko 998907f107 process group results 2024-05-28 21:54:13 +03:00
Alexander Bondarenko a975ffe82a add rfc 2024-05-28 16:42:51 +03:00
Alexander Bondarenko a1d2f4cda9 give active user a minor priority 2024-05-27 22:22:37 +03:00
Alexander Bondarenko cd3992fd0f WIP 2024-05-27 22:12:39 +03:00
Alexander Bondarenko 1bc47c6910 inline calls 2024-05-25 11:04:01 +03:00
Alexander Bondarenko ab07096235 remove unused code 2024-05-25 11:03:28 +03:00
Alexander Bondarenko 1295e538ed WIP: remove sync connection replies 2024-05-25 11:03:28 +03:00
15 changed files with 272 additions and 207 deletions
@@ -0,0 +1,43 @@
# Optimized subscription
## Problem
The `subscribeUserConnections` function has a few problems that affect UX on app start:
1. It loads entity data that isn't used until result processing. This produces a memory spike and takes CPU time to parse all the data.
2. Subscription results are processed synchronously after the agent finishes all the batches for user. The app wouldn't see connections as active until the slowest server responds or timeouts.
3. User subscriptions are processed sequentially. A currently active user is given a first round of subs, but the remaining are blocked. If a user profile is switched right away, the new user may start receiving updates with even more lag.
## Solution
Functions that fetch connections and entities are reduced to return only connection IDs. The filters should be moved from Haskell into specialized SQL queries that only return `[ConnId]`.
With the connection list on hands the agent subscriber thread forks off to do its thing and process batch results. This allows outer loop to start collecting connections for the remaining users.
Successful results are communicated with a new `UP srv conns` message emitted from agent when a batch finishes its processing. The `conns` payload would be a list of connections that actually have just switched subs from "pending" to "active". The UP handling machinery processes status updates just as it would in a server reconnect event. This would update connection state in chat apps as soon as the server responds, keeping app and core in tighter sync.
`reconnectSMPClient` should stop sending UPs to prevent double processing of the same result. The `okConns` membership test it currently uses is the same "did not belong to an active connection" that the batch result would use.
Sending results with UP allows to reduce summary responses to a bunch of counters so no entity data would be needed for CLI here:
```haskell
| CRContactSubSummary {user :: User, okSubs :: Int, errSubs :: Int}
| CRUserGroupLinksSubSummary {user :: User, okSubs :: Int, errSubs :: Int}
| CRMemberSubSummary {user :: User, okSubs :: Int, errSubs :: Int}
| CRPendingSubSummary {user :: User, okSubs :: Int, errSubs :: Int}
```
Subscription errors are reported to API as `CRNetworkStatuses` as ususal, but the active subs are removed from the list as they are already handled by `UP`.
Subscription errors for CLI (when connection error reporting is enabled) are reported with the types reduced to a textual name:
```haskell
| CRContactSubError {user :: User, contactName :: ContactName, chatError :: ChatError}
| CRMemberSubError {user :: User, groupName :: GroupName, contactName :: ContactName, chatError :: ChatError}
| CRSndFileSubError {user :: User, sndFileTransfer :: Text, chatError :: ChatError}
| CRRcvFileSubError {user :: User, rcvFileTransfer :: Text, chatError :: ChatError}
```
> A generic constructor could be used instead, but then it would contain extra fields to form a message in View if matching the current output is desired.
The textual names for connections can be requested for the subset of all connections that needs them with the same procedure that's used in `UP` handling.
+1
View File
@@ -146,6 +146,7 @@ library
Simplex.Chat.Migrations.M20240510_chat_items_via_proxy Simplex.Chat.Migrations.M20240510_chat_items_via_proxy
Simplex.Chat.Migrations.M20240515_rcv_files_user_approved_relays Simplex.Chat.Migrations.M20240515_rcv_files_user_approved_relays
Simplex.Chat.Migrations.M20240528_quota_err_counter Simplex.Chat.Migrations.M20240528_quota_err_counter
Simplex.Chat.Migrations.M20240530_user_contact_links_user_id
Simplex.Chat.Mobile Simplex.Chat.Mobile
Simplex.Chat.Mobile.File Simplex.Chat.Mobile.File
Simplex.Chat.Mobile.Shared Simplex.Chat.Mobile.Shared
+92 -119
View File
@@ -3400,71 +3400,55 @@ subscribeUserConnections :: VersionRangeChat -> Bool -> AgentBatchSubscribe -> U
subscribeUserConnections vr onlyNeeded agentBatchSubscribe user = do subscribeUserConnections vr onlyNeeded agentBatchSubscribe user = do
-- get user connections -- get user connections
ce <- asks $ subscriptionEvents . config ce <- asks $ subscriptionEvents . config
(conns, cts, ucs, gs, ms, sfts, rfts, pcs) <- (conns, ctConns, ucs, gs, mConns, sfts, rfts, pcConns) <-
if onlyNeeded if onlyNeeded
then do then do
(conns, entities) <- withStore' (`getConnectionsToSubscribe` vr) (conns, entities) <- withStore' $ \db -> getConnectionsToSubscribe db vr user
let (cts, ucs, ms, sfts, rfts, pcs) = foldl' addEntity (M.empty, M.empty, M.empty, M.empty, M.empty, M.empty) entities let (cts, ucs, ms, sfts, rfts, pcs) = foldl' addEntity ([], [], [], M.empty, M.empty, []) entities
pure (conns, cts, ucs, [], ms, sfts, rfts, pcs) pure (conns, cts, ucs, [], ms, sfts, rfts, pcs)
else do else do
withStore' unsetConnectionToSubscribe withStore' (`unsetConnectionToSubscribe` user)
(ctConns, cts) <- getContactConns ctConns <- getContactConns
(ucConns, ucs) <- getUserContactLinkConns (ucConns, ucs) <- getUserContactLinkConns
(gs, mConns, ms) <- getGroupMemberConns (gs, mConns) <- getGroupMemberConns
(sftConns, sfts) <- getSndFileTransferConns (sftConns, sfts) <- getSndFileTransferConns
(rftConns, rfts) <- getRcvFileTransferConns (rftConns, rfts) <- getRcvFileTransferConns
(pcConns, pcs) <- getPendingContactConns pcConns <- getPendingContactConns
let conns = concat [ctConns, ucConns, mConns, sftConns, rftConns, pcConns] let conns = concat [ctConns, ucConns, mConns, sftConns, rftConns, pcConns]
pure (conns, cts, ucs, gs, ms, sfts, rfts, pcs) pure (conns, ctConns, ucs, gs, mConns, sfts, rfts, pcConns)
-- subscribe using batched commands -- detach subscription and result processing
rs <- withAgent $ \a -> agentBatchSubscribe a conns void . lift . forkIO . runSubscriber $ do
-- send connection events to view -- subscribe using batched commands
contactSubsToView rs cts ce rs <- withAgent $ \a -> agentBatchSubscribe a conns
-- TODO possibly, we could either disable these events or replace with less noisy for API let (errs, _oks) = M.mapEither id rs
contactLinkSubsToView rs ucs refs <- if ce then withStore' $ \db -> getConnectionsContacts db (M.keys errs) else pure []
groupSubsToView rs gs ms ce let connRefs = M.fromList $ map (\ContactRef {agentConnId = AgentConnId acId, localDisplayName} -> (acId, localDisplayName)) refs
sndFileSubsToView rs sfts contactSubsToView errs ctConns connRefs ce
rcvFileSubsToView rs rfts contactLinkSubsToView errs ucs
pendingConnSubsToView rs pcs groupSubsToView errs gs mConns connRefs ce
sndFileSubsToView errs sfts
rcvFileSubsToView errs rfts
pendingConnSubsToView errs pcConns
where where
runSubscriber :: CM () -> CM' ()
runSubscriber action = tryAllErrors' mkChatError action >>= either (logError . tshow) pure
addEntity (cts, ucs, ms, sfts, rfts, pcs) = \case addEntity (cts, ucs, ms, sfts, rfts, pcs) = \case
RcvDirectMsgConnection c (Just ct) -> let cts' = addConn c ct cts in (cts', ucs, ms, sfts, rfts, pcs) RcvDirectMsgConnection c (Just _ct) -> let cts' = aConnId c : cts in (cts', ucs, ms, sfts, rfts, pcs)
RcvDirectMsgConnection c Nothing -> let pcs' = addConn c (toPCC c) pcs in (cts, ucs, ms, sfts, rfts, pcs') RcvDirectMsgConnection c Nothing -> let pcs' = aConnId c : pcs in (cts, ucs, ms, sfts, rfts, pcs')
RcvGroupMsgConnection c _g m -> let ms' = addConn c m ms in (cts, ucs, ms', sfts, rfts, pcs) RcvGroupMsgConnection c _g _m -> let ms' = aConnId c : ms in (cts, ucs, ms', sfts, rfts, pcs)
SndFileConnection c sft -> let sfts' = addConn c sft sfts in (cts, ucs, ms, sfts', rfts, pcs) SndFileConnection c sft -> let sfts' = M.insert (aConnId c) sft sfts in (cts, ucs, ms, sfts', rfts, pcs)
RcvFileConnection c rft -> let rfts' = addConn c rft rfts in (cts, ucs, ms, sfts, rfts', pcs) RcvFileConnection c rft -> let rfts' = M.insert (aConnId c) rft rfts in (cts, ucs, ms, sfts, rfts', pcs)
UserContactConnection c uc -> let ucs' = addConn c uc ucs in (cts, ucs', ms, sfts, rfts, pcs) UserContactConnection c uc -> let ucs' = (aConnId c, isNothing $ userContactGroupId uc) : ucs in (cts, ucs', ms, sfts, rfts, pcs)
addConn :: Connection -> a -> Map ConnId a -> Map ConnId a getContactConns :: CM [ConnId]
addConn = M.insert . aConnId getContactConns = withStore_ getUserContactConnIds
toPCC Connection {connId, agentConnId, connStatus, viaUserContactLink, groupLinkId, customUserProfileId, localAlias, createdAt} = getUserContactLinkConns :: CM ([ConnId], [(ConnId, Bool)])
PendingContactConnection
{ pccConnId = connId,
pccAgentConnId = agentConnId,
pccConnStatus = connStatus,
viaContactUri = False,
viaUserContactLink,
groupLinkId,
customUserProfileId,
connReqInv = Nothing,
localAlias,
createdAt,
updatedAt = createdAt
}
getContactConns :: CM ([ConnId], Map ConnId Contact)
getContactConns = do
cts <- withStore_ (`getUserContacts` vr)
let cts' = mapMaybe (\ct -> (,ct) <$> contactConnId ct) $ filter contactActive cts
pure (map fst cts', M.fromList cts')
getUserContactLinkConns :: CM ([ConnId], Map ConnId UserContact)
getUserContactLinkConns = do getUserContactLinkConns = do
(cs, ucs) <- unzip <$> withStore_ (`getUserContactLinks` vr) ucs <- withStore_ getUserContactLinks
let connIds = map aConnId cs pure (map fst ucs, ucs)
pure (connIds, M.fromList $ zip connIds ucs) getGroupMemberConns :: CM ([(GroupInfo, [ConnId])], [ConnId])
getGroupMemberConns :: CM ([Group], [ConnId], Map ConnId GroupMember)
getGroupMemberConns = do getGroupMemberConns = do
gs <- withStore_ (`getUserGroups` vr) gs <- withStore_ (`getUserGroupMemberConnIds` vr)
let mPairs = concatMap (\(Group _ ms) -> mapMaybe (\m -> (,m) <$> memberConnId m) (filter (not . memberRemoved) ms)) gs pure (gs, concatMap snd gs)
pure (gs, map fst mPairs, M.fromList mPairs)
getSndFileTransferConns :: CM ([ConnId], Map ConnId SndFileTransfer) getSndFileTransferConns :: CM ([ConnId], Map ConnId SndFileTransfer)
getSndFileTransferConns = do getSndFileTransferConns = do
sfts <- withStore_ getLiveSndFileTransfers sfts <- withStore_ getLiveSndFileTransfers
@@ -3475,92 +3459,81 @@ subscribeUserConnections vr onlyNeeded agentBatchSubscribe user = do
rfts <- withStore_ getLiveRcvFileTransfers rfts <- withStore_ getLiveRcvFileTransfers
let rftPairs = mapMaybe (\ft -> (,ft) <$> liveRcvFileTransferConnId ft) rfts let rftPairs = mapMaybe (\ft -> (,ft) <$> liveRcvFileTransferConnId ft) rfts
pure (map fst rftPairs, M.fromList rftPairs) pure (map fst rftPairs, M.fromList rftPairs)
getPendingContactConns :: CM ([ConnId], Map ConnId PendingContactConnection) getPendingContactConns :: CM [ConnId]
getPendingContactConns = do getPendingContactConns = withStore_ getPendingContactConnections
pcs <- withStore_ getPendingContactConnections contactSubsToView :: Map ConnId AgentErrorType -> [ConnId] -> Map ConnId ContactName -> Bool -> CM ()
let connIds = map aConnId' pcs contactSubsToView errs cts names ce = ifM (asks $ coreApi . config) notifyAPI notifyCLI
pure (connIds, M.fromList $ zip connIds pcs)
contactSubsToView :: Map ConnId (Either AgentErrorType ()) -> Map ConnId Contact -> Bool -> CM ()
contactSubsToView rs cts ce = do
chatModifyVar connNetworkStatuses $ M.union (M.fromList statuses)
ifM (asks $ coreApi . config) (notifyAPI statuses) notifyCLI
where where
conns = S.fromList cts
errConns = M.restrictKeys errs conns
notifyCLI = do notifyCLI = do
let cRs = resultsFor rs cts toView CRContactSubSummary {user, okSubs = S.size conns - M.size errConns, errSubs = M.size errConns}
cErrors = sortOn (\(Contact {localDisplayName = n}, _) -> n) $ filterErrors cRs when ce $ forM_ (M.assocs errConns) $ \(acId, err) ->
toView . CRContactSubSummary user $ map (uncurry ContactSubStatus) cRs forM_ (M.lookup acId names) $ \contactName ->
when ce $ mapM_ (toView . uncurry (CRContactSubError user)) cErrors toView CRContactSubError {user, contactName, chatError = ChatErrorAgent err Nothing}
notifyAPI = toView . CRNetworkStatuses (Just user) . map (uncurry ConnNetworkStatus) notifyAPI = unless (M.null errConns) $ toView $ CRNetworkStatuses (Just user) $ map status (M.assocs errConns)
statuses = M.foldrWithKey' addStatus [] cts
where where
addStatus :: ConnId -> Contact -> [(AgentConnId, NetworkStatus)] -> [(AgentConnId, NetworkStatus)] status (connId, err) = ConnNetworkStatus (AgentConnId connId) $ NSError (errorNetworkStatus err)
addStatus _ Contact {activeConn = Nothing} nss = nss errorNetworkStatus :: AgentErrorType -> String
addStatus connId Contact {activeConn = Just Connection {agentConnId}} nss =
let ns = (agentConnId, netStatus $ resultErr connId rs)
in ns : nss
netStatus :: Maybe ChatError -> NetworkStatus
netStatus = maybe NSConnected $ NSError . errorNetworkStatus
errorNetworkStatus :: ChatError -> String
errorNetworkStatus = \case errorNetworkStatus = \case
ChatErrorAgent (BROKER _ NETWORK) _ -> "network" BROKER _ NETWORK -> "network"
ChatErrorAgent (SMP _ SMP.AUTH) _ -> "contact deleted" SMP _ SMP.AUTH -> "contact deleted"
e -> show e e -> show e
-- TODO possibly below could be replaced with less noisy events for API contactLinkSubsToView :: Map ConnId AgentErrorType -> [(ConnId, Bool)] -> CM ()
contactLinkSubsToView :: Map ConnId (Either AgentErrorType ()) -> Map ConnId UserContact -> CM () contactLinkSubsToView errs ucs = do
contactLinkSubsToView rs = toView . CRUserContactSubSummary user . map (uncurry UserContactSubStatus) . resultsFor rs let (addresses, groupLinks) = partition snd ucs
groupSubsToView :: Map ConnId (Either AgentErrorType ()) -> [Group] -> Map ConnId GroupMember -> Bool -> CM () forM_ addresses $ \(acId, _uc) -> toView $ CRUserAddrSubStatus {user, userContactError = (`ChatErrorAgent` Nothing) <$> M.lookup acId errs}
groupSubsToView rs gs ms ce = do let groups = S.fromList $ map fst groupLinks
mapM_ groupSub $ errGroups = M.restrictKeys errs groups
sortOn (\(Group GroupInfo {localDisplayName = g} _) -> g) gs unless (S.null groups) $ toView CRUserGroupLinksSubSummary
toView . CRMemberSubSummary user $ map (uncurry MemberSubStatus) mRs { user,
okSubs = S.size groups - M.size errGroups,
errSubs = M.size errGroups
}
groupSubsToView :: Map ConnId AgentErrorType -> [(GroupInfo, [ConnId])] -> [ConnId] -> Map ConnId ContactName -> Bool -> CM ()
groupSubsToView errs gs allMembers names ce = do
mapM_ (uncurry groupSub) gs
toView CRMemberSubSummary {user, okSubs = S.size conns - M.size errConns, errSubs = M.size errConns}
where where
mRs = resultsFor rs ms conns = S.fromList allMembers
groupSub :: Group -> CM () errConns = M.restrictKeys errs conns
groupSub (Group g@GroupInfo {membership, groupId = gId} members) = do groupSub :: GroupInfo -> [ConnId] -> CM ()
when ce $ mapM_ (toView . uncurry (CRMemberSubError user g)) mErrors groupSub g@GroupInfo {membership} groupMembers = do
when ce $ mapM_ (toView . uncurry (CRMemberSubError user g) ) mErrors
toView groupEvent toView groupEvent
where where
mErrors :: [(GroupMember, ChatError)] mErrors :: [(ContactName, ChatError)]
mErrors = mErrors = sortOn fst $ mapMaybe mError groupMembers
sortOn (\(GroupMember {localDisplayName = n}, _) -> n) mError :: ConnId -> Maybe (ContactName, ChatError)
. filterErrors mError mConnId = do
$ filter (\(GroupMember {groupId}, _) -> groupId == gId) mRs mErr <- M.lookup mConnId errConns
name <- M.lookup mConnId names
Just (name, ChatErrorAgent mErr Nothing)
groupEvent :: ChatResponse groupEvent :: ChatResponse
groupEvent groupEvent
| memberStatus membership == GSMemInvited = CRGroupInvitation user g | memberStatus membership == GSMemInvited = CRGroupInvitation user g
| all (\GroupMember {activeConn} -> isNothing activeConn) members = | null groupMembers =
if memberActive membership if memberActive membership
then CRGroupEmpty user g then CRGroupEmpty user g
else CRGroupRemoved user g else CRGroupRemoved user g
| otherwise = CRGroupSubscribed user g | otherwise = CRGroupSubscribed user g
sndFileSubsToView :: Map ConnId (Either AgentErrorType ()) -> Map ConnId SndFileTransfer -> CM () sndFileSubsToView :: Map ConnId AgentErrorType -> Map ConnId SndFileTransfer -> CM ()
sndFileSubsToView rs sfts = do sndFileSubsToView errs sfts =
let sftRs = resultsFor rs sfts forM_ (M.assocs sfts) $ \(acId, ft@SndFileTransfer {fileId, fileStatus}) -> do
forM_ sftRs $ \(ft@SndFileTransfer {fileId, fileStatus}, err_) -> do forM_ (M.lookup acId errs) $ toView . CRSndFileSubError user ft . (`ChatErrorAgent` Nothing)
forM_ err_ $ toView . CRSndFileSubError user ft
void . forkIO $ do void . forkIO $ do
threadDelay 1000000 threadDelay 1000000
when (fileStatus == FSConnected) . unlessM (isFileActive fileId sndFiles) . withChatLock "subscribe sendFileChunk" $ when (fileStatus == FSConnected) . unlessM (isFileActive fileId sndFiles) . withChatLock "subscribe sendFileChunk" $
sendFileChunk user ft sendFileChunk user ft
rcvFileSubsToView :: Map ConnId (Either AgentErrorType ()) -> Map ConnId RcvFileTransfer -> CM () rcvFileSubsToView :: Map ConnId AgentErrorType -> Map ConnId RcvFileTransfer -> CM ()
rcvFileSubsToView rs = mapM_ (toView . uncurry (CRRcvFileSubError user)) . filterErrors . resultsFor rs rcvFileSubsToView errs = mapM_ (toView . uncurry (CRRcvFileSubError user)) . M.mapMaybeWithKey (\acId rft -> (\e -> (rft, ChatErrorAgent e Nothing)) <$> M.lookup acId errs)
pendingConnSubsToView :: Map ConnId (Either AgentErrorType ()) -> Map ConnId PendingContactConnection -> CM () pendingConnSubsToView :: Map ConnId AgentErrorType -> [ConnId] -> CM () -- XXX: ignored by View
pendingConnSubsToView rs = toView . CRPendingSubSummary user . map (uncurry PendingSubStatus) . resultsFor rs pendingConnSubsToView errs pcs = toView CRPendingSubSummary {user, okSubs = S.size conns - M.size errConns, errSubs = M.size errConns}
where
conns = S.fromList pcs
errConns = M.restrictKeys errs conns
withStore_ :: (DB.Connection -> User -> IO [a]) -> CM [a] withStore_ :: (DB.Connection -> User -> IO [a]) -> CM [a]
withStore_ a = withStore' (`a` user) `catchChatError` \e -> toView (CRChatError (Just user) e) $> [] withStore_ a = withStore' (`a` user) `catchChatError` \e -> toView (CRChatError (Just user) e) $> []
filterErrors :: [(a, Maybe ChatError)] -> [(a, ChatError)]
filterErrors = mapMaybe (\(a, e_) -> (a,) <$> e_)
resultsFor :: Map ConnId (Either AgentErrorType ()) -> Map ConnId a -> [(a, Maybe ChatError)]
resultsFor rs = M.foldrWithKey' addResult []
where
addResult :: ConnId -> a -> [(a, Maybe ChatError)] -> [(a, Maybe ChatError)]
addResult connId = (:) . (,resultErr connId rs)
resultErr :: ConnId -> Map ConnId (Either AgentErrorType ()) -> Maybe ChatError
resultErr connId rs = case M.lookup connId rs of
Just (Left e) -> Just $ ChatErrorAgent e Nothing
Just _ -> Nothing
_ -> Just . ChatError . CEAgentNoSubResult $ AgentConnId connId
cleanupManager :: CM () cleanupManager :: CM ()
cleanupManager = do cleanupManager = do
interval <- asks (cleanupManagerInterval . config) interval <- asks (cleanupManagerInterval . config)
@@ -3746,7 +3719,7 @@ processAgentMessageNoConn = \case
where where
connIds = map AgentConnId conns connIds = map AgentConnId conns
notifyAPI = toView . CRNetworkStatus nsStatus notifyAPI = toView . CRNetworkStatus nsStatus
notifyCLI = do notifyCLI = whenM (asks $ subscriptionEvents . config) $ do
cs <- withStore' (`getConnectionsContacts` conns) cs <- withStore' (`getConnectionsContacts` conns)
toView $ event srv cs toView $ event srv cs
+7 -8
View File
@@ -680,9 +680,10 @@ data ChatResponse
| CRSubscriptionEnd {user :: User, connectionEntity :: ConnectionEntity} | CRSubscriptionEnd {user :: User, connectionEntity :: ConnectionEntity}
| CRContactsDisconnected {server :: SMPServer, contactRefs :: [ContactRef]} | CRContactsDisconnected {server :: SMPServer, contactRefs :: [ContactRef]}
| CRContactsSubscribed {server :: SMPServer, contactRefs :: [ContactRef]} | CRContactsSubscribed {server :: SMPServer, contactRefs :: [ContactRef]}
| CRContactSubError {user :: User, contact :: Contact, chatError :: ChatError} | CRContactSubSummary {user :: User, okSubs :: Int, errSubs :: Int}
| CRContactSubSummary {user :: User, contactSubscriptions :: [ContactSubStatus]} | CRContactSubError {user :: User, contactName :: ContactName, chatError :: ChatError}
| CRUserContactSubSummary {user :: User, userContactSubscriptions :: [UserContactSubStatus]} | CRUserAddrSubStatus {user :: User, userContactError :: Maybe ChatError}
| CRUserGroupLinksSubSummary {user :: User, okSubs :: Int, errSubs :: Int}
| CRNetworkStatus {networkStatus :: NetworkStatus, connections :: [AgentConnId]} | CRNetworkStatus {networkStatus :: NetworkStatus, connections :: [AgentConnId]}
| CRNetworkStatuses {user_ :: Maybe User, networkStatuses :: [ConnNetworkStatus]} | CRNetworkStatuses {user_ :: Maybe User, networkStatuses :: [ConnNetworkStatus]}
| CRHostConnected {protocol :: AProtocolType, transportHost :: TransportHost} | CRHostConnected {protocol :: AProtocolType, transportHost :: TransportHost}
@@ -719,10 +720,10 @@ data ChatResponse
| CRNewMemberContactSentInv {user :: User, contact :: Contact, groupInfo :: GroupInfo, member :: GroupMember} | CRNewMemberContactSentInv {user :: User, contact :: Contact, groupInfo :: GroupInfo, member :: GroupMember}
| CRNewMemberContactReceivedInv {user :: User, contact :: Contact, groupInfo :: GroupInfo, member :: GroupMember} | CRNewMemberContactReceivedInv {user :: User, contact :: Contact, groupInfo :: GroupInfo, member :: GroupMember}
| CRContactAndMemberAssociated {user :: User, contact :: Contact, groupInfo :: GroupInfo, member :: GroupMember, updatedContact :: Contact} | CRContactAndMemberAssociated {user :: User, contact :: Contact, groupInfo :: GroupInfo, member :: GroupMember, updatedContact :: Contact}
| CRMemberSubError {user :: User, groupInfo :: GroupInfo, member :: GroupMember, chatError :: ChatError} | CRMemberSubError {user :: User, groupInfo :: GroupInfo, contactName :: ContactName, chatError :: ChatError}
| CRMemberSubSummary {user :: User, memberSubscriptions :: [MemberSubStatus]} | CRMemberSubSummary {user :: User, okSubs :: Int, errSubs :: Int}
| CRGroupSubscribed {user :: User, groupInfo :: GroupInfo} | CRGroupSubscribed {user :: User, groupInfo :: GroupInfo}
| CRPendingSubSummary {user :: User, pendingSubscriptions :: [PendingSubStatus]} | CRPendingSubSummary {user :: User, okSubs :: Int, errSubs :: Int}
| CRSndFileSubError {user :: User, sndFileTransfer :: SndFileTransfer, chatError :: ChatError} | CRSndFileSubError {user :: User, sndFileTransfer :: SndFileTransfer, chatError :: ChatError}
| CRRcvFileSubError {user :: User, rcvFileTransfer :: RcvFileTransfer, chatError :: ChatError} | CRRcvFileSubError {user :: User, rcvFileTransfer :: RcvFileTransfer, chatError :: ChatError}
| CRCallInvitation {callInvitation :: RcvCallInvitation} | CRCallInvitation {callInvitation :: RcvCallInvitation}
@@ -731,8 +732,6 @@ data ChatResponse
| CRCallExtraInfo {user :: User, contact :: Contact, extraInfo :: WebRTCExtraInfo} | CRCallExtraInfo {user :: User, contact :: Contact, extraInfo :: WebRTCExtraInfo}
| CRCallEnded {user :: User, contact :: Contact} | CRCallEnded {user :: User, contact :: Contact}
| CRCallInvitations {callInvitations :: [RcvCallInvitation]} | CRCallInvitations {callInvitations :: [RcvCallInvitation]}
| CRUserContactLinkSubscribed -- TODO delete
| CRUserContactLinkSubError {chatError :: ChatError} -- TODO delete
| CRNtfTokenStatus {status :: NtfTknStatus} | CRNtfTokenStatus {status :: NtfTknStatus}
| CRNtfToken {token :: DeviceToken, status :: NtfTknStatus, ntfMode :: NotificationsMode, ntfServer :: NtfServer} | CRNtfToken {token :: DeviceToken, status :: NtfTknStatus, ntfMode :: NotificationsMode, ntfServer :: NtfServer}
| CRNtfMessages {user_ :: Maybe User, connEntity_ :: Maybe ConnectionEntity, msgTs :: Maybe UTCTime, ntfMessages :: [NtfMsgInfo]} | CRNtfMessages {user_ :: Maybe User, connEntity_ :: Maybe ConnectionEntity, msgTs :: Maybe UTCTime, ntfMessages :: [NtfMsgInfo]}
@@ -0,0 +1,18 @@
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Chat.Migrations.M20240530_user_contact_links_user_id where
import Database.SQLite.Simple (Query)
import Database.SQLite.Simple.QQ (sql)
m20240530_user_contact_links_user_id :: Query
m20240530_user_contact_links_user_id =
[sql|
CREATE INDEX idx_user_contact_links_user_id ON user_contact_links(user_id);
|]
down_m20240530_user_contact_links_user_id :: Query
down_m20240530_user_contact_links_user_id =
[sql|
DROP INDEX idx_user_contact_links_user_id;
|]
@@ -882,3 +882,4 @@ CREATE INDEX idx_chat_items_fwd_from_group_id ON chat_items(fwd_from_group_id);
CREATE INDEX idx_chat_items_fwd_from_chat_item_id ON chat_items( CREATE INDEX idx_chat_items_fwd_from_chat_item_id ON chat_items(
fwd_from_chat_item_id fwd_from_chat_item_id
); );
CREATE INDEX idx_user_contact_links_user_id ON user_contact_links(user_id);
+9 -12
View File
@@ -19,7 +19,7 @@ module Simplex.Chat.Store.Connections
where where
import Control.Applicative ((<|>)) import Control.Applicative ((<|>))
import Control.Monad import Control.Monad (forM)
import Control.Monad.Except import Control.Monad.Except
import Data.Int (Int64) import Data.Int (Int64)
import Data.Maybe (catMaybes, fromMaybe) import Data.Maybe (catMaybes, fromMaybe)
@@ -28,7 +28,6 @@ import Database.SQLite.Simple.QQ (sql)
import Simplex.Chat.Protocol import Simplex.Chat.Protocol
import Simplex.Chat.Store.Files import Simplex.Chat.Store.Files
import Simplex.Chat.Store.Groups import Simplex.Chat.Store.Groups
import Simplex.Chat.Store.Profiles
import Simplex.Chat.Store.Shared import Simplex.Chat.Store.Shared
import Simplex.Chat.Types import Simplex.Chat.Types
import Simplex.Messaging.Agent.Protocol (ConnId) import Simplex.Messaging.Agent.Protocol (ConnId)
@@ -213,19 +212,17 @@ getContactConnEntityByConnReqHash db vr user@User {userId} (cReqHash1, cReqHash2
(userId, cReqHash1, cReqHash2, ConnDeleted) (userId, cReqHash1, cReqHash2, ConnDeleted)
maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getConnectionEntity db vr user) connId_ maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getConnectionEntity db vr user) connId_
getConnectionsToSubscribe :: DB.Connection -> VersionRangeChat -> IO ([ConnId], [ConnectionEntity]) getConnectionsToSubscribe :: DB.Connection -> VersionRangeChat -> User -> IO ([ConnId], [ConnectionEntity])
getConnectionsToSubscribe db vr = do getConnectionsToSubscribe db vr user@User {userId} = do
aConnIds <- map fromOnly <$> DB.query_ db "SELECT agent_conn_id FROM connections where to_subscribe = 1" aConnIds <- map fromOnly <$> DB.query db "SELECT agent_conn_id FROM connections WHERE to_subscribe = 1 AND user_id = ?" (Only userId)
entities <- forM aConnIds $ \acId -> do entities <- forM aConnIds $ \acId ->
getUserByAConnId db acId >>= \case eitherToMaybe <$> runExceptT (getConnectionEntity db vr user acId)
Just user -> eitherToMaybe <$> runExceptT (getConnectionEntity db vr user acId) unsetConnectionToSubscribe db user
Nothing -> pure Nothing
unsetConnectionToSubscribe db
let connIds = map (\(AgentConnId connId) -> connId) aConnIds let connIds = map (\(AgentConnId connId) -> connId) aConnIds
pure (connIds, catMaybes entities) pure (connIds, catMaybes entities)
unsetConnectionToSubscribe :: DB.Connection -> IO () unsetConnectionToSubscribe :: DB.Connection -> User -> IO ()
unsetConnectionToSubscribe db = DB.execute_ db "UPDATE connections SET to_subscribe = 0 WHERE to_subscribe = 1" unsetConnectionToSubscribe db User {userId} = DB.execute db "UPDATE connections SET to_subscribe = 0 WHERE user_id = ? AND to_subscribe = 1" (Only userId)
deleteConnectionRecord :: DB.Connection -> User -> Int64 -> IO () deleteConnectionRecord :: DB.Connection -> User -> Int64 -> IO ()
deleteConnectionRecord db User {userId} cId = do deleteConnectionRecord db User {userId} cId = do
+31 -13
View File
@@ -56,6 +56,7 @@ module Simplex.Chat.Store.Direct
incQuotaErrCounter, incQuotaErrCounter,
setQuotaErrCounter, setQuotaErrCounter,
getUserContacts, getUserContacts,
getUserContactConnIds,
createOrUpdateContactRequest, createOrUpdateContactRequest,
getContactRequest', getContactRequest',
getContactRequest, getContactRequest,
@@ -839,19 +840,9 @@ getUserByContactRequestId db contactRequestId =
ExceptT . firstRow toUser (SEUserNotFoundByContactRequestId contactRequestId) $ ExceptT . firstRow toUser (SEUserNotFoundByContactRequestId contactRequestId) $
DB.query db (userQuery <> " JOIN contact_requests cr ON cr.user_id = u.user_id WHERE cr.contact_request_id = ?") (Only contactRequestId) DB.query db (userQuery <> " JOIN contact_requests cr ON cr.user_id = u.user_id WHERE cr.contact_request_id = ?") (Only contactRequestId)
getPendingContactConnections :: DB.Connection -> User -> IO [PendingContactConnection] getPendingContactConnections :: DB.Connection -> User -> IO [ConnId]
getPendingContactConnections db User {userId} = do getPendingContactConnections db User {userId} =
map toPendingContactConnection map fromOnly <$> DB.query db "SELECT agent_conn_id FROM connections WHERE user_id = ? AND conn_type = ? AND contact_id IS NULL" (userId, ConnContact)
<$> DB.queryNamed
db
[sql|
SELECT connection_id, agent_conn_id, conn_status, via_contact_uri_hash, via_user_contact_link, group_link_id, custom_user_profile_id, conn_req_inv, local_alias, created_at, updated_at
FROM connections
WHERE user_id = :user_id
AND conn_type = :conn_type
AND contact_id IS NULL
|]
[":user_id" := userId, ":conn_type" := ConnContact]
getContactConnections :: DB.Connection -> VersionRangeChat -> UserId -> Contact -> IO [Connection] getContactConnections :: DB.Connection -> VersionRangeChat -> UserId -> Contact -> IO [Connection]
getContactConnections db vr userId Contact {contactId} = getContactConnections db vr userId Contact {contactId} =
@@ -873,6 +864,33 @@ getContactConnections db vr userId Contact {contactId} =
connections [] = pure [] connections [] = pure []
connections rows = pure $ map (toConnection vr) rows connections rows = pure $ map (toConnection vr) rows
getUserContactConnIds :: DB.Connection -> User -> IO [ConnId]
getUserContactConnIds db User {userId} =
map fromOnly
<$> DB.query
db
[sql|
SELECT c.agent_conn_id
FROM contacts ct
LEFT JOIN connections c ON c.contact_id = ct.contact_id
WHERE ct.user_id = ?
AND ct.contact_status = ?
AND ct.deleted = 0
AND
c.connection_id = (
SELECT cc_connection_id FROM (
SELECT
cc.connection_id AS cc_connection_id,
cc.created_at AS cc_created_at
FROM connections cc
WHERE cc.user_id = ct.user_id AND cc.contact_id = ct.contact_id
ORDER BY cc_created_at DESC
LIMIT 1
)
)
|]
(userId, CSActive)
getConnectionById :: DB.Connection -> VersionRangeChat -> User -> Int64 -> ExceptT StoreError IO Connection getConnectionById :: DB.Connection -> VersionRangeChat -> User -> Int64 -> ExceptT StoreError IO Connection
getConnectionById db vr User {userId} connId = ExceptT $ do getConnectionById db vr User {userId} connId = ExceptT $ do
firstRow (toConnection vr) (SEConnectionNotFoundById connId) $ firstRow (toConnection vr) (SEConnectionNotFoundById connId) $
+29
View File
@@ -52,6 +52,7 @@ module Simplex.Chat.Store.Groups
deleteGroupItemsAndMembers, deleteGroupItemsAndMembers,
deleteGroup, deleteGroup,
getUserGroups, getUserGroups,
getUserGroupMemberConnIds,
getUserGroupDetails, getUserGroupDetails,
getUserGroupsWithSummary, getUserGroupsWithSummary,
getGroupSummary, getGroupSummary,
@@ -628,6 +629,14 @@ getUserGroups db vr user@User {userId} = do
groupIds <- map fromOnly <$> DB.query db "SELECT group_id FROM groups WHERE user_id = ?" (Only userId) groupIds <- map fromOnly <$> DB.query db "SELECT group_id FROM groups WHERE user_id = ?" (Only userId)
rights <$> mapM (runExceptT . getGroup db vr user) groupIds rights <$> mapM (runExceptT . getGroup db vr user) groupIds
getUserGroupMemberConnIds :: DB.Connection -> VersionRangeChat -> User -> IO [(GroupInfo, [ConnId])]
getUserGroupMemberConnIds db vr user@User {userId} = do
groupIds <- map fromOnly <$> DB.query db "SELECT group_id FROM groups WHERE user_id = ? ORDER BY local_display_name" (Only userId)
fmap rights . forM groupIds $ \groupId -> runExceptT $ do
gInfo <- getGroupInfo db vr user groupId
members <- liftIO $ getGroupMemberConnIds db user gInfo
pure (gInfo, members)
getUserGroupDetails :: DB.Connection -> VersionRangeChat -> User -> Maybe ContactId -> Maybe String -> IO [GroupInfo] getUserGroupDetails :: DB.Connection -> VersionRangeChat -> User -> Maybe ContactId -> Maybe String -> IO [GroupInfo]
getUserGroupDetails db vr User {userId, userContactId} _contactId_ search_ = getUserGroupDetails db vr User {userId, userContactId} _contactId_ search_ =
map (toGroupInfo vr userContactId) map (toGroupInfo vr userContactId)
@@ -748,6 +757,26 @@ getGroupMembers db vr user@User {userId, userContactId} GroupInfo {groupId} = do
(groupMemberQuery <> " WHERE m.group_id = ? AND m.user_id = ? AND (m.contact_id IS NULL OR m.contact_id != ?)") (groupMemberQuery <> " WHERE m.group_id = ? AND m.user_id = ? AND (m.contact_id IS NULL OR m.contact_id != ?)")
(userId, groupId, userId, userContactId) (userId, groupId, userId, userContactId)
getGroupMemberConnIds :: DB.Connection -> User -> GroupInfo -> IO [ConnId]
getGroupMemberConnIds db User {userId, userContactId} GroupInfo {groupId} = do
map fromOnly
<$> DB.query
db
[sql|
SELECT c.agent_conn_id
FROM group_members m
JOIN connections c ON c.connection_id = (
SELECT max(cc.connection_id)
FROM connections cc
WHERE cc.user_id = ? AND cc.group_member_id = m.group_member_id
)
WHERE m.group_id = ?
AND m.user_id = ?
AND (m.contact_id IS NULL OR m.contact_id != ?)
AND m.member_status NOT IN (?, ?, ?, ?)
|]
(userId, groupId, userId, userContactId, GSMemRemoved, GSMemLeft, GSMemGroupDeleted, GSMemUnknown)
getGroupMembersForExpiration :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> IO [GroupMember] getGroupMembersForExpiration :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> IO [GroupMember]
getGroupMembersForExpiration db vr user@User {userId, userContactId} GroupInfo {groupId} = do getGroupMembersForExpiration db vr user@User {userId, userContactId} GroupInfo {groupId} = do
map (toContactMember vr user) map (toContactMember vr user)
+3 -1
View File
@@ -110,6 +110,7 @@ import Simplex.Chat.Migrations.M20240501_chat_deleted
import Simplex.Chat.Migrations.M20240510_chat_items_via_proxy import Simplex.Chat.Migrations.M20240510_chat_items_via_proxy
import Simplex.Chat.Migrations.M20240515_rcv_files_user_approved_relays import Simplex.Chat.Migrations.M20240515_rcv_files_user_approved_relays
import Simplex.Chat.Migrations.M20240528_quota_err_counter import Simplex.Chat.Migrations.M20240528_quota_err_counter
import Simplex.Chat.Migrations.M20240530_user_contact_links_user_id
import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..)) import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..))
schemaMigrations :: [(String, Query, Maybe Query)] schemaMigrations :: [(String, Query, Maybe Query)]
@@ -219,7 +220,8 @@ schemaMigrations =
("20240501_chat_deleted", m20240501_chat_deleted, Just down_m20240501_chat_deleted), ("20240501_chat_deleted", m20240501_chat_deleted, Just down_m20240501_chat_deleted),
("20240510_chat_items_via_proxy", m20240510_chat_items_via_proxy, Just down_m20240510_chat_items_via_proxy), ("20240510_chat_items_via_proxy", m20240510_chat_items_via_proxy, Just down_m20240510_chat_items_via_proxy),
("20240515_rcv_files_user_approved_relays", m20240515_rcv_files_user_approved_relays, Just down_m20240515_rcv_files_user_approved_relays), ("20240515_rcv_files_user_approved_relays", m20240515_rcv_files_user_approved_relays, Just down_m20240515_rcv_files_user_approved_relays),
("20240528_quota_err_counter", m20240528_quota_err_counter, Just down_m20240528_quota_err_counter) ("20240528_quota_err_counter", m20240528_quota_err_counter, Just down_m20240528_quota_err_counter),
("20240530_user_contact_links_user_id", m20240530_user_contact_links_user_id, Just down_m20240530_user_contact_links_user_id)
] ]
-- | The list of migrations in ascending order by date -- | The list of migrations in ascending order by date
+11 -19
View File
@@ -350,25 +350,17 @@ getUserAddressConnections db vr User {userId} = do
|] |]
(userId, userId) (userId, userId)
getUserContactLinks :: DB.Connection -> VersionRangeChat -> User -> IO [(Connection, UserContact)] getUserContactLinks :: DB.Connection -> User -> IO [(ConnId, Bool)]
getUserContactLinks db vr User {userId} = getUserContactLinks db User {userId} =
map toUserContactConnection DB.query
<$> DB.query db
db [sql|
[sql| SELECT c.agent_conn_id, uc.group_id IS NULL
SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, FROM connections c
c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, JOIN user_contact_links uc ON c.user_contact_link_id = uc.user_contact_link_id
c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, WHERE c.user_id = ? AND uc.user_id = ?
c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version, |]
uc.user_contact_link_id, uc.conn_req_contact, uc.group_id (userId, userId)
FROM connections c
JOIN user_contact_links uc ON c.user_contact_link_id = uc.user_contact_link_id
WHERE c.user_id = ? AND uc.user_id = ?
|]
(userId, userId)
where
toUserContactConnection :: (ConnectionRow :. (Int64, ConnReqContact, Maybe GroupId)) -> (Connection, UserContact)
toUserContactConnection (connRow :. (userContactLinkId, connReqContact, groupId)) = (toConnection vr connRow, UserContact {userContactLinkId, connReqContact, groupId})
deleteUserAddress :: DB.Connection -> User -> IO () deleteUserAddress :: DB.Connection -> User -> IO ()
deleteUserAddress db user@User {userId} = do deleteUserAddress db user@User {userId} = do
+13 -21
View File
@@ -18,7 +18,7 @@ import qualified Data.ByteString.Lazy.Char8 as LB
import Data.Char (isSpace, toUpper) import Data.Char (isSpace, toUpper)
import Data.Function (on) import Data.Function (on)
import Data.Int (Int64) import Data.Int (Int64)
import Data.List (groupBy, intercalate, intersperse, partition, sortOn) import Data.List (groupBy, intercalate, intersperse, sortOn)
import Data.List.NonEmpty (NonEmpty (..)) import Data.List.NonEmpty (NonEmpty (..))
import qualified Data.List.NonEmpty as L import qualified Data.List.NonEmpty as L
import Data.Map.Strict (Map) import Data.Map.Strict (Map)
@@ -238,19 +238,13 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe
in ttyUser u [sShow connId <> ": END"] in ttyUser u [sShow connId <> ": END"]
CRContactsDisconnected srv cs -> [plain $ "server disconnected " <> showSMPServer srv <> " (" <> contactList cs <> ")"] CRContactsDisconnected srv cs -> [plain $ "server disconnected " <> showSMPServer srv <> " (" <> contactList cs <> ")"]
CRContactsSubscribed srv cs -> [plain $ "server connected " <> showSMPServer srv <> " (" <> contactList cs <> ")"] CRContactsSubscribed srv cs -> [plain $ "server connected " <> showSMPServer srv <> " (" <> contactList cs <> ")"]
CRContactSubError u c e -> ttyUser u [ttyContact' c <> ": contact error " <> sShow e] CRContactSubError u c e -> ttyUser u [ttyContact c <> ": contact error " <> sShow e]
CRContactSubSummary u summary -> CRContactSubSummary {user, okSubs, errSubs} -> ttyUser user $ [sShow okSubs <> " contacts connected (use " <> highlight' "/cs" <> " for the list)" | okSubs > 0] <> viewErrorsSummary errSubs " contact errors"
ttyUser u $ [sShow (length subscribed) <> " contacts connected (use " <> highlight' "/cs" <> " for the list)" | not (null subscribed)] <> viewErrorsSummary errors " contact errors" CRUserAddrSubStatus {user, userContactError} -> ttyUser user [maybe ("Your address is active! To show: " <> highlight' "/sa") (\e -> "User address error: " <> sShow e <> ", to delete your address: " <> highlight' "/da") userContactError]
where CRUserGroupLinksSubSummary {user, okSubs, errSubs} ->
(errors, subscribed) = partition (isJust . contactError) summary ttyUser user $
CRUserContactSubSummary u summary -> [sShow okSubs <> " group links active" | okSubs > 0]
ttyUser u $ <> viewErrorsSummary errSubs " group link errors"
map addressSS addresses
<> ([sShow (length groupLinksSubscribed) <> " group links active" | not (null groupLinksSubscribed)] <> viewErrorsSummary groupLinkErrors " group link errors")
where
(addresses, groupLinks) = partition (\UserContactSubStatus {userContact} -> isNothing . userContactGroupId $ userContact) summary
addressSS UserContactSubStatus {userContactError} = maybe ("Your address is active! To show: " <> highlight' "/sa") (\e -> "User address error: " <> sShow e <> ", to delete your address: " <> highlight' "/da") userContactError
(groupLinkErrors, groupLinksSubscribed) = partition (isJust . userContactError) groupLinks
CRNetworkStatus status conns -> if testView then [plain $ show (length conns) <> " connections " <> netStatusStr status] else [] CRNetworkStatus status conns -> if testView then [plain $ show (length conns) <> " connections " <> netStatusStr status] else []
CRNetworkStatuses u statuses -> if testView then ttyUser' u $ viewNetworkStatuses statuses else [] CRNetworkStatuses u statuses -> if testView then ttyUser' u $ viewNetworkStatuses statuses else []
CRGroupInvitation u g -> ttyUser u [groupInvitation' g] CRGroupInvitation u g -> ttyUser u [groupInvitation' g]
@@ -284,10 +278,10 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe
CRNewMemberContactSentInv u _ct g m -> ttyUser u ["sent invitation to connect directly to member " <> ttyGroup' g <> " " <> ttyMember m] CRNewMemberContactSentInv u _ct g m -> ttyUser u ["sent invitation to connect directly to member " <> ttyGroup' g <> " " <> ttyMember m]
CRNewMemberContactReceivedInv u ct g m -> ttyUser u [ttyGroup' g <> " " <> ttyMember m <> " is creating direct contact " <> ttyContact' ct <> " with you"] CRNewMemberContactReceivedInv u ct g m -> ttyUser u [ttyGroup' g <> " " <> ttyMember m <> " is creating direct contact " <> ttyContact' ct <> " with you"]
CRContactAndMemberAssociated u ct g m ct' -> ttyUser u $ viewContactAndMemberAssociated ct g m ct' CRContactAndMemberAssociated u ct g m ct' -> ttyUser u $ viewContactAndMemberAssociated ct g m ct'
CRMemberSubError u g m e -> ttyUser u [ttyGroup' g <> " member " <> ttyMember m <> " error: " <> sShow e] CRMemberSubError u g m e -> ttyUser u [ttyGroup' g <> " member " <> ttyContact m <> " error: " <> sShow e]
CRMemberSubSummary u summary -> ttyUser u $ viewErrorsSummary (filter (isJust . memberError) summary) " group member errors" CRMemberSubSummary {user, errSubs} -> ttyUser user $ viewErrorsSummary errSubs " group member errors"
CRGroupSubscribed u g -> ttyUser u $ viewGroupSubscribed g CRGroupSubscribed u g -> ttyUser u $ viewGroupSubscribed g
CRPendingSubSummary u _ -> ttyUser u [] CRPendingSubSummary {user} -> ttyUser user [] -- XXX: ???
CRSndFileSubError u SndFileTransfer {fileId, fileName} e -> CRSndFileSubError u SndFileTransfer {fileId, fileName} e ->
ttyUser u ["sent file " <> sShow fileId <> " (" <> plain fileName <> ") error: " <> sShow e] ttyUser u ["sent file " <> sShow fileId <> " (" <> plain fileName <> ") error: " <> sShow e]
CRRcvFileSubError u RcvFileTransfer {fileId, fileInvitation = FileInvitation {fileName}} e -> CRRcvFileSubError u RcvFileTransfer {fileId, fileInvitation = FileInvitation {fileName}} e ->
@@ -298,8 +292,6 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe
CRCallExtraInfo {user = u, contact} -> ttyUser u ["call extra info from " <> ttyContact' contact] CRCallExtraInfo {user = u, contact} -> ttyUser u ["call extra info from " <> ttyContact' contact]
CRCallEnded {user = u, contact} -> ttyUser u ["call with " <> ttyContact' contact <> " ended"] CRCallEnded {user = u, contact} -> ttyUser u ["call with " <> ttyContact' contact <> " ended"]
CRCallInvitations _ -> [] CRCallInvitations _ -> []
CRUserContactLinkSubscribed -> ["Your address is active! To show: " <> highlight' "/sa"]
CRUserContactLinkSubError e -> ["user address error: " <> sShow e, "to delete your address: " <> highlight' "/da"]
CRContactConnectionDeleted u PendingContactConnection {pccConnId} -> ttyUser u ["connection :" <> sShow pccConnId <> " deleted"] CRContactConnectionDeleted u PendingContactConnection {pccConnId} -> ttyUser u ["connection :" <> sShow pccConnId <> " deleted"]
CRNtfTokenStatus status -> ["device token status: " <> plain (smpEncode status)] CRNtfTokenStatus status -> ["device token status: " <> plain (smpEncode status)]
CRNtfToken _ status mode srv -> ["device token status: " <> plain (smpEncode status) <> ", notifications mode: " <> plain (strEncode mode) <> ", server: " <> sShow srv] CRNtfToken _ status mode srv -> ["device token status: " <> plain (smpEncode status) <> ", notifications mode: " <> plain (strEncode mode) <> ", server: " <> sShow srv]
@@ -458,8 +450,8 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe
testViewItem (CChatItem _ ci@ChatItem {meta = CIMeta {itemText}}) membership_ = testViewItem (CChatItem _ ci@ChatItem {meta = CIMeta {itemText}}) membership_ =
let deleted_ = maybe "" (\t -> " [" <> t <> "]") (chatItemDeletedText ci membership_) let deleted_ = maybe "" (\t -> " [" <> t <> "]") (chatItemDeletedText ci membership_)
in itemText <> deleted_ in itemText <> deleted_
viewErrorsSummary :: [a] -> StyledString -> [StyledString] viewErrorsSummary :: Int -> StyledString -> [StyledString]
viewErrorsSummary summary s = [ttyError (T.pack . show $ length summary) <> s <> " (run with -c option to show each error)" | not (null summary)] viewErrorsSummary numErrors s = [ttyError (tshow numErrors) <> s <> " (run with -c option to show each error)" | numErrors > 0]
contactList :: [ContactRef] -> String contactList :: [ContactRef] -> String
contactList cs = T.unpack . T.intercalate ", " $ map (\ContactRef {localDisplayName = n} -> "@" <> n) cs contactList cs = T.unpack . T.intercalate ", " $ map (\ContactRef {localDisplayName = n} -> "@" <> n) cs
unmuted :: User -> ChatInfo c -> ChatItem c d -> [StyledString] -> [StyledString] unmuted :: User -> ChatInfo c -> ChatItem c d -> [StyledString] -> [StyledString]
+9 -10
View File
@@ -1448,6 +1448,7 @@ testUsersSubscribeAfterRestart :: HasCallStack => FilePath -> IO ()
testUsersSubscribeAfterRestart tmp = do testUsersSubscribeAfterRestart tmp = do
withNewTestChat tmp "bob" bobProfile $ \bob -> do withNewTestChat tmp "bob" bobProfile $ \bob -> do
withNewTestChat tmp "alice" aliceProfile $ \alice -> do withNewTestChat tmp "alice" aliceProfile $ \alice -> do
threadDelay 100000
connectUsers alice bob connectUsers alice bob
alice <##> bob alice <##> bob
@@ -1458,8 +1459,7 @@ testUsersSubscribeAfterRestart tmp = do
withTestChat tmp "alice" $ \alice -> do withTestChat tmp "alice" $ \alice -> do
-- second user is active -- second user is active
alice <## "1 contacts connected (use /cs for the list)" alice <### ["1 contacts connected (use /cs for the list)", "[user: alice] 1 contacts connected (use /cs for the list)"]
alice <## "[user: alice] 1 contacts connected (use /cs for the list)"
-- second user receives message -- second user receives message
alice <##> bob alice <##> bob
@@ -1793,8 +1793,7 @@ testUsersRestartCIExpiration tmp = do
showActiveUser alice "alice (Alice)" showActiveUser alice "alice (Alice)"
withTestChatCfg tmp cfg "alice" $ \alice -> do withTestChatCfg tmp cfg "alice" $ \alice -> do
alice <## "1 contacts connected (use /cs for the list)" alice <### ["1 contacts connected (use /cs for the list)", "[user: alisa] 1 contacts connected (use /cs for the list)"]
alice <## "[user: alisa] 1 contacts connected (use /cs for the list)"
-- first user messages -- first user messages
alice ##> "/user alice" alice ##> "/user alice"
@@ -1892,8 +1891,7 @@ testEnableCIExpirationOnlyForOneUser tmp = do
alice #$> ("/_get chat @4 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")]) alice #$> ("/_get chat @4 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")])
withTestChatCfg tmp cfg "alice" $ \alice -> do withTestChatCfg tmp cfg "alice" $ \alice -> do
alice <## "1 contacts connected (use /cs for the list)" alice <### ["1 contacts connected (use /cs for the list)", "[user: alice] 1 contacts connected (use /cs for the list)"]
alice <## "[user: alice] 1 contacts connected (use /cs for the list)"
-- messages are not deleted for second user after restart -- messages are not deleted for second user after restart
alice #$> ("/_get chat @4 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")]) alice #$> ("/_get chat @4 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")])
@@ -1948,8 +1946,7 @@ testDisableCIExpirationOnlyForOneUser tmp = do
alice #$> ("/_get chat @4 count=100", chat, []) alice #$> ("/_get chat @4 count=100", chat, [])
withTestChatCfg tmp cfg "alice" $ \alice -> do withTestChatCfg tmp cfg "alice" $ \alice -> do
alice <## "1 contacts connected (use /cs for the list)" alice <### ["1 contacts connected (use /cs for the list)", "[user: alice] 1 contacts connected (use /cs for the list)"]
alice <## "[user: alice] 1 contacts connected (use /cs for the list)"
-- second user still has ttl configured after restart -- second user still has ttl configured after restart
alice #$> ("/ttl", id, "old messages are set to be deleted after: 1 second(s)") alice #$> ("/ttl", id, "old messages are set to be deleted after: 1 second(s)")
@@ -2055,8 +2052,10 @@ testUsersTimedMessages tmp = do
alice <# "bob> alisa 4" alice <# "bob> alisa 4"
withTestChat tmp "alice" $ \alice -> do withTestChat tmp "alice" $ \alice -> do
alice <## "1 contacts connected (use /cs for the list)" alice
alice <## "[user: alice] 1 contacts connected (use /cs for the list)" <### [ "1 contacts connected (use /cs for the list)",
"[user: alice] 1 contacts connected (use /cs for the list)"
]
alice ##> "/user alice" alice ##> "/user alice"
showActiveUser alice "alice (Alice)" showActiveUser alice "alice (Alice)"
+2
View File
@@ -2590,6 +2590,7 @@ testPlanGroupLinkConnecting :: HasCallStack => FilePath -> IO ()
testPlanGroupLinkConnecting tmp = do testPlanGroupLinkConnecting tmp = do
-- gLink <- withNewTestChatCfg tmp cfg "alice" aliceProfile $ \alice -> do -- gLink <- withNewTestChatCfg tmp cfg "alice" aliceProfile $ \alice -> do
gLink <- withNewTestChatCfg tmp cfg "alice" aliceProfile $ \a -> withTestOutput a $ \alice -> do gLink <- withNewTestChatCfg tmp cfg "alice" aliceProfile $ \a -> withTestOutput a $ \alice -> do
threadDelay 100000
alice ##> "/g team" alice ##> "/g team"
alice <## "group #team is created" alice <## "group #team is created"
alice <## "to add members use /a team <name> or /create link #team" alice <## "to add members use /a team <name> or /create link #team"
@@ -3200,6 +3201,7 @@ testPlanGroupLinkNoContactKnown =
testPlanGroupLinkNoContactConnecting :: HasCallStack => FilePath -> IO () testPlanGroupLinkNoContactConnecting :: HasCallStack => FilePath -> IO ()
testPlanGroupLinkNoContactConnecting tmp = do testPlanGroupLinkNoContactConnecting tmp = do
gLink <- withNewTestChat tmp "alice" aliceProfile $ \alice -> do gLink <- withNewTestChat tmp "alice" aliceProfile $ \alice -> do
threadDelay 100000
alice ##> "/g team" alice ##> "/g team"
alice <## "group #team is created" alice <## "group #team is created"
alice <## "to add members use /a team <name> or /create link #team" alice <## "to add members use /a team <name> or /create link #team"
+3 -4
View File
@@ -136,10 +136,10 @@ networkStatuses =
#endif #endif
networkStatusesSwift :: LB.ByteString networkStatusesSwift :: LB.ByteString
networkStatusesSwift = "{\"resp\":{\"_owsf\":true,\"networkStatuses\":{\"user_\":" <> userJSON <> ",\"networkStatuses\":[]}}}" networkStatusesSwift = "{\"resp\":{\"_owsf\":true,\"networkStatuses\":{\"networkStatuses\":[]}}}"
networkStatusesTagged :: LB.ByteString networkStatusesTagged :: LB.ByteString
networkStatusesTagged = "{\"resp\":{\"type\":\"networkStatuses\",\"user_\":" <> userJSON <> ",\"networkStatuses\":[]}}" networkStatusesTagged = "{\"resp\":{\"type\":\"networkStatuses\",\"networkStatuses\":[]}}"
memberSubSummary :: LB.ByteString memberSubSummary :: LB.ByteString
memberSubSummary = memberSubSummary =
@@ -222,8 +222,7 @@ testChatApi tmp = do
chatSendCmd cc "/u" `shouldReturn` activeUser chatSendCmd cc "/u" `shouldReturn` activeUser
chatSendCmd cc "/create user alice Alice" `shouldReturn` activeUserExists chatSendCmd cc "/create user alice Alice" `shouldReturn` activeUserExists
chatSendCmd cc "/_start" `shouldReturn` chatStarted chatSendCmd cc "/_start" `shouldReturn` chatStarted
chatRecvMsg cc `shouldReturn` networkStatuses chatSendCmd cc "/_network_statuses" `shouldReturn` networkStatuses
chatRecvMsg cc `shouldReturn` userContactSubSummary
chatRecvMsgWait cc 10000 `shouldReturn` "" chatRecvMsgWait cc 10000 `shouldReturn` ""
chatParseMarkdown "hello" `shouldBe` "{}" chatParseMarkdown "hello" `shouldBe` "{}"
chatParseMarkdown "*hello*" `shouldBe` parsedMarkdown chatParseMarkdown "*hello*" `shouldBe` parsedMarkdown