diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt index 82505b3d7..e9864edfa 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt @@ -1611,7 +1611,7 @@ sealed class CC { val cmdString: String get() = when (this) { is Console -> cmd is ShowActiveUser -> "/u" - is CreateActiveUser -> "/u ${profile.displayName} ${profile.fullName}" + is CreateActiveUser -> "/create user ${profile.displayName} ${profile.fullName}" is StartChat -> "/_start subscribe=on expire=${onOff(expire)}" is ApiStopChat -> "/_stop" is SetFilesFolder -> "/_files_folder $filesFolder" diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 1c9f99185..27f26a7a9 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -95,7 +95,7 @@ public enum ChatCommand { get { switch self { case .showActiveUser: return "/u" - case let .createActiveUser(profile): return "/u \(profile.displayName) \(profile.fullName)" + case let .createActiveUser(profile): return "/create user \(profile.displayName) \(profile.fullName)" case let .startChat(subscribe, expire): return "/_start subscribe=\(onOff(subscribe)) expire=\(onOff(expire))" case .apiStopChat: return "/_stop" case .apiActivateChat: return "/_app activate" diff --git a/packages/simplex-chat-client/typescript/src/command.ts b/packages/simplex-chat-client/typescript/src/command.ts index e3b017284..2694e4d25 100644 --- a/packages/simplex-chat-client/typescript/src/command.ts +++ b/packages/simplex-chat-client/typescript/src/command.ts @@ -450,7 +450,7 @@ export function cmdString(cmd: ChatCommand): string { case "showActiveUser": return "/u" case "createActiveUser": - return `/u ${JSON.stringify(cmd.profile)}` + return `/create user ${JSON.stringify(cmd.profile)}` case "startChat": return `/_start subscribe=${cmd.subscribeConnections ? "on" : "off"} expire=${cmd.expireChatItems ? "on" : "off"}` case "apiStopChat": diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index ec7a5c0d7..b36c634f8 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -213,7 +213,8 @@ startChatController user subConns enableExpireCIs = do setExpireCIs True _ -> setExpireCIs True runExpireCIs = forever $ do - flip catchError (toView . CRChatError) $ do + -- TODO per user + flip catchError (toView . CRChatError (Just user)) $ do expire <- asks expireCIs atomically $ readTVar expire >>= \b -> unless b retry ttl <- withStore' (`getChatItemTTL` user) @@ -237,8 +238,10 @@ stopChatController ChatController {smpAgent, agentAsync = s, expireCIs} = do execChatCommand :: (MonadUnliftIO m, MonadReader ChatController m) => ByteString -> m ChatResponse execChatCommand s = case parseChatCommand s of - Left e -> pure $ chatCmdError e - Right cmd -> either CRChatCmdError id <$> runExceptT (processChatCommand cmd) + Left e -> do + u <- readTVarIO =<< asks currentUser + pure $ chatCmdError u e + Right cmd -> either (CRChatCmdError Nothing) id <$> runExceptT (processChatCommand cmd) parseChatCommand :: ByteString -> Either String ChatCommand parseChatCommand = A.parseOnly chatCommandP . B.dropWhileEnd isSpace @@ -253,10 +256,24 @@ processChatCommand = \case ShowActiveUser -> withUser' $ pure . CRActiveUser CreateActiveUser p -> do u <- asks currentUser - whenM (isJust <$> readTVarIO u) $ throwChatError CEActiveUserExists user <- withStore $ \db -> createUser db p True atomically . writeTVar u $ Just user pure $ CRActiveUser user + ListUsers -> do + users <- withStore' $ \db -> getUsers db + pure $ CRUsersList users + APISetActiveUser userId -> do + u <- asks currentUser + user <- withStore $ \db -> getSetActiveUser db userId + atomically . writeTVar u $ Just user + pure $ CRActiveUser user + SetActiveUser uName -> withUserName uName APISetActiveUser + APIDeleteUser _userId -> do + -- prohibit to delete active user + -- withStore' $ \db -> deleteUser db userId + -- ? other cleanup + pure $ CRCmdOk Nothing + DeleteUser uName -> withUserName uName APIDeleteUser StartChat subConns enableExpireCIs -> withUser' $ \user -> asks agentAsync >>= readTVarIO >>= \case Just _ -> pure CRChatRunning @@ -264,48 +281,54 @@ processChatCommand = \case APIStopChat -> do ask >>= stopChatController pure CRChatStopped - APIActivateChat -> do - withUser $ \user -> restoreCalls user + APIActivateChat -> withUser $ \user -> do + restoreCalls user withAgent activateAgent setExpireCIs True - pure CRCmdOk + pure $ CRCmdOk Nothing APISuspendChat t -> do setExpireCIs False withAgent (`suspendAgent` t) - pure CRCmdOk - ResubscribeAllConnections -> withUser (subscribeUserConnections Agent.resubscribeConnections) $> CRCmdOk + pure $ CRCmdOk Nothing + ResubscribeAllConnections -> withUser $ \user -> do + subscribeUserConnections Agent.resubscribeConnections user + pure $ CRCmdOk Nothing SetFilesFolder filesFolder' -> do createDirectoryIfMissing True filesFolder' ff <- asks filesFolder atomically . writeTVar ff $ Just filesFolder' - pure CRCmdOk + pure $ CRCmdOk Nothing SetIncognito onOff -> do incognito <- asks incognitoMode atomically . writeTVar incognito $ onOff - pure CRCmdOk - APIExportArchive cfg -> checkChatStopped $ exportArchive cfg $> CRCmdOk + pure $ CRCmdOk Nothing + APIExportArchive cfg -> checkChatStopped $ exportArchive cfg $> CRCmdOk Nothing APIImportArchive cfg -> withStoreChanged $ importArchive cfg APIDeleteStorage -> withStoreChanged deleteStorage APIStorageEncryption cfg -> withStoreChanged $ sqlCipherExport cfg ExecChatStoreSQL query -> CRSQLResult <$> withStore' (`execSQL` query) ExecAgentStoreSQL query -> CRSQLResult <$> withAgent (`execAgentStoreSQL` query) - APIGetChats withPCC -> CRApiChats <$> withUser' (\user -> withStore' $ \db -> getChatPreviews db user withPCC) + APIGetChats withPCC -> withUser' $ \user -> do + chats <- withStore' $ \db -> getChatPreviews db user withPCC + pure $ CRApiChats user chats APIGetChat (ChatRef cType cId) pagination search -> withUser $ \user -> case cType of -- TODO optimize queries calculating ChatStats, currently they're disabled CTDirect -> do directChat <- withStore (\db -> getDirectChat db user cId pagination search) - pure . CRApiChat $ AChat SCTDirect directChat - CTGroup -> CRApiChat . AChat SCTGroup <$> withStore (\db -> getGroupChat db user cId pagination search) - CTContactRequest -> pure $ chatCmdError "not implemented" - CTContactConnection -> pure $ chatCmdError "not supported" - APIGetChatItems _pagination -> pure $ chatCmdError "not implemented" + pure $ CRApiChat user (AChat SCTDirect directChat) + CTGroup -> do + groupChat <- withStore (\db -> getGroupChat db user cId pagination search) + pure $ CRApiChat user (AChat SCTGroup groupChat) + CTContactRequest -> pure $ chatCmdError (Just user) "not implemented" + CTContactConnection -> pure $ chatCmdError (Just user) "not supported" + APIGetChatItems _pagination -> pure $ chatCmdError Nothing "not implemented" APISendMessage (ChatRef cType chatId) live (ComposedMessage file_ quotedItemId_ mc) -> withUser $ \user@User {userId} -> withChatLock "sendMessage" $ case cType of CTDirect -> do ct@Contact {contactId, localDisplayName = c, contactUsed} <- withStore $ \db -> getContact db user chatId assertDirectAllowed user MDSnd ct XMsgNew_ unless contactUsed $ withStore' $ \db -> updateContactUsed db user ct if isVoice mc && not (featureAllowed SCFVoice forUser ct) - then pure $ chatCmdError $ "feature not allowed " <> T.unpack (chatFeatureNameText CFVoice) + then pure $ chatCmdError (Just user) ("feature not allowed " <> T.unpack (chatFeatureNameText CFVoice)) else do (fileInvitation_, ciFile_, ft_) <- unzipMaybe3 <$> setupSndFileTransfer ct timed_ <- sndContactCITimed live ct @@ -319,7 +342,7 @@ processChatCommand = \case forM_ (timed_ >>= deleteAt) $ startProximateTimedItemThread user (ChatRef CTDirect contactId, chatItemId' ci) setActive $ ActiveC c - pure . CRNewChatItem $ AChatItem SCTDirect SMDSnd (DirectChat ct) ci + pure $ CRNewChatItem user (AChatItem SCTDirect SMDSnd (DirectChat ct) ci) where setupSndFileTransfer :: Contact -> m (Maybe (FileInvitation, CIFile 'MDSnd, FileTransferMeta)) setupSndFileTransfer ct = forM file_ $ \file -> do @@ -358,7 +381,7 @@ processChatCommand = \case Group gInfo@GroupInfo {groupId, membership, localDisplayName = gName} ms <- withStore $ \db -> getGroup db user chatId unless (memberActive membership) $ throwChatError CEGroupMemberUserRemoved if isVoice mc && not (groupFeatureAllowed SGFVoice gInfo) - then pure $ chatCmdError $ "feature not allowed " <> T.unpack (groupFeatureNameText GFVoice) + then pure $ chatCmdError (Just user) ("feature not allowed " <> T.unpack (groupFeatureNameText GFVoice)) else do (fileInvitation_, ciFile_, ft_) <- unzipMaybe3 <$> setupSndFileTransfer gInfo (length $ filter memberCurrent ms) timed_ <- sndGroupCITimed live gInfo @@ -369,7 +392,7 @@ processChatCommand = \case forM_ (timed_ >>= deleteAt) $ startProximateTimedItemThread user (ChatRef CTGroup groupId, chatItemId' ci) setActive $ ActiveG gName - pure . CRNewChatItem $ AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci + pure $ CRNewChatItem user (AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci) where setupSndFileTransfer :: GroupInfo -> Int -> m (Maybe (FileInvitation, CIFile 'MDSnd, FileTransferMeta)) setupSndFileTransfer gInfo n = forM file_ $ \file -> do @@ -406,8 +429,8 @@ processChatCommand = \case quoteData ChatItem {chatDir = CIGroupSnd, content = CISndMsgContent qmc} membership' = pure (qmc, CIQGroupSnd, True, membership') quoteData ChatItem {chatDir = CIGroupRcv m, content = CIRcvMsgContent qmc} _ = pure (qmc, CIQGroupRcv $ Just m, False, m) quoteData _ _ = throwChatError CEInvalidQuote - CTContactRequest -> pure $ chatCmdError "not supported" - CTContactConnection -> pure $ chatCmdError "not supported" + CTContactRequest -> pure $ chatCmdError (Just user) "not supported" + CTContactConnection -> pure $ chatCmdError (Just user) "not supported" where quoteContent :: forall d. MsgContent -> Maybe (CIFile d) -> MsgContent quoteContent qmc ciFile_ @@ -446,7 +469,7 @@ processChatCommand = \case ci' <- withStore' $ \db -> updateDirectChatItem' db user contactId ci (CISndMsgContent mc) live $ Just msgId startUpdatedTimedItemThread user (ChatRef CTDirect contactId) ci ci' setActive $ ActiveC c - pure . CRChatItemUpdated $ AChatItem SCTDirect SMDSnd (DirectChat ct) ci' + pure $ CRChatItemUpdated user (AChatItem SCTDirect SMDSnd (DirectChat ct) ci') _ -> throwChatError CEInvalidChatItemUpdate CChatItem SMDRcv _ -> throwChatError CEInvalidChatItemUpdate CTGroup -> do @@ -461,11 +484,11 @@ processChatCommand = \case ci' <- withStore' $ \db -> updateGroupChatItem db user groupId ci (CISndMsgContent mc) live $ Just msgId startUpdatedTimedItemThread user (ChatRef CTGroup groupId) ci ci' setActive $ ActiveG gName - pure . CRChatItemUpdated $ AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci' + pure $ CRChatItemUpdated user (AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci') _ -> throwChatError CEInvalidChatItemUpdate CChatItem SMDRcv _ -> throwChatError CEInvalidChatItemUpdate - CTContactRequest -> pure $ chatCmdError "not supported" - CTContactConnection -> pure $ chatCmdError "not supported" + CTContactRequest -> pure $ chatCmdError (Just user) "not supported" + CTContactConnection -> pure $ chatCmdError (Just user) "not supported" APIDeleteChatItem (ChatRef cType chatId) itemId mode -> withUser $ \user -> withChatLock "deleteChatItem" $ case cType of CTDirect -> do (ct@Contact {localDisplayName = c}, ci@(CChatItem msgDir ChatItem {meta = CIMeta {itemSharedMsgId}})) <- withStore $ \db -> (,) <$> getContact db user chatId <*> getDirectChatItem db user chatId itemId @@ -492,8 +515,8 @@ processChatCommand = \case then deleteGroupCI user gInfo ci True False else markGroupCIDeleted user gInfo ci msgId True (CIDMBroadcast, _, _) -> throwChatError CEInvalidChatItemDelete - CTContactRequest -> pure $ chatCmdError "not supported" - CTContactConnection -> pure $ chatCmdError "not supported" + CTContactRequest -> pure $ chatCmdError (Just user) "not supported" + CTContactConnection -> pure $ chatCmdError (Just user) "not supported" APIChatRead (ChatRef cType chatId) fromToIds -> withUser $ \user@User {userId} -> case cType of CTDirect -> do timedItems <- withStore' $ \db -> getDirectUnreadTimedItems db user chatId fromToIds @@ -503,7 +526,7 @@ processChatCommand = \case withStore' $ \db -> setDirectChatItemDeleteAt db user chatId itemId deleteAt startProximateTimedItemThread user (ChatRef CTDirect chatId, itemId) deleteAt withStore' $ \db -> updateDirectChatItemsRead db user chatId fromToIds - pure CRCmdOk + pure $ CRCmdOk (Just user) CTGroup -> do timedItems <- withStore' $ \db -> getGroupUnreadTimedItems db user chatId fromToIds ts <- liftIO getCurrentTime @@ -512,21 +535,21 @@ processChatCommand = \case withStore' $ \db -> setGroupChatItemDeleteAt db user chatId itemId deleteAt startProximateTimedItemThread user (ChatRef CTGroup chatId, itemId) deleteAt withStore' $ \db -> updateGroupChatItemsRead db userId chatId fromToIds - pure CRCmdOk - CTContactRequest -> pure $ chatCmdError "not supported" - CTContactConnection -> pure $ chatCmdError "not supported" + pure $ CRCmdOk (Just user) + CTContactRequest -> pure $ chatCmdError (Just user) "not supported" + CTContactConnection -> pure $ chatCmdError (Just user) "not supported" APIChatUnread (ChatRef cType chatId) unreadChat -> withUser $ \user -> case cType of CTDirect -> do withStore $ \db -> do ct <- getContact db user chatId liftIO $ updateContactUnreadChat db user ct unreadChat - pure CRCmdOk + pure $ CRCmdOk (Just user) CTGroup -> do withStore $ \db -> do Group {groupInfo} <- getGroup db user chatId liftIO $ updateGroupUnreadChat db user groupInfo unreadChat - pure CRCmdOk - _ -> pure $ chatCmdError "not supported" + pure $ CRCmdOk (Just user) + _ -> pure $ chatCmdError (Just user) "not supported" APIDeleteChat (ChatRef cType chatId) -> withUser $ \user@User {userId} -> case cType of CTDirect -> do ct@Contact {localDisplayName} <- withStore $ \db -> getContact db user chatId @@ -540,12 +563,12 @@ processChatCommand = \case withStore' $ \db -> deleteContactConnectionsAndFiles db userId ct withStore' $ \db -> deleteContact db user ct unsetActive $ ActiveC localDisplayName - pure $ CRContactDeleted ct + pure $ CRContactDeleted user ct CTContactConnection -> withChatLock "deleteChat contactConnection" . procCmd $ do conn@PendingContactConnection {pccConnId, pccAgentConnId} <- withStore $ \db -> getPendingContactConnection db userId chatId deleteAgentConnectionAsync' user pccConnId pccAgentConnId withStore' $ \db -> deletePendingContactConnection db userId chatId - pure $ CRContactConnectionDeleted conn + pure $ CRContactConnectionDeleted user conn CTGroup -> do Group gInfo@GroupInfo {membership} members <- withStore $ \db -> getGroup db user chatId let canDelete = memberRole (membership :: GroupMember) == GROwner || not (memberCurrent membership) @@ -563,8 +586,8 @@ processChatCommand = \case withStore' $ \db -> deleteGroup db user gInfo let contactIds = mapMaybe memberContactId members forM_ contactIds $ \ctId -> - deleteUnusedContact ctId `catchError` (toView . CRChatError) - pure $ CRGroupDeletedUser gInfo + deleteUnusedContact ctId `catchError` (toView . CRChatError (Just user)) + pure $ CRGroupDeletedUser user gInfo where deleteUnusedContact contactId = do ct <- withStore $ \db -> getContact db user contactId @@ -574,7 +597,7 @@ processChatCommand = \case conns <- withStore $ \db -> getContactConnections db userId ct forM_ conns $ \conn -> deleteAgentConnectionAsync user conn `catchError` \_ -> pure () withStore' $ \db -> deleteContactWithoutGroups db user ct - CTContactRequest -> pure $ chatCmdError "not supported" + CTContactRequest -> pure $ chatCmdError (Just user) "not supported" APIClearChat (ChatRef cType chatId) -> withUser $ \user -> case cType of CTDirect -> do ct <- withStore $ \db -> getContact db user chatId @@ -589,7 +612,7 @@ processChatCommand = \case withStore' $ \db -> updateContactTs db user ct ts pure (ct :: Contact) {updatedAt = ts} _ -> pure ct - pure $ CRChatCleared (AChatInfo SCTDirect (DirectChat ct')) + pure $ CRChatCleared user (AChatInfo SCTDirect (DirectChat ct')) CTGroup -> do gInfo <- withStore $ \db -> getGroupInfo db user chatId filesInfo <- withStore' $ \db -> getGroupFileInfo db user gInfo @@ -605,23 +628,23 @@ processChatCommand = \case withStore' $ \db -> updateGroupTs db user gInfo ts pure (gInfo :: GroupInfo) {updatedAt = ts} _ -> pure gInfo - pure $ CRChatCleared (AChatInfo SCTGroup (GroupChat gInfo')) - CTContactConnection -> pure $ chatCmdError "not supported" - CTContactRequest -> pure $ chatCmdError "not supported" + pure $ CRChatCleared user (AChatInfo SCTGroup (GroupChat gInfo')) + CTContactConnection -> pure $ chatCmdError (Just user) "not supported" + CTContactRequest -> pure $ chatCmdError (Just user) "not supported" APIAcceptContact connReqId -> withUser $ \user@User {userId} -> withChatLock "acceptContact" $ do cReq <- withStore $ \db -> getContactRequest db userId connReqId -- [incognito] generate profile to send, create connection with incognito profile incognito <- readTVarIO =<< asks incognitoMode incognitoProfile <- if incognito then Just . NewIncognito <$> liftIO generateRandomProfile else pure Nothing ct <- acceptContactRequest user cReq incognitoProfile - pure $ CRAcceptingContactRequest ct - APIRejectContact connReqId -> withUser $ \User {userId} -> withChatLock "rejectContact" $ do + pure $ CRAcceptingContactRequest user ct + APIRejectContact connReqId -> withUser $ \user@User {userId} -> withChatLock "rejectContact" $ do cReq@UserContactRequest {agentContactConnId = AgentConnId connId, agentInvitationId = AgentInvId invId} <- withStore $ \db -> getContactRequest db userId connReqId `E.finally` liftIO (deleteContactRequest db userId connReqId) withAgent $ \a -> rejectContact a connId invId - pure $ CRContactRequestRejected cReq + pure $ CRContactRequestRejected user cReq APISendCallInvitation contactId callType -> withUser $ \user -> do -- party initiating call ct <- withStore $ \db -> getContact db user contactId @@ -637,8 +660,8 @@ processChatCommand = \case let call' = Call {contactId, callId, chatItemId = chatItemId' ci, callState, callTs = chatItemTs' ci} call_ <- atomically $ TM.lookupInsert contactId call' calls forM_ call_ $ \call -> updateCallItemStatus user ct call WCSDisconnected Nothing - toView . CRNewChatItem $ AChatItem SCTDirect SMDSnd (DirectChat ct) ci - pure CRCmdOk + toView $ CRNewChatItem user (AChatItem SCTDirect SMDSnd (DirectChat ct) ci) + pure $ CRCmdOk (Just user) SendCallInvitation cName callType -> withUser $ \user -> do contactId <- withStore $ \db -> getContactIdByName db user cName processChatCommand $ APISendCallInvitation contactId callType @@ -696,7 +719,8 @@ processChatCommand = \case APIGetCallInvitations -> withUser $ \user -> do calls <- asks currentCalls >>= readTVarIO let invs = mapMaybe callInvitation $ M.elems calls - CRCallInvitations <$> mapM (rcvCallInvitation user) invs + rcvCallInvitations <- mapM (rcvCallInvitation user) invs + pure $ CRCallInvitations user rcvCallInvitations where callInvitation Call {contactId, callState, callTs} = case callState of CallInvitationReceived {peerCallType, sharedKey} -> Just (contactId, callTs, peerCallType, sharedKey) @@ -715,35 +739,37 @@ processChatCommand = \case ct' <- withStore $ \db -> do ct <- getContact db user contactId liftIO $ updateContactAlias db userId ct localAlias - pure $ CRContactAliasUpdated ct' - APISetConnectionAlias connId localAlias -> withUser $ \User {userId} -> do + pure $ CRContactAliasUpdated user ct' + APISetConnectionAlias connId localAlias -> withUser $ \user@User {userId} -> do conn' <- withStore $ \db -> do conn <- getPendingContactConnection db userId connId liftIO $ updateContactConnectionAlias db userId conn localAlias - pure $ CRConnectionAliasUpdated conn' + pure $ CRConnectionAliasUpdated user conn' APIParseMarkdown text -> pure . CRApiParsedMarkdown $ parseMaybeMarkdownList text APIGetNtfToken -> withUser $ \_ -> crNtfToken <$> withAgent getNtfToken - APIRegisterToken token mode -> CRNtfTokenStatus <$> withUser (\_ -> withAgent $ \a -> registerNtfToken a token mode) - APIVerifyToken token nonce code -> withUser $ \_ -> withAgent (\a -> verifyNtfToken a token nonce code) $> CRCmdOk - APIDeleteToken token -> withUser $ \_ -> withAgent (`deleteNtfToken` token) $> CRCmdOk + APIRegisterToken token mode -> withUser $ \_ -> do + tokenStatus <- withAgent $ \a -> registerNtfToken a token mode + pure $ CRNtfTokenStatus tokenStatus + APIVerifyToken token nonce code -> withUser $ \_ -> withAgent (\a -> verifyNtfToken a token nonce code) $> CRCmdOk Nothing + APIDeleteToken token -> withUser $ \_ -> withAgent (`deleteNtfToken` token) $> CRCmdOk Nothing APIGetNtfMessage nonce encNtfInfo -> withUser $ \user -> do (NotificationInfo {ntfConnId, ntfMsgMeta}, msgs) <- withAgent $ \a -> getNotificationMessage a nonce encNtfInfo let ntfMessages = map (\SMP.SMPMsgMeta {msgTs, msgFlags} -> NtfMsgInfo {msgTs = systemToUTCTime msgTs, msgFlags}) msgs msgTs' = systemToUTCTime . (SMP.msgTs :: SMP.NMsgMeta -> SystemTime) <$> ntfMsgMeta connEntity <- withStore (\db -> Just <$> getConnectionEntity db user (AgentConnId ntfConnId)) `catchError` \_ -> pure Nothing - pure CRNtfMessages {connEntity, msgTs = msgTs', ntfMessages} - GetUserSMPServers -> do + pure CRNtfMessages {user, connEntity, msgTs = msgTs', ntfMessages} + GetUserSMPServers -> withUser $ \user -> do ChatConfig {defaultServers = InitialAgentServers {smp = defaultSMPServers}} <- asks config - smpServers <- withUser (\user -> withStore' (`getSMPServers` user)) + smpServers <- withStore' (`getSMPServers` user) let smpServers' = fromMaybe (L.map toServerCfg defaultSMPServers) $ nonEmpty smpServers - pure $ CRUserSMPServers smpServers' defaultSMPServers + pure $ CRUserSMPServers user smpServers' defaultSMPServers where toServerCfg server = ServerCfg {server, preset = True, tested = Nothing, enabled = True} SetUserSMPServers (SMPServersConfig smpServers) -> withUser $ \user -> withChatLock "setUserSMPServers" $ do withStore $ \db -> overwriteSMPServers db user smpServers cfg <- asks config withAgent $ \a -> setSMPServers a $ activeAgentServers cfg smpServers - pure CRCmdOk + pure $ CRCmdOk (Just user) TestSMPServer smpServer -> CRSmpTestResult <$> withAgent (`testSMPServerConnection` smpServer) APISetChatItemTTL newTTL_ -> withUser' $ \user -> checkStoreNotChanged $ @@ -759,10 +785,14 @@ processChatCommand = \case expireChatItems user newTTL True withStore' $ \db -> setChatItemTTL db user newTTL_ whenM chatStarted $ setExpireCIs True - pure CRCmdOk - APIGetChatItemTTL -> CRChatItemTTL <$> withUser (\user -> withStore' (`getChatItemTTL` user)) - APISetNetworkConfig cfg -> withUser' $ \_ -> withAgent (`setNetworkConfig` cfg) $> CRCmdOk - APIGetNetworkConfig -> CRNetworkConfig <$> withUser' (\_ -> withAgent getNetworkConfig) + pure $ CRCmdOk (Just user) + APIGetChatItemTTL -> withUser $ \user -> do + ttl <- withStore' (`getChatItemTTL` user) + pure $ CRChatItemTTL user ttl + APISetNetworkConfig cfg -> withUser' $ \_ -> withAgent (`setNetworkConfig` cfg) $> CRCmdOk Nothing + APIGetNetworkConfig -> withUser' $ \_ -> do + networkConfig <- withAgent getNetworkConfig + pure $ CRNetworkConfig networkConfig APISetChatSettings (ChatRef cType chatId) chatSettings -> withUser $ \user -> case cType of CTDirect -> do ct <- withStore $ \db -> do @@ -770,34 +800,34 @@ processChatCommand = \case liftIO $ updateContactSettings db user chatId chatSettings pure ct withAgent $ \a -> toggleConnectionNtfs a (contactConnId ct) (enableNtfs chatSettings) - pure CRCmdOk + pure $ CRCmdOk (Just user) CTGroup -> do ms <- withStore $ \db -> do Group _ ms <- getGroup db user chatId liftIO $ updateGroupSettings db user chatId chatSettings pure ms forM_ (filter memberActive ms) $ \m -> forM_ (memberConnId m) $ \connId -> - withAgent (\a -> toggleConnectionNtfs a connId $ enableNtfs chatSettings) `catchError` (toView . CRChatError) - pure CRCmdOk - _ -> pure $ chatCmdError "not supported" + withAgent (\a -> toggleConnectionNtfs a connId $ enableNtfs chatSettings) `catchError` (toView . CRChatError (Just user)) + pure $ CRCmdOk (Just user) + _ -> pure $ chatCmdError (Just user) "not supported" APIContactInfo contactId -> withUser $ \user@User {userId} -> do -- [incognito] print user's incognito profile for this contact ct@Contact {activeConn = Connection {customUserProfileId}} <- withStore $ \db -> getContact db user contactId incognitoProfile <- forM customUserProfileId $ \profileId -> withStore (\db -> getProfileById db userId profileId) connectionStats <- withAgent (`getConnectionServers` contactConnId ct) - pure $ CRContactInfo ct connectionStats (fmap fromLocalProfile incognitoProfile) + pure $ CRContactInfo user ct connectionStats (fmap fromLocalProfile incognitoProfile) APIGroupMemberInfo gId gMemberId -> withUser $ \user -> do (g, m) <- withStore $ \db -> (,) <$> getGroupInfo db user gId <*> getGroupMember db user gId gMemberId connectionStats <- mapM (withAgent . flip getConnectionServers) (memberConnId m) - pure $ CRGroupMemberInfo g m connectionStats + pure $ CRGroupMemberInfo user g m connectionStats APISwitchContact contactId -> withUser $ \user -> do ct <- withStore $ \db -> getContact db user contactId withAgent $ \a -> switchConnectionAsync a "" $ contactConnId ct - pure CRCmdOk + pure $ CRCmdOk (Just user) APISwitchGroupMember gId gMemberId -> withUser $ \user -> do m <- withStore $ \db -> getGroupMember db user gId gMemberId case memberConnId m of - Just connId -> withAgent (\a -> switchConnectionAsync a "" connId) $> CRCmdOk + Just connId -> withAgent (\a -> switchConnectionAsync a "" connId) $> CRCmdOk (Just user) _ -> throwChatError CEGroupMemberNotActive APIGetContactCode contactId -> withUser $ \user -> do ct@Contact {activeConn = conn@Connection {connId}} <- withStore $ \db -> getContact db user contactId @@ -809,7 +839,7 @@ processChatCommand = \case withStore' $ \db -> setConnectionVerified db user connId Nothing pure (ct :: Contact) {activeConn = conn {connectionCode = Nothing}} _ -> pure ct - pure $ CRContactCode ct' code + pure $ CRContactCode user ct' code APIGetGroupMemberCode gId gMemberId -> withUser $ \user -> do (g, m@GroupMember {activeConn}) <- withStore $ \db -> (,) <$> getGroupInfo db user gId <*> getGroupMember db user gId gMemberId case activeConn of @@ -822,7 +852,7 @@ processChatCommand = \case withStore' $ \db -> setConnectionVerified db user connId Nothing pure (m :: GroupMember) {activeConn = Just $ (conn :: Connection) {connectionCode = Nothing}} _ -> pure m - pure $ CRGroupMemberCode g m' code + pure $ CRGroupMemberCode user g m' code _ -> throwChatError CEGroupMemberNotActive APIVerifyContact contactId code -> withUser $ \user -> do Contact {activeConn} <- withStore $ \db -> getContact db user contactId @@ -848,14 +878,14 @@ processChatCommand = \case VerifyGroupMember gName mName code -> withMemberName gName mName $ \gId mId -> APIVerifyGroupMember gId mId code ChatHelp section -> pure $ CRChatHelp section Welcome -> withUser $ pure . CRWelcome - AddContact -> withUser $ \User {userId} -> withChatLock "addContact" . procCmd $ do + AddContact -> withUser $ \user@User {userId} -> withChatLock "addContact" . procCmd $ do -- [incognito] generate profile for connection incognito <- readTVarIO =<< asks incognitoMode incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing (connId, cReq) <- withAgent $ \a -> createConnection a True SCMInvitation Nothing conn <- withStore' $ \db -> createDirectConnection db userId connId cReq ConnNew incognitoProfile - toView $ CRNewContactConnection conn - pure $ CRInvitation cReq + toView $ CRNewContactConnection user conn + pure $ CRInvitation user cReq Connect (Just (ACR SCMInvitation cReq)) -> withUser $ \user@User {userId} -> withChatLock "connect" . procCmd $ do -- [incognito] generate profile to send incognito <- readTVarIO =<< asks incognitoMode @@ -863,8 +893,8 @@ processChatCommand = \case let profileToSend = userProfileToSend user incognitoProfile Nothing connId <- withAgent $ \a -> joinConnection a True cReq . directMessage $ XInfo profileToSend conn <- withStore' $ \db -> createDirectConnection db userId connId cReq ConnJoined $ incognitoProfile $> profileToSend - toView $ CRNewContactConnection conn - pure CRSentConfirmation + toView $ CRNewContactConnection user conn + pure $ CRSentConfirmation user Connect (Just (ACR SCMContact cReq)) -> withUser $ \user -> -- [incognito] generate profile to send connectViaContact user cReq @@ -874,21 +904,25 @@ processChatCommand = \case connectViaContact user adminContactReq DeleteContact cName -> withContactName cName $ APIDeleteChat . ChatRef CTDirect ClearContact cName -> withContactName cName $ APIClearChat . ChatRef CTDirect - ListContacts -> withUser $ \user -> CRContactsList <$> withStore' (`getUserContacts` user) - CreateMyAddress -> withUser $ \User {userId} -> withChatLock "createMyAddress" . procCmd $ do + ListContacts -> withUser $ \user -> do + contacts <- withStore' (`getUserContacts` user) + pure $ CRContactsList user contacts + CreateMyAddress -> withUser $ \user@User {userId} -> withChatLock "createMyAddress" . procCmd $ do (connId, cReq) <- withAgent $ \a -> createConnection a True SCMContact Nothing withStore $ \db -> createUserContactLink db userId connId cReq - pure $ CRUserContactLinkCreated cReq + pure $ CRUserContactLinkCreated user cReq DeleteMyAddress -> withUser $ \user -> withChatLock "deleteMyAddress" $ do conns <- withStore (`getUserAddressConnections` user) procCmd $ do forM_ conns $ \conn -> deleteAgentConnectionAsync user conn `catchError` \_ -> pure () withStore' (`deleteUserAddress` user) - pure CRUserContactLinkDeleted - ShowMyAddress -> withUser $ \User {userId} -> - CRUserContactLink <$> withStore (`getUserAddress` userId) - AddressAutoAccept autoAccept_ -> withUser $ \User {userId} -> do - CRUserContactLinkUpdated <$> withStore (\db -> updateUserAddressAutoAccept db userId autoAccept_) + pure $ CRUserContactLinkDeleted user + ShowMyAddress -> withUser $ \user@User {userId} -> do + contactLink <- withStore (`getUserAddress` userId) + pure $ CRUserContactLink user contactLink + AddressAutoAccept autoAccept_ -> withUser $ \user@User {userId} -> do + contactLink <- withStore (\db -> updateUserAddressAutoAccept db userId autoAccept_) + pure $ CRUserContactLinkUpdated user contactLink AcceptContact cName -> withUser $ \User {userId} -> do connReqId <- withStore $ \db -> getContactRequestIdByName db userId cName processChatCommand $ APIAcceptContact connReqId @@ -908,8 +942,8 @@ processChatCommand = \case (sndMsg, _) <- sendDirectContactMessage ct (XMsgNew $ MCSimple (extMsgContent mc Nothing)) saveSndChatItem user (CDDirectSnd ct) sndMsg (CISndMsgContent mc) ) - `catchError` (toView . CRChatError) - CRBroadcastSent mc (length cts) <$> liftIO getZonedTime + `catchError` (toView . CRChatError (Just user)) + CRBroadcastSent user mc (length cts) <$> liftIO getZonedTime SendMessageQuote cName (AMsgDirection msgDir) quotedMsg msg -> withUser $ \user@User {userId} -> do contactId <- withStore $ \db -> getContactIdByName db user cName quotedItemId <- withStore $ \db -> getDirectChatItemIdByText db userId contactId msgDir (safeDecodeUtf8 quotedMsg) @@ -931,7 +965,7 @@ processChatCommand = \case NewGroup gProfile -> withUser $ \user -> do gVar <- asks idsDrg groupInfo <- withStore (\db -> createNewGroup db gVar user gProfile) - pure $ CRGroupCreated groupInfo + pure $ CRGroupCreated user groupInfo APIAddMember groupId contactId memRole -> withUser $ \user -> withChatLock "addMember" $ do -- TODO for large groups: no need to load all members to determine if contact is a member (group, contact) <- withStore $ \db -> (,) <$> getGroup db user groupId <*> getContact db user contactId @@ -953,12 +987,14 @@ processChatCommand = \case (agentConnId, cReq) <- withAgent $ \a -> createConnection a True SCMInvitation Nothing member <- withStore $ \db -> createNewContactMember db gVar user groupId contact memRole agentConnId cReq sendInvitation member cReq - pure $ CRSentGroupInvitation gInfo contact member + pure $ CRSentGroupInvitation user gInfo contact member Just member@GroupMember {groupMemberId, memberStatus, memberRole = mRole} | memberStatus == GSMemInvited -> do unless (mRole == memRole) $ withStore' $ \db -> updateGroupMemberRole db user member memRole withStore' (\db -> getMemberInvitation db user groupMemberId) >>= \case - Just cReq -> sendInvitation member {memberRole = memRole} cReq $> CRSentGroupInvitation gInfo contact member {memberRole = memRole} + Just cReq -> do + sendInvitation member {memberRole = memRole} cReq + pure $ CRSentGroupInvitation user gInfo contact member {memberRole = memRole} Nothing -> throwChatError $ CEGroupCantResendInvitation gInfo cName | otherwise -> throwChatError $ CEGroupDuplicateMember cName APIJoinGroup groupId -> withUser $ \user@User {userId} -> do @@ -970,7 +1006,7 @@ processChatCommand = \case updateGroupMemberStatus db userId fromMember GSMemAccepted updateGroupMemberStatus db userId membership GSMemAccepted updateCIGroupInvitationStatus user - pure $ CRUserAcceptedGroupSent g {membership = membership {memberStatus = GSMemAccepted}} Nothing + pure $ CRUserAcceptedGroupSent user g {membership = membership {memberStatus = GSMemAccepted}} Nothing where updateCIGroupInvitationStatus user = do AChatItem _ _ cInfo ChatItem {content, meta = CIMeta {itemId}} <- withStore $ \db -> getChatItemByGroupId db user groupId @@ -1003,8 +1039,8 @@ processChatCommand = \case _ -> do msg <- sendGroupMessage gInfo members $ XGrpMemRole mId memRole ci <- saveSndChatItem user (CDGroupSnd gInfo) msg (CISndGroupEvent gEvent) - toView . CRNewChatItem $ AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci - pure CRMemberRoleUser {groupInfo = gInfo, member = m {memberRole = memRole}, fromRole = mRole, toRole = memRole} + toView $ CRNewChatItem user (AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci) + pure CRMemberRoleUser {user, groupInfo = gInfo, member = m {memberRole = memRole}, fromRole = mRole, toRole = memRole} APIRemoveMember groupId memberId -> withUser $ \user -> do Group gInfo@GroupInfo {membership} members <- withStore $ \db -> getGroup db user groupId case find ((== memberId) . groupMemberId') members of @@ -1021,24 +1057,26 @@ processChatCommand = \case _ -> do msg <- sendGroupMessage gInfo members $ XGrpMemDel mId ci <- saveSndChatItem user (CDGroupSnd gInfo) msg (CISndGroupEvent $ SGEMemberDeleted memberId (fromLocalProfile memberProfile)) - toView . CRNewChatItem $ AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci + toView $ CRNewChatItem user (AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci) deleteMemberConnection user m -- undeleted "member connected" chat item will prevent deletion of member record deleteOrUpdateMemberRecord user m - pure $ CRUserDeletedMember gInfo m {memberStatus = GSMemRemoved} + pure $ CRUserDeletedMember user gInfo m {memberStatus = GSMemRemoved} APILeaveGroup groupId -> withUser $ \user@User {userId} -> do Group gInfo@GroupInfo {membership} members <- withStore $ \db -> getGroup db user groupId withChatLock "leaveGroup" . procCmd $ do msg <- sendGroupMessage gInfo members XGrpLeave ci <- saveSndChatItem user (CDGroupSnd gInfo) msg (CISndGroupEvent SGEUserLeft) - toView . CRNewChatItem $ AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci + toView $ CRNewChatItem user (AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci) -- TODO delete direct connections that were unused deleteGroupLink' user gInfo `catchError` \_ -> pure () -- member records are not deleted to keep history forM_ members $ deleteMemberConnection user withStore' $ \db -> updateGroupMemberStatus db userId membership GSMemLeft - pure $ CRLeftMemberUser gInfo {membership = membership {memberStatus = GSMemLeft}} - APIListMembers groupId -> CRGroupMembers <$> withUser (\user -> withStore (\db -> getGroup db user groupId)) + pure $ CRLeftMemberUser user gInfo {membership = membership {memberStatus = GSMemLeft}} + APIListMembers groupId -> withUser $ \user -> do + group <- withStore $ \db -> getGroup db user groupId + pure $ CRGroupMembers user group AddMember gName cName memRole -> withUser $ \user -> do (groupId, contactId) <- withStore $ \db -> (,) <$> getGroupIdByName db user gName <*> getContactIdByName db user cName processChatCommand $ APIAddMember groupId contactId memRole @@ -1059,14 +1097,17 @@ processChatCommand = \case ListMembers gName -> withUser $ \user -> do groupId <- withStore $ \db -> getGroupIdByName db user gName processChatCommand $ APIListMembers groupId - ListGroups -> CRGroupsList <$> withUser (\user -> withStore' (`getUserGroupDetails` user)) + ListGroups -> withUser $ \user -> do + groups <- withStore' (`getUserGroupDetails` user) + pure $ CRGroupsList user groups APIUpdateGroupProfile groupId p' -> withUser $ \user -> do g <- withStore $ \db -> getGroup db user groupId runUpdateGroupProfile user g p' UpdateGroupNames gName GroupProfile {displayName, fullName} -> updateGroupProfileByName gName $ \p -> p {displayName, fullName} - ShowGroupProfile gName -> withUser $ \user -> - CRGroupProfile <$> withStore (\db -> getGroupInfoByName db user gName) + ShowGroupProfile gName -> withUser $ \user -> do + groupProfile <- withStore $ \db -> getGroupInfoByName db user gName + pure $ CRGroupProfile user groupProfile UpdateGroupDescription gName description -> updateGroupProfileByName gName $ \p -> p {description} APICreateGroupLink groupId -> withUser $ \user -> withChatLock "createGroupLink" $ do @@ -1078,14 +1119,15 @@ processChatCommand = \case let crClientData = encodeJSON $ CRDataGroup groupLinkId (connId, cReq) <- withAgent $ \a -> createConnection a True SCMContact $ Just crClientData withStore $ \db -> createGroupLink db user gInfo connId cReq groupLinkId - pure $ CRGroupLinkCreated gInfo cReq + pure $ CRGroupLinkCreated user gInfo cReq APIDeleteGroupLink groupId -> withUser $ \user -> withChatLock "deleteGroupLink" $ do gInfo <- withStore $ \db -> getGroupInfo db user groupId deleteGroupLink' user gInfo - pure $ CRGroupLinkDeleted gInfo + pure $ CRGroupLinkDeleted user gInfo APIGetGroupLink groupId -> withUser $ \user -> do gInfo <- withStore $ \db -> getGroupInfo db user groupId - CRGroupLink gInfo <$> withStore (\db -> getGroupLink db user gInfo) + groupLink <- withStore (\db -> getGroupLink db user gInfo) + pure $ CRGroupLink user gInfo groupLink CreateGroupLink gName -> withUser $ \user -> do groupId <- withStore $ \db -> getGroupIdByName db user gName processChatCommand $ APICreateGroupLink groupId @@ -1102,21 +1144,27 @@ processChatCommand = \case processChatCommand . APISendMessage (ChatRef CTGroup groupId) False $ ComposedMessage Nothing (Just quotedItemId) mc LastMessages (Just chatName) count search -> withUser $ \user -> do chatRef <- getChatRef user chatName - CRChatItems . aChatItems . chat <$> processChatCommand (APIGetChat chatRef (CPLast count) search) - LastMessages Nothing count search -> withUser $ \user -> withStore $ \db -> - CRChatItems <$> getAllChatItems db user (CPLast count) search + chatResp <- processChatCommand $ APIGetChat chatRef (CPLast count) search + pure $ CRChatItems user (aChatItems . chat $ chatResp) + LastMessages Nothing count search -> withUser $ \user -> do + chatItems <- withStore $ \db -> getAllChatItems db user (CPLast count) search + pure $ CRChatItems user chatItems LastChatItemId (Just chatName) index -> withUser $ \user -> do chatRef <- getChatRef user chatName - CRChatItemId . fmap aChatItemId . listToMaybe . aChatItems . chat <$> processChatCommand (APIGetChat chatRef (CPLast $ index + 1) Nothing) - LastChatItemId Nothing index -> withUser $ \user -> withStore $ \db -> - CRChatItemId . fmap aChatItemId . listToMaybe <$> getAllChatItems db user (CPLast $ index + 1) Nothing - ShowChatItem (Just itemId) -> withUser $ \user -> withStore $ \db -> - CRChatItems . (: []) <$> getAChatItem db user itemId - ShowChatItem Nothing -> withUser $ \user -> withStore $ \db -> - CRChatItems <$> getAllChatItems db user (CPLast 1) Nothing + chatResp <- processChatCommand (APIGetChat chatRef (CPLast $ index + 1) Nothing) + pure $ CRChatItemId user (fmap aChatItemId . listToMaybe . aChatItems . chat $ chatResp) + LastChatItemId Nothing index -> withUser $ \user -> do + chatItems <- withStore $ \db -> getAllChatItems db user (CPLast $ index + 1) Nothing + pure $ CRChatItemId user (fmap aChatItemId . listToMaybe $ chatItems) + ShowChatItem (Just itemId) -> withUser $ \user -> do + chatItem <- withStore $ \db -> getAChatItem db user itemId + pure $ CRChatItems user ((: []) chatItem) + ShowChatItem Nothing -> withUser $ \user -> do + chatItems <- withStore $ \db -> getAllChatItems db user (CPLast 1) Nothing + pure $ CRChatItems user chatItems ShowLiveItems on -> withUser $ \_ -> do asks showLiveItems >>= atomically . (`writeTVar` on) - pure CRCmdOk + pure $ CRCmdOk Nothing SendFile chatName f -> withUser $ \user -> do chatRef <- getChatRef user chatName processChatCommand . APISendMessage chatRef False $ ComposedMessage (Just f) Nothing (MCFile "") @@ -1132,12 +1180,12 @@ processChatCommand = \case ReceiveFile fileId rcvInline_ filePath_ -> withUser $ \user -> withChatLock "receiveFile" . procCmd $ do ft <- withStore $ \db -> getRcvFileTransfer db user fileId - (CRRcvFileAccepted <$> acceptFileReceive user ft rcvInline_ filePath_) `catchError` processError ft + (CRRcvFileAccepted user <$> acceptFileReceive user ft rcvInline_ filePath_) `catchError` processError user ft where - processError ft = \case + processError user ft = \case -- TODO AChatItem in Cancelled events - ChatErrorAgent (SMP SMP.AUTH) -> pure $ CRRcvFileAcceptedSndCancelled ft - ChatErrorAgent (CONN DUPLICATE) -> pure $ CRRcvFileAcceptedSndCancelled ft + ChatErrorAgent (SMP SMP.AUTH) -> pure $ CRRcvFileAcceptedSndCancelled user ft + ChatErrorAgent (CONN DUPLICATE) -> pure $ CRRcvFileAcceptedSndCancelled user ft e -> throwError e CancelFile fileId -> withUser $ \user@User {userId} -> withChatLock "cancelFile" . procCmd $ @@ -1155,13 +1203,14 @@ processChatCommand = \case void . sendGroupMessage gInfo ms $ XFileCancel sharedMsgId _ -> throwChatError $ CEFileInternal "invalid chat ref for file transfer" ci <- withStore $ \db -> getChatItemByFileId db user fileId - pure $ CRSndGroupFileCancelled ci ftm fts + pure $ CRSndGroupFileCancelled user ci ftm fts FTRcv ftr@RcvFileTransfer {cancelled} -> do unless cancelled $ cancelRcvFileTransfer user ftr - pure $ CRRcvFileCancelled ftr - FileStatus fileId -> - CRFileTransferStatus <$> withUser (\user -> withStore $ \db -> getFileTransferProgress db user fileId) - ShowProfile -> withUser $ \User {profile} -> pure $ CRUserProfile (fromLocalProfile profile) + pure $ CRRcvFileCancelled user ftr + FileStatus fileId -> withUser $ \user -> do + fileStatus <- withStore $ \db -> getFileTransferProgress db user fileId + pure $ CRFileTransferStatus user fileStatus + ShowProfile -> withUser $ \user@User {profile} -> pure $ CRUserProfile user (fromLocalProfile profile) UpdateProfile displayName fullName -> withUser $ \user@User {profile} -> do let p = (fromLocalProfile profile :: Profile) {displayName = displayName, fullName = fullName} updateProfile user p @@ -1203,7 +1252,7 @@ processChatCommand = \case where stat (AgentStatsKey {host, clientTs, cmd, res}, count) = map B.unpack [host, clientTs, cmd, res, bshow count] - ResetAgentStats -> CRCmdOk <$ withAgent resetAgentStats + ResetAgentStats -> withAgent resetAgentStats $> CRCmdOk Nothing where withChatLock name action = asks chatLock >>= \l -> withLock l name action -- below code would make command responses asynchronous where they can be slow @@ -1230,9 +1279,11 @@ processChatCommand = \case setStoreChanged :: m () setStoreChanged = asks chatStoreChanged >>= atomically . (`writeTVar` True) withStoreChanged :: m () -> m ChatResponse - withStoreChanged a = checkChatStopped $ a >> setStoreChanged $> CRCmdOk + withStoreChanged a = checkChatStopped $ a >> setStoreChanged $> CRCmdOk Nothing checkStoreNotChanged :: m ChatResponse -> m ChatResponse checkStoreNotChanged = ifM (asks chatStoreChanged >>= readTVarIO) (throwChatError CEChatStoreChanged) + withUserName :: UserName -> (UserId -> ChatCommand) -> m ChatResponse + withUserName uName cmd = withStore (`getUserIdByName` uName) >>= processChatCommand . cmd withContactName :: ContactName -> (ContactId -> ChatCommand) -> m ChatResponse withContactName cName cmd = withUser $ \user -> withStore (\db -> getContactIdByName db user cName) >>= processChatCommand . cmd @@ -1246,11 +1297,11 @@ processChatCommand = \case code' <- getConnectionCode $ aConnId conn let verified = sameVerificationCode code code' when verified . withStore' $ \db -> setConnectionVerified db user connId $ Just code' - pure $ CRConnectionVerified verified code' + pure $ CRConnectionVerified user verified code' verifyConnectionCode user conn@Connection {connId} _ = do code' <- getConnectionCode $ aConnId conn withStore' $ \db -> setConnectionVerified db user connId Nothing - pure $ CRConnectionVerified False code' + pure $ CRConnectionVerified user False code' getSentChatItemIdByText :: User -> ChatRef -> ByteString -> m Int64 getSentChatItemIdByText user@User {userId, localDisplayName} (ChatRef cType cId) msg = case cType of CTDirect -> withStore $ \db -> getDirectChatItemIdByText db userId cId SMDSnd (safeDecodeUtf8 msg) @@ -1260,7 +1311,7 @@ processChatCommand = \case connectViaContact user@User {userId} cReq@(CRContactUri ConnReqUriData {crClientData}) = withChatLock "connectViaContact" $ do let cReqHash = ConnReqUriHash . C.sha256Hash $ strEncode cReq withStore' (\db -> getConnReqContactXContactId db user cReqHash) >>= \case - (Just contact, _) -> pure $ CRContactAlreadyExists contact + (Just contact, _) -> pure $ CRContactAlreadyExists user contact (_, xContactId_) -> procCmd $ do let randomXContactId = XContactId <$> (asks idsDrg >>= liftIO . (`randomBytes` 16)) xContactId <- maybe randomXContactId pure xContactId_ @@ -1275,8 +1326,8 @@ processChatCommand = \case connId <- withAgent $ \a -> joinConnection a True cReq $ directMessage (XContact profileToSend $ Just xContactId) let groupLinkId = crClientData >>= decodeJSON >>= \(CRDataGroup gli) -> Just gli conn <- withStore' $ \db -> createConnReqConnection db userId connId cReqHash xContactId incognitoProfile groupLinkId - toView $ CRNewContactConnection conn - pure $ CRSentInvitation incognitoProfile + toView $ CRNewContactConnection user conn + pure $ CRSentInvitation user incognitoProfile contactMember :: Contact -> [GroupMember] -> Maybe GroupMember contactMember Contact {contactId} = find $ \GroupMember {memberContactId = cId, memberStatus = s} -> @@ -1295,7 +1346,7 @@ processChatCommand = \case | otherwise = Just IFMOffer updateProfile :: User -> Profile -> m ChatResponse updateProfile user@User {profile = p} p' - | p' == fromLocalProfile p = pure CRUserProfileNoChange + | p' == fromLocalProfile p = pure $ CRUserProfileNoChange user | otherwise = do -- read contacts before user update to correctly merge preferences -- [incognito] filter out contacts with whom user has incognito connections @@ -1310,12 +1361,12 @@ processChatCommand = \case ct' = updateMergedPreferences user' ct mergedProfile' = userProfileToSend user' Nothing $ Just ct' when (mergedProfile' /= mergedProfile) $ do - void (sendDirectContactMessage ct' $ XInfo mergedProfile') `catchError` (toView . CRChatError) + void (sendDirectContactMessage ct' $ XInfo mergedProfile') `catchError` (toView . CRChatError (Just user)) when (directOrUsed ct') $ createSndFeatureItems user' ct ct' - pure $ CRUserProfileUpdated (fromLocalProfile p) p' + pure $ CRUserProfileUpdated user' (fromLocalProfile p) p' updateContactPrefs :: User -> Contact -> Preferences -> m ChatResponse updateContactPrefs user@User {userId} ct@Contact {activeConn = Connection {customUserProfileId}, userPreferences = contactUserPrefs} contactUserPrefs' - | contactUserPrefs == contactUserPrefs' = pure $ CRContactPrefsUpdated ct ct + | contactUserPrefs == contactUserPrefs' = pure $ CRContactPrefsUpdated user ct ct | otherwise = do assertDirectAllowed user MDSnd ct XInfo_ ct' <- withStore' $ \db -> updateContactUserPreferences db user ct contactUserPrefs' @@ -1324,9 +1375,9 @@ processChatCommand = \case mergedProfile' = userProfileToSend user (fromLocalProfile <$> incognitoProfile) (Just ct') when (mergedProfile' /= mergedProfile) $ withChatLock "updateProfile" $ do - void (sendDirectContactMessage ct' $ XInfo mergedProfile') `catchError` (toView . CRChatError) + void (sendDirectContactMessage ct' $ XInfo mergedProfile') `catchError` (toView . CRChatError (Just user)) when (directOrUsed ct') $ createSndFeatureItems user ct ct' - pure $ CRContactPrefsUpdated ct ct' + pure $ CRContactPrefsUpdated user ct ct' runUpdateGroupProfile :: User -> Group -> GroupProfile -> m ChatResponse runUpdateGroupProfile user (Group g@GroupInfo {groupProfile = p} ms) p' = do let s = memberStatus $ membership g @@ -1339,9 +1390,9 @@ processChatCommand = \case let cd = CDGroupSnd g' unless (sameGroupProfileInfo p p') $ do ci <- saveSndChatItem user cd msg (CISndGroupEvent $ SGEGroupUpdated p') - toView . CRNewChatItem $ AChatItem SCTGroup SMDSnd (GroupChat g') ci + toView $ CRNewChatItem user (AChatItem SCTGroup SMDSnd (GroupChat g') ci) createGroupFeatureChangedItems user cd CISndGroupFeature g g' - pure $ CRGroupUpdated g g' Nothing + pure $ CRGroupUpdated user g g' Nothing updateGroupProfileByName :: GroupName -> (GroupProfile -> GroupProfile) -> m ChatResponse updateGroupProfileByName gName update = withUser $ \user -> do g@(Group GroupInfo {groupProfile = p} _) <- withStore $ \db -> @@ -1368,7 +1419,7 @@ processChatCommand = \case _ -> do withStore' $ \db -> deleteCalls db user ctId atomically $ TM.delete ctId calls - pure CRCmdOk + pure $ CRCmdOk (Just user) | otherwise -> throwChatError $ CECallContact contactId forwardFile :: ChatName -> FileTransferId -> (ChatName -> FilePath -> ChatCommand) -> m ChatResponse forwardFile chatName fileId sendCommand = withUser $ \user -> do @@ -1391,7 +1442,7 @@ processChatCommand = \case (msg, _) <- sendDirectContactMessage ct $ XGrpInv groupInv let content = CISndGroupInvitation (CIGroupInvitation {groupId, groupMemberId, localDisplayName, groupProfile, status = CIGISPending}) memRole ci <- saveSndChatItem user (CDDirectSnd ct) msg content - toView . CRNewChatItem $ AChatItem SCTDirect SMDSnd (DirectChat ct) ci + toView $ CRNewChatItem user (AChatItem SCTDirect SMDSnd (DirectChat ct) ci) setActive $ ActiveG localDisplayName sendTextMessage chatName msg live = withUser $ \user -> do chatRef <- getChatRef user chatName @@ -1430,7 +1481,7 @@ setExpireCIs b = do deleteFile :: forall m. ChatMonad m => User -> CIFileInfo -> m () deleteFile user CIFileInfo {filePath, fileId, fileStatus} = - (cancel' >> delete) `catchError` (toView . CRChatError) + (cancel' >> delete) `catchError` (toView . CRChatError (Just user)) where cancel' = forM_ fileStatus $ \(AFS dir status) -> unless (ciFileEnded status) $ @@ -1458,7 +1509,7 @@ updateCallItemStatus user ct Call {chatItemId} receivedStatus msgId_ = do updateDirectChatItemView :: ChatMonad m => User -> Contact -> ChatItemId -> ACIContent -> Bool -> Maybe MessageId -> m () updateDirectChatItemView user ct@Contact {contactId} chatItemId (ACIContent msgDir ciContent) live msgId_ = do ci' <- withStore $ \db -> updateDirectChatItem db user contactId chatItemId ciContent live msgId_ - toView . CRChatItemUpdated $ AChatItem SCTDirect msgDir (DirectChat ct) ci' + toView $ CRChatItemUpdated user (AChatItem SCTDirect msgDir (DirectChat ct) ci') callStatusItemContent :: ChatMonad m => User -> Contact -> ChatItemId -> WebRTCCallStatus -> m (Maybe ACIContent) callStatusItemContent user Contact {contactId} chatItemId receivedStatus = do @@ -1620,10 +1671,9 @@ agentSubscriber = do l <- asks chatLock forever $ do (corrId, connId, msg) <- atomically $ readTBQueue q - u <- readTVarIO =<< asks currentUser let name = "agentSubscriber connId=" <> str connId <> " corrId=" <> str corrId <> " msg=" <> str (aCommandTag msg) withLock l name . void . runExceptT $ - processAgentMessage u corrId connId msg `catchError` (toView . CRChatError) + processAgentMessage corrId connId msg `catchError` (toView . CRChatError Nothing) where str :: StrEncoding a => a -> String str = B.unpack . strEncode @@ -1744,7 +1794,7 @@ cleanupManagerInterval = 1800 -- 30 minutes cleanupManager :: forall m. ChatMonad m => User -> m () cleanupManager user = do forever $ do - flip catchError (toView . CRChatError) $ do + flip catchError (toView . CRChatError (Just user)) $ do waitChatStarted cleanupTimedItems threadDelay $ cleanupManagerInterval * 1000000 @@ -1788,7 +1838,7 @@ deleteTimedItem user (ChatRef cType chatId, itemId) deleteAt = do CTGroup -> do (gInfo, ci) <- withStore $ \db -> (,) <$> getGroupInfo db user chatId <*> getGroupChatItem db user chatId itemId deleteGroupCI user gInfo ci True True >>= toView - _ -> toView . CRChatError . ChatError $ CEInternalError "bad deleteTimedItem cType" + _ -> toView . CRChatError (Just user) . ChatError $ CEInternalError "bad deleteTimedItem cType" startUpdatedTimedItemThread :: ChatMonad m => User -> ChatRef -> ChatItem c d -> ChatItem c d -> m () startUpdatedTimedItemThread user chatRef ci ci' = @@ -1812,7 +1862,7 @@ expireChatItems user ttl sync = do loop :: TVar Bool -> [a] -> (a -> m ()) -> m () loop _ [] _ = pure () loop expire (a : as) process = continue expire $ do - process a `catchError` (toView . CRChatError) + process a `catchError` (toView . CRChatError (Just user)) loop expire as process continue :: TVar Bool -> m () -> m () continue expire = if sync then id else \a -> whenM (readTVarIO expire) $ threadDelay 100000 >> a @@ -1845,9 +1895,18 @@ expireChatItems user ttl sync = do (Just ts, Just count) -> when (count == 0) $ updateGroupTs db user gInfo ts _ -> pure () -processAgentMessage :: forall m. ChatMonad m => Maybe User -> ACorrId -> ConnId -> ACommand 'Agent -> m () -processAgentMessage Nothing _ _ _ = throwChatError CENoActiveUser -processAgentMessage (Just User {userId}) _ "" agentMessage = case agentMessage of +processAgentMessage :: forall m. ChatMonad m => ACorrId -> ConnId -> ACommand 'Agent -> m () +processAgentMessage _ "" msg = + asks currentUser >>= readTVarIO >>= \case + Just user -> processAgentMessageNoConn user msg `catchError` (toView . CRChatError (Just user)) + _ -> throwChatError CENoActiveUser +processAgentMessage corrId connId msg = + withStore' (`getUserByAConnId` AgentConnId connId) >>= \case + Just user -> processAgentMessageConn user corrId connId msg `catchError` (toView . CRChatError (Just user)) + _ -> throwChatError $ CENoConnectionUser (AgentConnId connId) + +processAgentMessageNoConn :: forall m. ChatMonad m => User -> ACommand 'Agent -> m () +processAgentMessageNoConn user@User {userId} = \case CONNECT p h -> hostEvent $ CRHostConnected p h DISCONNECT p h -> hostEvent $ CRHostDisconnected p h DOWN srv conns -> serverEvent srv conns CRContactsDisconnected "disconnected" @@ -1858,16 +1917,18 @@ processAgentMessage (Just User {userId}) _ "" agentMessage = case agentMessage o hostEvent = whenM (asks $ hostEvents . config) . toView serverEvent srv@(SMPServer host _ _) conns event str = do cs <- withStore' $ \db -> getConnectionsContacts db userId conns - toView $ event srv cs + toView $ event user srv cs showToast ("server " <> str) (safeDecodeUtf8 $ strEncode host) -processAgentMessage (Just user) _ agentConnId END = + +processAgentMessageConn :: forall m. ChatMonad m => User -> ACorrId -> ConnId -> ACommand 'Agent -> m () +processAgentMessageConn user _ agentConnId END = withStore (\db -> getConnectionEntity db user $ AgentConnId agentConnId) >>= \case RcvDirectMsgConnection _ (Just ct@Contact {localDisplayName = c}) -> do - toView $ CRContactAnotherClient ct + toView $ CRContactAnotherClient user ct showToast (c <> "> ") "connected to another client" unsetActive $ ActiveC c - entity -> toView $ CRSubscriptionEnd entity -processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage = + entity -> toView $ CRSubscriptionEnd user entity +processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = (withStore (\db -> getConnectionEntity db user $ AgentConnId agentConnId) >>= updateConnStatus) >>= \case RcvDirectMsgConnection conn contact_ -> processDirectMessage agentMessage conn contact_ @@ -1922,9 +1983,9 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage = -- [async agent commands] continuation on receiving OK withCompletedCommand conn agentMsg $ \CommandData {cmdFunction, cmdId} -> when (cmdFunction == CFAckMessage) $ ackMsgDeliveryEvent conn cmdId - MERR _ err -> toView . CRChatError $ ChatErrorAgent err -- ? updateDirectChatItemStatus + MERR _ err -> toView $ CRChatError (Just user) (ChatErrorAgent err) -- ? updateDirectChatItemStatus ERR err -> do - toView . CRChatError $ ChatErrorAgent err + toView $ CRChatError (Just user) (ChatErrorAgent err) when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () -- TODO add debugging output _ -> pure () @@ -1993,7 +2054,7 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage = Nothing -> do -- [incognito] print incognito profile used for this contact incognitoProfile <- forM customUserProfileId $ \profileId -> withStore (\db -> getProfileById db userId profileId) - toView $ CRContactConnected ct (fmap fromLocalProfile incognitoProfile) + toView $ CRContactConnected user ct (fmap fromLocalProfile incognitoProfile) when (directOrUsed ct) $ createFeatureEnabledItems ct setActive $ ActiveC c showToast (c <> "> ") "connected" @@ -2004,7 +2065,7 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage = forM_ mc_ $ \mc -> do (msg, _) <- sendDirectContactMessage ct (XMsgNew $ MCSimple (extMsgContent mc Nothing)) ci <- saveSndChatItem user (CDDirectSnd ct) msg (CISndMsgContent mc) - toView . CRNewChatItem $ AChatItem SCTDirect SMDSnd (DirectChat ct) ci + toView $ CRNewChatItem user (AChatItem SCTDirect SMDSnd (DirectChat ct) ci) forM_ groupId_ $ \groupId -> do gVar <- asks idsDrg groupConnIds <- createAgentConnectionAsync user CFCreateConnGrpInv True SCMInvitation @@ -2021,10 +2082,10 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage = withStore' (\db -> getDirectChatItemByAgentMsgId db user contactId connId msgId) >>= \case Just (CChatItem SMDSnd ci) -> do chatItem <- withStore $ \db -> updateDirectChatItemStatus db user contactId (chatItemId' ci) CISSndSent - toView $ CRChatItemStatusUpdated (AChatItem SCTDirect SMDSnd (DirectChat ct) chatItem) + toView $ CRChatItemStatusUpdated user (AChatItem SCTDirect SMDSnd (DirectChat ct) chatItem) _ -> pure () SWITCH qd phase cStats -> do - toView . CRContactSwitch ct $ SwitchProgress qd phase cStats + toView $ CRContactSwitch user ct (SwitchProgress qd phase cStats) when (phase /= SPConfirmed) $ case qd of QDRcv -> createInternalChatItem user (CDDirectSnd ct) (CISndConnEvent $ SCESwitchQueue phase Nothing) Nothing QDSnd -> createInternalChatItem user (CDDirectRcv ct) (CIRcvConnEvent $ RCESwitchQueue phase) Nothing @@ -2036,9 +2097,9 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage = chatItemId_ <- withStore' $ \db -> getChatItemIdByAgentMsgId db connId msgId forM_ chatItemId_ $ \chatItemId -> do chatItem <- withStore $ \db -> updateDirectChatItemStatus db user contactId chatItemId (agentErrToItemStatus err) - toView $ CRChatItemStatusUpdated (AChatItem SCTDirect SMDSnd (DirectChat ct) chatItem) + toView $ CRChatItemStatusUpdated user (AChatItem SCTDirect SMDSnd (DirectChat ct) chatItem) ERR err -> do - toView . CRChatError $ ChatErrorAgent err + toView $ CRChatError (Just user) (ChatErrorAgent err) when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () -- TODO add debugging output _ -> pure () @@ -2065,7 +2126,7 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage = withStore' $ \db -> setNewContactMemberConnRequest db user m cReq groupLinkId <- withStore' $ \db -> getGroupLinkId db user gInfo sendGrpInvitation ct m groupLinkId - toView $ CRSentGroupInvitation gInfo ct m + toView $ CRSentGroupInvitation user gInfo ct m where sendGrpInvitation :: Contact -> GroupMember -> Maybe GroupLinkId -> m () sendGrpInvitation ct GroupMember {memberId, memberRole = memRole} groupLinkId = do @@ -2118,7 +2179,7 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage = withAgent $ \a -> toggleConnectionNtfs a (aConnId conn) $ enableNtfs chatSettings case memberCategory m of GCHostMember -> do - toView $ CRUserJoinedGroup gInfo {membership = membership {memberStatus = GSMemConnected}} m {memberStatus = GSMemConnected} + toView $ CRUserJoinedGroup user gInfo {membership = membership {memberStatus = GSMemConnected}} m {memberStatus = GSMemConnected} createGroupFeatureItems gInfo m let GroupInfo {groupProfile = GroupProfile {description}} = gInfo memberConnectedChatItem gInfo m @@ -2127,7 +2188,7 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage = showToast ("#" <> gName) "you are connected to group" GCInviteeMember -> do memberConnectedChatItem gInfo m - toView $ CRJoinedGroupMember gInfo m {memberStatus = GSMemConnected} + toView $ CRJoinedGroupMember user gInfo m {memberStatus = GSMemConnected} setActive $ ActiveG gName showToast ("#" <> gName) $ "member " <> localDisplayName (m :: GroupMember) <> " is connected" intros <- withStore' $ \db -> createIntroductions db members m @@ -2175,7 +2236,7 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage = sentMsgDeliveryEvent conn msgId checkSndInlineFTComplete conn msgId SWITCH qd phase cStats -> do - toView . CRGroupMemberSwitch gInfo m $ SwitchProgress qd phase cStats + toView $ CRGroupMemberSwitch user gInfo m (SwitchProgress qd phase cStats) when (phase /= SPConfirmed) $ case qd of QDRcv -> createInternalChatItem user (CDGroupSnd gInfo) (CISndConnEvent . SCESwitchQueue phase . Just $ groupMemberRef m) Nothing QDSnd -> createInternalChatItem user (CDGroupRcv gInfo m) (CIRcvConnEvent $ RCESwitchQueue phase) Nothing @@ -2183,9 +2244,9 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage = -- [async agent commands] continuation on receiving OK withCompletedCommand conn agentMsg $ \CommandData {cmdFunction, cmdId} -> when (cmdFunction == CFAckMessage) $ ackMsgDeliveryEvent conn cmdId - MERR _ err -> toView . CRChatError $ ChatErrorAgent err + MERR _ err -> toView $ CRChatError (Just user) (ChatErrorAgent err) ERR err -> do - toView . CRChatError $ ChatErrorAgent err + toView $ CRChatError (Just user) (ChatErrorAgent err) when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () -- TODO add debugging output _ -> pure () @@ -2210,7 +2271,7 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage = ci <- withStore $ \db -> do liftIO $ updateSndFileStatus db ft FSConnected updateDirectCIFileStatus db user fileId CIFSSndTransfer - toView $ CRSndFileStart ci ft + toView $ CRSndFileStart user ci ft sendFileChunk user ft SENT msgId -> do withStore' $ \db -> updateSndFileChunkSent db ft msgId @@ -2220,7 +2281,7 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage = case err of SMP SMP.AUTH -> unless (fileStatus == FSCancelled) $ do ci <- withStore $ \db -> getChatItemByFileId db user fileId - toView $ CRSndFileRcvCancelled ci ft + toView $ CRSndFileRcvCancelled user ci ft _ -> throwChatError $ CEFileSend fileId err MSG meta _ _ -> do cmdId <- createAckCmd conn @@ -2229,7 +2290,7 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage = -- [async agent commands] continuation on receiving OK withCompletedCommand conn agentMsg $ \_cmdData -> pure () ERR err -> do - toView . CRChatError $ ChatErrorAgent err + toView $ CRChatError (Just user) (ChatErrorAgent err) when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () -- TODO add debugging output _ -> pure () @@ -2274,9 +2335,9 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage = OK -> -- [async agent commands] continuation on receiving OK withCompletedCommand conn agentMsg $ \_cmdData -> pure () - MERR _ err -> toView . CRChatError $ ChatErrorAgent err + MERR _ err -> toView $ CRChatError (Just user) (ChatErrorAgent err) ERR err -> do - toView . CRChatError $ ChatErrorAgent err + toView $ CRChatError (Just user) (ChatErrorAgent err) when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () -- TODO add debugging output _ -> pure () @@ -2287,14 +2348,14 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage = liftIO $ updateRcvFileStatus db ft FSConnected liftIO $ updateCIFileStatus db user fileId CIFSRcvTransfer getChatItemByFileId db user fileId - toView $ CRRcvFileStart ci + toView $ CRRcvFileStart user ci receiveFileChunk :: RcvFileTransfer -> Maybe Connection -> MsgMeta -> FileChunk -> m () receiveFileChunk ft@RcvFileTransfer {fileId, chunkSize, cancelled} conn_ MsgMeta {recipient = (msgId, _), integrity} = \case FileChunkCancel -> unless cancelled $ do cancelRcvFileTransfer user ft - toView (CRRcvFileSndCancelled ft) + toView $ CRRcvFileSndCancelled user ft FileChunk {chunkNo, chunkBytes = chunk} -> do case integrity of MsgOk -> pure () @@ -2317,7 +2378,7 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage = updateCIFileStatus db user fileId CIFSRcvComplete deleteRcvFileChunks db ft getChatItemByFileId db user fileId - toView $ CRRcvFileComplete ci + toView $ CRRcvFileComplete user ci closeFileHandle fileId rcvFiles mapM_ (deleteAgentConnectionAsync user) conn_ RcvChunkDuplicate -> pure () @@ -2332,9 +2393,9 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage = XInfo p -> profileContactRequest invId p Nothing -- TODO show/log error, other events in contact request _ -> pure () - MERR _ err -> toView . CRChatError $ ChatErrorAgent err + MERR _ err -> toView $ CRChatError (Just user) (ChatErrorAgent err) ERR err -> do - toView . CRChatError $ ChatErrorAgent err + toView $ CRChatError (Just user) (ChatErrorAgent err) when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () -- TODO add debugging output _ -> pure () @@ -2342,7 +2403,7 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage = profileContactRequest :: InvitationId -> Profile -> Maybe XContactId -> m () profileContactRequest invId p xContactId_ = do withStore (\db -> createOrUpdateContactRequest db user userContactLinkId invId p xContactId_) >>= \case - CORContact contact -> toView $ CRContactRequestAlreadyAccepted contact + CORContact contact -> toView $ CRContactRequestAlreadyAccepted user contact CORRequest cReq@UserContactRequest {localDisplayName} -> do withStore' (\db -> getUserContactLinkById db userId userContactLinkId) >>= \case Just (UserContactLink {autoAccept}, groupId_) -> @@ -2352,14 +2413,14 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage = -- [incognito] generate profile to send, create connection with incognito profile incognitoProfile <- if acceptIncognito then Just . NewIncognito <$> liftIO generateRandomProfile else pure Nothing ct <- acceptContactRequestAsync user cReq incognitoProfile - toView $ CRAcceptingContactRequest ct + toView $ CRAcceptingContactRequest user ct Just groupId -> do gInfo@GroupInfo {membership = membership@GroupMember {memberProfile}} <- withStore $ \db -> getGroupInfo db user groupId let profileMode = if memberIncognito membership then Just $ ExistingIncognito memberProfile else Nothing ct <- acceptContactRequestAsync user cReq profileMode - toView $ CRAcceptingGroupJoinRequest gInfo ct + toView $ CRAcceptingGroupJoinRequest user gInfo ct _ -> do - toView $ CRReceivedContactRequest cReq + toView $ CRReceivedContactRequest user cReq showToast (localDisplayName <> "> ") "wants to connect to you" _ -> pure () @@ -2426,7 +2487,7 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage = notifyMemberConnected :: GroupInfo -> GroupMember -> m () notifyMemberConnected gInfo m@GroupMember {localDisplayName = c} = do memberConnectedChatItem gInfo m - toView $ CRConnectedToGroupMember gInfo m + toView $ CRConnectedToGroupMember user gInfo m let g = groupName' gInfo setActive $ ActiveG g showToast ("#" <> g) $ "member " <> c <> " is connected" @@ -2449,10 +2510,10 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage = withStore' $ \db -> createSentProbeHash db userId probeId c messageWarning :: Text -> m () - messageWarning = toView . CRMessageError "warning" + messageWarning = toView . CRMessageError user "warning" messageError :: Text -> m () - messageError = toView . CRMessageError "error" + messageError = toView . CRMessageError user "error" newContentMessage :: Contact -> MsgContainer -> RcvMessage -> MsgMeta -> m () newContentMessage ct@Contact {localDisplayName = c, contactUsed, chatSettings} mc msg@RcvMessage {sharedMsgId_} msgMeta = do @@ -2475,7 +2536,7 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage = where newChatItem ciContent ciFile_ timed_ live = do ci <- saveRcvChatItem' user (CDDirectRcv ct) msg sharedMsgId_ msgMeta ciContent ciFile_ timed_ live - toView . CRNewChatItem $ AChatItem SCTDirect SMDRcv (DirectChat ct) ci + toView $ CRNewChatItem user (AChatItem SCTDirect SMDRcv (DirectChat ct) ci) pure ci processFileInvitation :: Maybe FileInvitation -> MsgContent -> (DB.Connection -> FileInvitation -> Maybe InlineFileMode -> Integer -> IO RcvFileTransfer) -> m (Maybe (CIFile 'MDRcv)) @@ -2503,7 +2564,7 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage = let timed_ = rcvContactCITimed ct ttl ci <- saveRcvChatItem' user (CDDirectRcv ct) msg (Just sharedMsgId) msgMeta content Nothing timed_ live ci' <- withStore' $ \db -> updateDirectChatItem' db user contactId ci content live Nothing - toView . CRChatItemUpdated $ AChatItem SCTDirect SMDRcv (DirectChat ct) ci' + toView $ CRChatItemUpdated user (AChatItem SCTDirect SMDRcv (DirectChat ct) ci') setActive $ ActiveC c _ -> throwError e where @@ -2514,7 +2575,7 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage = case msgDir of SMDRcv -> do ci' <- withStore' $ \db -> updateDirectChatItem' db user contactId ci content live $ Just msgId - toView . CRChatItemUpdated $ AChatItem SCTDirect SMDRcv (DirectChat ct) ci' + toView $ CRChatItemUpdated user (AChatItem SCTDirect SMDRcv (DirectChat ct) ci') startUpdatedTimedItemThread user (ChatRef CTDirect contactId) ci ci' SMDSnd -> messageError "x.msg.update: contact attempted invalid message update" @@ -2523,7 +2584,7 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage = checkIntegrityCreateItem (CDDirectRcv ct) msgMeta deleteRcvChatItem `catchError` \e -> case e of - (ChatErrorStore (SEChatItemSharedMsgIdNotFound sMsgId)) -> toView $ CRChatItemDeletedNotFound ct sMsgId + (ChatErrorStore (SEChatItemSharedMsgIdNotFound sMsgId)) -> toView $ CRChatItemDeletedNotFound user ct sMsgId _ -> throwError e where deleteRcvChatItem = do @@ -2567,7 +2628,7 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage = let timed_ = rcvGroupCITimed gInfo ttl_ ci <- saveRcvChatItem' user (CDGroupRcv gInfo m) msg (Just sharedMsgId) msgMeta content Nothing timed_ live ci' <- withStore' $ \db -> updateGroupChatItem db user groupId ci content live Nothing - toView . CRChatItemUpdated $ AChatItem SCTGroup SMDRcv (GroupChat gInfo) ci' + toView $ CRChatItemUpdated user (AChatItem SCTGroup SMDRcv (GroupChat gInfo) ci') setActive $ ActiveG g _ -> throwError e where @@ -2580,7 +2641,7 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage = if sameMemberId memberId m' then do ci' <- withStore' $ \db -> updateGroupChatItem db user groupId ci content live $ Just msgId - toView . CRChatItemUpdated $ AChatItem SCTGroup SMDRcv (GroupChat gInfo) ci' + toView $ CRChatItemUpdated user (AChatItem SCTGroup SMDRcv (GroupChat gInfo) ci') setActive $ ActiveG g startUpdatedTimedItemThread user (ChatRef CTGroup groupId) ci ci' else messageError "x.msg.update: group member attempted to update a message of another member" -- shouldn't happen now that query includes group member id @@ -2608,7 +2669,7 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage = RcvFileTransfer {fileId} <- withStore' $ \db -> createRcvFileTransfer db userId ct fInv inline chSize let ciFile = Just $ CIFile {fileId, fileName, fileSize, filePath = Nothing, fileStatus = CIFSRcvInvitation} ci <- saveRcvChatItem' user (CDDirectRcv ct) msg sharedMsgId_ msgMeta (CIRcvMsgContent $ MCFile "") ciFile Nothing False - toView . CRNewChatItem $ AChatItem SCTDirect SMDRcv (DirectChat ct) ci + toView $ CRNewChatItem user (AChatItem SCTDirect SMDRcv (DirectChat ct) ci) showToast (c <> "> ") "wants to send a file" setActive $ ActiveC c @@ -2641,7 +2702,7 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage = ft@RcvFileTransfer {cancelled} <- withStore (\db -> getRcvFileTransfer db user fileId) unless cancelled $ do cancelRcvFileTransfer user ft - toView $ CRRcvFileSndCancelled ft + toView $ CRRcvFileSndCancelled user ft xFileAcptInv :: Contact -> SharedMsgId -> Maybe ConnReqInvitation -> String -> MsgMeta -> m () xFileAcptInv ct sharedMsgId fileConnReq_ fName msgMeta = do @@ -2660,7 +2721,7 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage = event <- withStore $ \db -> do ci <- updateDirectCIFileStatus db user fileId CIFSSndTransfer sft <- liftIO $ createSndDirectInlineFT db ct ft - pure $ CRSndFileStart ci sft + pure $ CRSndFileStart user ci sft toView event ifM (allowSendInline fileSize fileInline) @@ -2676,7 +2737,7 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage = liftIO $ updateSndFileStatus db ft FSComplete liftIO $ deleteSndFileChunks db ft updateDirectCIFileStatus db user fileId CIFSSndComplete - toView $ CRSndFileComplete ci ft + toView $ CRSndFileComplete user ci ft allowSendInline :: Integer -> Maybe InlineFileMode -> m Bool allowSendInline fileSize = \case @@ -2717,7 +2778,7 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage = ft@RcvFileTransfer {cancelled} <- withStore (\db -> getRcvFileTransfer db user fileId) unless cancelled $ do cancelRcvFileTransfer user ft - toView $ CRRcvFileSndCancelled ft + toView $ CRRcvFileSndCancelled user ft else messageError "x.file.cancel: group member attempted to cancel file of another member" -- shouldn't happen now that query includes group member id (SMDSnd, _) -> messageError "x.file.cancel: group member attempted invalid file cancel" @@ -2739,7 +2800,7 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage = event <- withStore $ \db -> do ci <- updateDirectCIFileStatus db user fileId CIFSSndTransfer sft <- liftIO $ createSndGroupInlineFT db m conn ft - pure $ CRSndFileStart ci sft + pure $ CRSndFileStart user ci sft toView event ifM (allowSendInline fileSize fileInline) @@ -2751,7 +2812,7 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage = groupMsgToView :: GroupInfo -> GroupMember -> ChatItem 'CTGroup 'MDRcv -> MsgMeta -> m () groupMsgToView gInfo m ci msgMeta = do checkIntegrityCreateItem (CDGroupRcv gInfo m) msgMeta - toView . CRNewChatItem $ AChatItem SCTGroup SMDRcv (GroupChat gInfo) ci + toView $ CRNewChatItem user (AChatItem SCTGroup SMDRcv (GroupChat gInfo) ci) processGroupInvitation :: Contact -> GroupInvitation -> RcvMessage -> MsgMeta -> m () processGroupInvitation ct@Contact {localDisplayName = c, activeConn = Connection {customUserProfileId, groupLinkId = groupLinkId'}} inv@GroupInvitation {fromMember = (MemberIdRole fromMemId fromRole), invitedMember = (MemberIdRole memId memRole), connRequest, groupLinkId} msg msgMeta = do @@ -2767,13 +2828,13 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage = createMemberConnectionAsync db user hostId connIds updateGroupMemberStatusById db userId hostId GSMemAccepted updateGroupMemberStatus db userId membership GSMemAccepted - toView $ CRUserAcceptedGroupSent gInfo {membership = membership {memberStatus = GSMemAccepted}} (Just ct) + toView $ CRUserAcceptedGroupSent user gInfo {membership = membership {memberStatus = GSMemAccepted}} (Just ct) else do let content = CIRcvGroupInvitation (CIGroupInvitation {groupId, groupMemberId, localDisplayName, groupProfile, status = CIGISPending}) memRole ci <- saveRcvChatItem user (CDDirectRcv ct) msg msgMeta content withStore' $ \db -> setGroupInvitationChatItemId db user groupId (chatItemId' ci) - toView . CRNewChatItem $ AChatItem SCTDirect SMDRcv (DirectChat ct) ci - toView $ CRReceivedGroupInvitation gInfo ct memRole + toView $ CRNewChatItem user (AChatItem SCTDirect SMDRcv (DirectChat ct) ci) + toView $ CRReceivedGroupInvitation user gInfo ct memRole showToast ("#" <> localDisplayName <> " " <> c <> "> ") "invited you to join the group" where sameGroupLinkId :: Maybe GroupLinkId -> Maybe GroupLinkId -> Bool @@ -2785,7 +2846,7 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage = MsgOk -> pure () MsgError e -> case e of MsgSkipped {} -> createInternalChatItem user cd (CIRcvIntegrityError e) (Just brokerTs) - _ -> toView $ CRMsgIntegrityError e + _ -> toView $ CRMsgIntegrityError user e xInfo :: Contact -> Profile -> m () xInfo c@Contact {profile = p} p' = unless (fromLocalProfile p == p') $ do @@ -2796,7 +2857,7 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage = c' <- liftIO $ updateContactUserPreferences db user c ctUserPrefs' updateContactProfile db user c' p' when (directOrUsed c') $ createRcvFeatureItems user c c' - toView $ CRContactUpdated c c' + toView $ CRContactUpdated user c c' where Contact {userPreferences = ctUserPrefs@Preferences {timedMessages = ctUserTMPref}} = c userTTL = prefParam $ getPreference SCFTimedMessages ctUserPrefs @@ -2871,8 +2932,8 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage = withStore' $ \db -> createCall db user call' $ chatItemTs' ci call_ <- atomically (TM.lookupInsert contactId call' calls) forM_ call_ $ \call -> updateCallItemStatus user ct call WCSDisconnected Nothing - toView . CRCallInvitation $ RcvCallInvitation {contact = ct, callType, sharedKey, callTs = chatItemTs' ci} - toView . CRNewChatItem $ AChatItem SCTDirect SMDRcv (DirectChat ct) ci + toView $ CRCallInvitation user (RcvCallInvitation {contact = ct, callType, sharedKey, callTs = chatItemTs' ci}) + toView $ CRNewChatItem user (AChatItem SCTDirect SMDRcv (DirectChat ct) ci) where saveCallItem status = saveRcvChatItem user (CDDirectRcv ct) msg msgMeta (CIRcvCall status 0) @@ -2885,7 +2946,7 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage = let sharedKey = C.Key . C.dhBytes' <$> (C.dh' <$> callDhPubKey <*> localDhPrivKey) callState' = CallOfferReceived {localCallType, peerCallType = callType, peerCallSession = rtcSession, sharedKey} askConfirmation = encryptedCall localCallType && not (encryptedCall callType) - toView CRCallOffer {contact = ct, callType, offer = rtcSession, sharedKey, askConfirmation} + toView CRCallOffer {user, contact = ct, callType, offer = rtcSession, sharedKey, askConfirmation} pure (Just call {callState = callState'}, Just . ACIContent SMDSnd $ CISndCall CISCallAccepted 0) _ -> do msgCallStateError "x.call.offer" call @@ -2898,7 +2959,7 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage = \call -> case callState call of CallOfferSent {localCallType, peerCallType, localCallSession, sharedKey} -> do let callState' = CallNegotiated {localCallType, peerCallType, localCallSession, peerCallSession = rtcSession, sharedKey} - toView $ CRCallAnswer ct rtcSession + toView $ CRCallAnswer user ct rtcSession pure (Just call {callState = callState'}, Just . ACIContent SMDRcv $ CIRcvCall CISCallNegotiated 0) _ -> do msgCallStateError "x.call.answer" call @@ -2912,12 +2973,12 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage = CallOfferReceived {localCallType, peerCallType, peerCallSession, sharedKey} -> do -- TODO update the list of ice servers in peerCallSession let callState' = CallOfferReceived {localCallType, peerCallType, peerCallSession, sharedKey} - toView $ CRCallExtraInfo ct rtcExtraInfo + toView $ CRCallExtraInfo user ct rtcExtraInfo pure (Just call {callState = callState'}, Nothing) CallNegotiated {localCallType, peerCallType, localCallSession, peerCallSession, sharedKey} -> do -- TODO update the list of ice servers in peerCallSession let callState' = CallNegotiated {localCallType, peerCallType, localCallSession, peerCallSession, sharedKey} - toView $ CRCallExtraInfo ct rtcExtraInfo + toView $ CRCallExtraInfo user ct rtcExtraInfo pure (Just call {callState = callState'}, Nothing) _ -> do msgCallStateError "x.call.extra" call @@ -2927,7 +2988,7 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage = xCallEnd :: Contact -> CallId -> RcvMessage -> MsgMeta -> m () xCallEnd ct callId msg msgMeta = msgCurrentCall ct callId "x.call.end" msg msgMeta $ \Call {chatItemId} -> do - toView $ CRCallEnded ct + toView $ CRCallEnded user ct (Nothing,) <$> callStatusItemContent user ct chatItemId WCSDisconnected msgCurrentCall :: Contact -> CallId -> Text -> RcvMessage -> MsgMeta -> (Call -> m (Maybe Call, Maybe ACIContent)) -> m () @@ -2957,7 +3018,7 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage = mergeContacts :: Contact -> Contact -> m () mergeContacts to from = do withStore' $ \db -> mergeContactRecords db userId to from - toView $ CRContactsMerged to from + toView $ CRContactsMerged user to from saveConnInfo :: Connection -> ConnInfo -> m () saveConnInfo activeConn connInfo = do @@ -2965,7 +3026,7 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage = case chatMsgEvent of XInfo p -> do ct <- withStore $ \db -> createDirectContact db user activeConn p - toView $ CRContactConnecting ct + toView $ CRContactConnecting user ct -- TODO show/log error, other events in SMP confirmation _ -> pure () @@ -2980,7 +3041,7 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage = newMember@GroupMember {groupMemberId} <- withStore $ \db -> createNewGroupMember db user gInfo memInfo GCPostMember GSMemAnnounced ci <- saveRcvChatItem user (CDGroupRcv gInfo m) msg msgMeta (CIRcvGroupEvent $ RGEMemberAdded groupMemberId memberProfile) groupMsgToView gInfo m ci msgMeta - toView $ CRJoinedGroupMemberConnecting gInfo m newMember + toView $ CRJoinedGroupMemberConnecting user gInfo m newMember xGrpMemIntro :: GroupInfo -> GroupMember -> MemberInfo -> m () xGrpMemIntro gInfo@GroupInfo {membership, chatSettings = ChatSettings {enableNtfs}} m@GroupMember {memberRole, localDisplayName = c} memInfo@(MemberInfo memId _ _) = do @@ -3056,7 +3117,7 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage = withStore' $ \db -> updateGroupMemberRole db user member memRole ci <- saveRcvChatItem user (CDGroupRcv gInfo m) msg msgMeta (CIRcvGroupEvent gEvent) groupMsgToView gInfo m ci msgMeta - toView CRMemberRole {groupInfo = gInfo', byMember = m, member = member {memberRole = memRole}, fromRole, toRole = memRole} + toView CRMemberRole {user, groupInfo = gInfo', byMember = m, member = member {memberRole = memRole}, fromRole, toRole = memRole} checkHostRole :: GroupMember -> GroupMemberRole -> m () checkHostRole GroupMember {memberRole, localDisplayName} memRole = @@ -3072,7 +3133,7 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage = forM_ members $ deleteMemberConnection user withStore' $ \db -> updateGroupMemberStatus db userId membership GSMemRemoved deleteMemberItem RGEUserDeleted - toView $ CRDeletedMemberUser gInfo {membership = membership {memberStatus = GSMemRemoved}} m + toView $ CRDeletedMemberUser user gInfo {membership = membership {memberStatus = GSMemRemoved}} m else case find (sameMemberId memId) members of Nothing -> messageError "x.grp.mem.del with unknown member ID" Just member@GroupMember {groupMemberId, memberProfile} -> @@ -3081,7 +3142,7 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage = -- undeleted "member connected" chat item will prevent deletion of member record deleteOrUpdateMemberRecord user member deleteMemberItem $ RGEMemberDeleted groupMemberId (fromLocalProfile memberProfile) - toView $ CRDeletedMember gInfo m member {memberStatus = GSMemRemoved} + toView $ CRDeletedMember user gInfo m member {memberStatus = GSMemRemoved} where checkRole GroupMember {memberRole} a | senderRole < GRAdmin || senderRole < memberRole = @@ -3101,7 +3162,7 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage = withStore' $ \db -> updateGroupMemberStatus db userId m GSMemLeft ci <- saveRcvChatItem user (CDGroupRcv gInfo m) msg msgMeta (CIRcvGroupEvent RGEMemberLeft) groupMsgToView gInfo m ci msgMeta - toView $ CRLeftMember gInfo m {memberStatus = GSMemLeft} + toView $ CRLeftMember user gInfo m {memberStatus = GSMemLeft} xGrpDel :: GroupInfo -> GroupMember -> RcvMessage -> MsgMeta -> m () xGrpDel gInfo@GroupInfo {membership} m@GroupMember {memberRole} msg msgMeta = do @@ -3114,14 +3175,14 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage = forM_ ms $ deleteMemberConnection user ci <- saveRcvChatItem user (CDGroupRcv gInfo m) msg msgMeta (CIRcvGroupEvent RGEGroupDeleted) groupMsgToView gInfo m ci msgMeta - toView $ CRGroupDeleted gInfo {membership = membership {memberStatus = GSMemGroupDeleted}} m + toView $ CRGroupDeleted user gInfo {membership = membership {memberStatus = GSMemGroupDeleted}} m xGrpInfo :: GroupInfo -> GroupMember -> GroupProfile -> RcvMessage -> MsgMeta -> m () xGrpInfo g@GroupInfo {groupProfile = p} m@GroupMember {memberRole} p' msg msgMeta | memberRole < GROwner = messageError "x.grp.info with insufficient member permissions" | otherwise = unless (p == p') $ do g' <- withStore $ \db -> updateGroupProfile db user g p' - toView . CRGroupUpdated g g' $ Just m + toView $ CRGroupUpdated user g g' (Just m) let cd = CDGroupRcv g' m unless (sameGroupProfileInfo p p') $ do ci <- saveRcvChatItem user cd msg msgMeta (CIRcvGroupEvent $ RGEGroupUpdated p') @@ -3166,7 +3227,7 @@ sendFileChunk user ft@SndFileTransfer {fileId, fileStatus, connId, agentConnId} liftIO $ updateSndFileStatus db ft FSComplete liftIO $ deleteSndFileChunks db ft updateDirectCIFileStatus db user fileId CIFSSndComplete - toView $ CRSndFileComplete ci ft + toView $ CRSndFileComplete user ci ft closeFileHandle fileId sndFiles deleteAgentConnectionAsync' user connId agentConnId @@ -3382,13 +3443,13 @@ deleteDirectCI :: ChatMonad m => User -> Contact -> CChatItem 'CTDirect -> Bool deleteDirectCI user ct ci@(CChatItem msgDir deletedItem@ChatItem {file}) byUser timed = do deleteCIFile user file withStore' $ \db -> deleteDirectChatItem db user ct ci - pure $ CRChatItemDeleted (AChatItem SCTDirect msgDir (DirectChat ct) deletedItem) Nothing byUser timed + pure $ CRChatItemDeleted user (AChatItem SCTDirect msgDir (DirectChat ct) deletedItem) Nothing byUser timed deleteGroupCI :: ChatMonad m => User -> GroupInfo -> CChatItem 'CTGroup -> Bool -> Bool -> m ChatResponse deleteGroupCI user gInfo ci@(CChatItem msgDir deletedItem@ChatItem {file}) byUser timed = do deleteCIFile user file withStore' $ \db -> deleteGroupChatItem db user gInfo ci - pure $ CRChatItemDeleted (AChatItem SCTGroup msgDir (GroupChat gInfo) deletedItem) Nothing byUser timed + pure $ CRChatItemDeleted user (AChatItem SCTGroup msgDir (GroupChat gInfo) deletedItem) Nothing byUser timed deleteCIFile :: (ChatMonad m, MsgDirectionI d) => User -> Maybe (CIFile d) -> m () deleteCIFile user file = @@ -3399,12 +3460,12 @@ deleteCIFile user file = markDirectCIDeleted :: ChatMonad m => User -> Contact -> CChatItem 'CTDirect -> MessageId -> Bool -> m ChatResponse markDirectCIDeleted user ct ci@(CChatItem msgDir deletedItem) msgId byUser = do toCi <- withStore' $ \db -> markDirectChatItemDeleted db user ct ci msgId - pure $ CRChatItemDeleted (AChatItem SCTDirect msgDir (DirectChat ct) deletedItem) (Just toCi) byUser False + pure $ CRChatItemDeleted user (AChatItem SCTDirect msgDir (DirectChat ct) deletedItem) (Just toCi) byUser False markGroupCIDeleted :: ChatMonad m => User -> GroupInfo -> CChatItem 'CTGroup -> MessageId -> Bool -> m ChatResponse markGroupCIDeleted user gInfo ci@(CChatItem msgDir deletedItem) msgId byUser = do toCi <- withStore' $ \db -> markGroupChatItemDeleted db user gInfo ci msgId - pure $ CRChatItemDeleted (AChatItem SCTGroup msgDir (GroupChat gInfo) deletedItem) (Just toCi) byUser False + pure $ CRChatItemDeleted user (AChatItem SCTGroup msgDir (GroupChat gInfo) deletedItem) (Just toCi) byUser False createAgentConnectionAsync :: forall m c. (ChatMonad m, ConnectionModeI c) => User -> CommandFunction -> Bool -> SConnectionMode c -> m (CommandId, ConnId) createAgentConnectionAsync user cmdFunction enableNtfs cMode = do @@ -3507,7 +3568,7 @@ createInternalChatItem user cd content itemTs_ = do when (ciRequiresAttention content) $ updateChatTs db user cd createdAt createNewChatItemNoMsg db user cd content itemTs createdAt ci <- liftIO $ mkChatItem cd ciId content Nothing Nothing Nothing Nothing False itemTs createdAt - toView $ CRNewChatItem $ AChatItem (chatTypeI @c) (msgDirection @d) (toChatInfo cd) ci + toView $ CRNewChatItem user (AChatItem (chatTypeI @c) (msgDirection @d) (toChatInfo cd) ci) getCreateActiveUser :: SQLiteStore -> IO User getCreateActiveUser st = do @@ -3538,7 +3599,7 @@ getCreateActiveUser st = do Right user -> pure user selectUser :: [User] -> IO User selectUser [user] = do - withTransaction st (`setActiveUser` userId user) + withTransaction st (`setActiveUser` userId (user :: User)) pure user selectUser users = do putStrLn "Select user profile:" @@ -3553,7 +3614,7 @@ getCreateActiveUser st = do | n <= 0 || n > length users -> putStrLn "invalid user number" >> loop | otherwise -> do let user = users !! (n - 1) - withTransaction st (`setActiveUser` userId user) + withTransaction st (`setActiveUser` userId (user :: User)) pure user userStr :: User -> String userStr User {localDisplayName, profile = LocalProfile {fullName}} = @@ -3582,13 +3643,13 @@ notificationSubscriber = do ChatController {notifyQ, sendNotification} <- ask forever $ atomically (readTBQueue notifyQ) >>= liftIO . sendNotification -withUser' :: ChatMonad m => (User -> m a) -> m a +withUser' :: ChatMonad m => (User -> m ChatResponse) -> m ChatResponse withUser' action = asks currentUser >>= readTVarIO - >>= maybe (throwChatError CENoActiveUser) action + >>= maybe (throwChatError CENoActiveUser) (\u -> action u `catchError` (pure . CRChatError (Just u))) -withUser :: ChatMonad m => (User -> m a) -> m a +withUser :: ChatMonad m => (User -> m ChatResponse) -> m ChatResponse withUser action = withUser' $ \user -> ifM chatStarted (action user) (throwChatError CEChatNotStarted) @@ -3626,7 +3687,12 @@ chatCommandP = choice [ "/mute " *> ((`ShowMessages` False) <$> chatNameP'), "/unmute " *> ((`ShowMessages` True) <$> chatNameP'), - ("/user " <|> "/u ") *> (CreateActiveUser <$> userProfile), + "/create user " *> (CreateActiveUser <$> userProfile), + "/users" $> ListUsers, + "/_user " *> (APISetActiveUser <$> A.decimal), + ("/user " <|> "/u ") *> (SetActiveUser <$> displayName), + "/_delete user " *> (APIDeleteUser <$> A.decimal), + "/delete user " *> (DeleteUser <$> displayName), ("/user" <|> "/u") $> ShowActiveUser, "/_start subscribe=" *> (StartChat <$> onOffP <* " expire=" <*> onOffP), "/_start" $> StartChat True True, diff --git a/src/Simplex/Chat/Bot.hs b/src/Simplex/Chat/Bot.hs index 9368b3645..e930bfb7c 100644 --- a/src/Simplex/Chat/Bot.hs +++ b/src/Simplex/Chat/Bot.hs @@ -24,10 +24,10 @@ chatBotRepl welcome answer _user cc = do race_ (forever $ void getLine) . forever $ do (_, resp) <- atomically . readTBQueue $ outputQ cc case resp of - CRContactConnected contact _ -> do + CRContactConnected _ contact _ -> do contactConnected contact void $ sendMsg contact welcome - CRNewChatItem (AChatItem _ SMDRcv (DirectChat contact) ChatItem {content}) -> do + CRNewChatItem _ (AChatItem _ SMDRcv (DirectChat contact) ChatItem {content}) -> do let msg = T.unpack $ ciContentToText content void . sendMsg contact $ answer msg _ -> pure () @@ -38,11 +38,11 @@ chatBotRepl welcome answer _user cc = do initializeBotAddress :: ChatController -> IO () initializeBotAddress cc = do sendChatCmd cc "/show_address" >>= \case - CRUserContactLink UserContactLink {connReqContact} -> showBotAddress connReqContact - CRChatCmdError (ChatErrorStore SEUserContactLinkNotFound) -> do + CRUserContactLink _ UserContactLink {connReqContact} -> showBotAddress connReqContact + CRChatCmdError _ (ChatErrorStore SEUserContactLinkNotFound) -> do putStrLn "No bot address, creating..." sendChatCmd cc "/address" >>= \case - CRUserContactLinkCreated uri -> showBotAddress uri + CRUserContactLinkCreated _ uri -> showBotAddress uri _ -> putStrLn "can't create bot address" >> exitFailure _ -> putStrLn "unexpected response" >> exitFailure where diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index bae3ca83e..3ad58bcc3 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -140,6 +140,11 @@ instance ToJSON HelpSection where data ChatCommand = ShowActiveUser | CreateActiveUser Profile + | ListUsers + | APISetActiveUser UserId + | SetActiveUser UserName + | APIDeleteUser UserId + | DeleteUser UserName | StartChat {subscribeConnections :: Bool, enableExpireChatItems :: Bool} | APIStopChat | APIActivateChat @@ -153,7 +158,7 @@ data ChatCommand | APIStorageEncryption DBEncryptionConfig | ExecChatStoreSQL Text | ExecAgentStoreSQL Text - | APIGetChats {pendingConnections :: Bool} + | APIGetChats {pendingConnections :: Bool} -- UserId | APIGetChat ChatRef ChatPagination (Maybe String) | APIGetChatItems Int | APISendMessage {chatRef :: ChatRef, liveMessage :: Bool, composedMessage :: ComposedMessage} @@ -172,9 +177,9 @@ data ChatCommand | APISendCallAnswer ContactId WebRTCSession | APISendCallExtraInfo ContactId WebRTCExtraInfo | APIEndCall ContactId - | APIGetCallInvitations + | APIGetCallInvitations -- UserId | APICallStatus ContactId WebRTCCallStatus - | APIUpdateProfile Profile + | APIUpdateProfile Profile -- UserId | APISetContactPrefs ContactId Preferences | APISetContactAlias ContactId LocalAlias | APISetConnectionAlias Int64 LocalAlias @@ -183,7 +188,7 @@ data ChatCommand | APIRegisterToken DeviceToken NotificationsMode | APIVerifyToken DeviceToken C.CbNonce ByteString | APIDeleteToken DeviceToken - | APIGetNtfMessage {nonce :: C.CbNonce, encNtfInfo :: ByteString} + | APIGetNtfMessage {nonce :: C.CbNonce, encNtfInfo :: ByteString} -- UserId | APIAddMember GroupId ContactId GroupMemberRole | APIJoinGroup GroupId | APIMemberRole GroupId GroupMemberId GroupMemberRole @@ -194,11 +199,11 @@ data ChatCommand | APICreateGroupLink GroupId | APIDeleteGroupLink GroupId | APIGetGroupLink GroupId - | GetUserSMPServers - | SetUserSMPServers SMPServersConfig + | GetUserSMPServers -- UserId + | SetUserSMPServers SMPServersConfig -- UserId | TestSMPServer SMPServerWithAuth - | APISetChatItemTTL (Maybe Int64) - | APIGetChatItemTTL + | APISetChatItemTTL (Maybe Int64) -- UserId + | APIGetChatItemTTL -- UserId | APISetNetworkConfig NetworkConfig | APIGetNetworkConfig | APISetChatSettings ChatRef ChatSettings @@ -221,26 +226,26 @@ data ChatCommand | VerifyGroupMember GroupName ContactName (Maybe Text) | ChatHelp HelpSection | Welcome - | AddContact - | Connect (Maybe AConnectionRequestUri) - | ConnectSimplex + | AddContact -- UserId + | Connect (Maybe AConnectionRequestUri) -- UserId + | ConnectSimplex -- UserId | DeleteContact ContactName | ClearContact ContactName - | ListContacts - | CreateMyAddress - | DeleteMyAddress - | ShowMyAddress - | AddressAutoAccept (Maybe AutoAccept) + | ListContacts -- UserId + | CreateMyAddress -- UserId + | DeleteMyAddress -- UserId + | ShowMyAddress -- UserId + | AddressAutoAccept (Maybe AutoAccept) -- UserId | AcceptContact ContactName | RejectContact ContactName | SendMessage ChatName ByteString | SendLiveMessage ChatName ByteString | SendMessageQuote {contactName :: ContactName, msgDir :: AMsgDirection, quotedMsg :: ByteString, message :: ByteString} - | SendMessageBroadcast ByteString + | SendMessageBroadcast ByteString -- UserId | DeleteMessage ChatName ByteString | EditMessage {chatName :: ChatName, editedMsg :: ByteString, message :: ByteString} | UpdateLiveMessage {chatName :: ChatName, chatItemId :: ChatItemId, liveMessage :: Bool, message :: ByteString} - | NewGroup GroupProfile + | NewGroup GroupProfile -- UserId | AddMember GroupName ContactName GroupMemberRole | JoinGroup GroupName | MemberRole GroupName ContactName GroupMemberRole @@ -249,7 +254,7 @@ data ChatCommand | DeleteGroup GroupName | ClearGroup GroupName | ListMembers GroupName - | ListGroups + | ListGroups -- UserId | UpdateGroupNames GroupName GroupProfile | ShowGroupProfile GroupName | UpdateGroupDescription GroupName (Maybe Text) @@ -257,9 +262,9 @@ data ChatCommand | DeleteGroupLink GroupName | ShowGroupLink GroupName | SendGroupMessageQuote {groupName :: GroupName, contactName_ :: Maybe ContactName, quotedMsg :: ByteString, message :: ByteString} - | LastMessages (Maybe ChatName) Int (Maybe String) - | LastChatItemId (Maybe ChatName) Int - | ShowChatItem (Maybe ChatItemId) + | LastMessages (Maybe ChatName) Int (Maybe String) -- UserId + | LastChatItemId (Maybe ChatName) Int -- UserId + | ShowChatItem (Maybe ChatItemId) -- UserId | ShowLiveItems Bool | SendFile ChatName FilePath | SendImage ChatName FilePath @@ -268,13 +273,13 @@ data ChatCommand | ReceiveFile {fileId :: FileTransferId, fileInline :: Maybe Bool, filePath :: Maybe FilePath} | CancelFile FileTransferId | FileStatus FileTransferId - | ShowProfile - | UpdateProfile ContactName Text - | UpdateProfileImage (Maybe ImageData) - | SetUserFeature AChatFeature FeatureAllowed + | ShowProfile -- UserId + | UpdateProfile ContactName Text -- UserId + | UpdateProfileImage (Maybe ImageData) -- UserId + | SetUserFeature AChatFeature FeatureAllowed -- UserId | SetContactFeature AChatFeature ContactName (Maybe FeatureAllowed) | SetGroupFeature AGroupFeature GroupName GroupFeatureEnabled - | SetUserTimedMessages Bool + | SetUserTimedMessages Bool -- UserId | SetContactTimedMessages ContactName (Maybe TimedMessagesEnabled) | SetGroupTimedMessages GroupName (Maybe Int) | QuitChat @@ -286,137 +291,138 @@ data ChatCommand data ChatResponse = CRActiveUser {user :: User} + | CRUsersList {users :: [User]} | CRChatStarted | CRChatRunning | CRChatStopped | CRChatSuspended - | CRApiChats {chats :: [AChat]} - | CRApiChat {chat :: AChat} - | CRChatItems {chatItems :: [AChatItem]} - | CRChatItemId (Maybe ChatItemId) + | CRApiChats {user :: User, chats :: [AChat]} + | CRApiChat {user :: User, chat :: AChat} + | CRChatItems {user :: User, chatItems :: [AChatItem]} + | CRChatItemId User (Maybe ChatItemId) | CRApiParsedMarkdown {formattedText :: Maybe MarkdownList} - | CRUserSMPServers {smpServers :: NonEmpty ServerCfg, presetSMPServers :: NonEmpty SMPServerWithAuth} + | CRUserSMPServers {user :: User, smpServers :: NonEmpty ServerCfg, presetSMPServers :: NonEmpty SMPServerWithAuth} | CRSmpTestResult {smpTestFailure :: Maybe SMPTestFailure} - | CRChatItemTTL {chatItemTTL :: Maybe Int64} + | CRChatItemTTL {user :: User, chatItemTTL :: Maybe Int64} | CRNetworkConfig {networkConfig :: NetworkConfig} - | CRContactInfo {contact :: Contact, connectionStats :: ConnectionStats, customUserProfile :: Maybe Profile} - | CRGroupMemberInfo {groupInfo :: GroupInfo, member :: GroupMember, connectionStats_ :: Maybe ConnectionStats} - | CRContactSwitch {contact :: Contact, switchProgress :: SwitchProgress} - | CRGroupMemberSwitch {groupInfo :: GroupInfo, member :: GroupMember, switchProgress :: SwitchProgress} - | CRContactCode {contact :: Contact, connectionCode :: Text} - | CRGroupMemberCode {groupInfo :: GroupInfo, member :: GroupMember, connectionCode :: Text} - | CRConnectionVerified {verified :: Bool, expectedCode :: Text} - | CRNewChatItem {chatItem :: AChatItem} - | CRChatItemStatusUpdated {chatItem :: AChatItem} - | CRChatItemUpdated {chatItem :: AChatItem} - | CRChatItemDeleted {deletedChatItem :: AChatItem, toChatItem :: Maybe AChatItem, byUser :: Bool, timed :: Bool} - | CRChatItemDeletedNotFound {contact :: Contact, sharedMsgId :: SharedMsgId} - | CRBroadcastSent MsgContent Int ZonedTime - | CRMsgIntegrityError {msgError :: MsgErrorType} + | CRContactInfo {user :: User, contact :: Contact, connectionStats :: ConnectionStats, customUserProfile :: Maybe Profile} + | CRGroupMemberInfo {user :: User, groupInfo :: GroupInfo, member :: GroupMember, connectionStats_ :: Maybe ConnectionStats} + | CRContactSwitch {user :: User, contact :: Contact, switchProgress :: SwitchProgress} + | CRGroupMemberSwitch {user :: User, groupInfo :: GroupInfo, member :: GroupMember, switchProgress :: SwitchProgress} + | CRContactCode {user :: User, contact :: Contact, connectionCode :: Text} + | CRGroupMemberCode {user :: User, groupInfo :: GroupInfo, member :: GroupMember, connectionCode :: Text} + | CRConnectionVerified {user :: User, verified :: Bool, expectedCode :: Text} + | CRNewChatItem {user :: User, chatItem :: AChatItem} + | CRChatItemStatusUpdated {user :: User, chatItem :: AChatItem} + | CRChatItemUpdated {user :: User, chatItem :: AChatItem} + | CRChatItemDeleted {user :: User, deletedChatItem :: AChatItem, toChatItem :: Maybe AChatItem, byUser :: Bool, timed :: Bool} + | CRChatItemDeletedNotFound {user :: User, contact :: Contact, sharedMsgId :: SharedMsgId} + | CRBroadcastSent User MsgContent Int ZonedTime + | CRMsgIntegrityError {user :: User, msgError :: MsgErrorType} | CRCmdAccepted {corr :: CorrId} - | CRCmdOk + | CRCmdOk {user_ :: Maybe User} | CRChatHelp {helpSection :: HelpSection} | CRWelcome {user :: User} - | CRGroupCreated {groupInfo :: GroupInfo} - | CRGroupMembers {group :: Group} - | CRContactsList {contacts :: [Contact]} - | CRUserContactLink {contactLink :: UserContactLink} - | CRUserContactLinkUpdated {contactLink :: UserContactLink} - | CRContactRequestRejected {contactRequest :: UserContactRequest} - | CRUserAcceptedGroupSent {groupInfo :: GroupInfo, hostContact :: Maybe Contact} - | CRUserDeletedMember {groupInfo :: GroupInfo, member :: GroupMember} - | CRGroupsList {groups :: [GroupInfo]} - | CRSentGroupInvitation {groupInfo :: GroupInfo, contact :: Contact, member :: GroupMember} - | CRFileTransferStatus (FileTransfer, [Integer]) -- TODO refactor this type to FileTransferStatus - | CRUserProfile {profile :: Profile} - | CRUserProfileNoChange + | CRGroupCreated {user :: User, groupInfo :: GroupInfo} + | CRGroupMembers {user :: User, group :: Group} + | CRContactsList {user :: User, contacts :: [Contact]} + | CRUserContactLink {user :: User, contactLink :: UserContactLink} + | CRUserContactLinkUpdated {user :: User, contactLink :: UserContactLink} + | CRContactRequestRejected {user :: User, contactRequest :: UserContactRequest} + | CRUserAcceptedGroupSent {user :: User, groupInfo :: GroupInfo, hostContact :: Maybe Contact} + | CRUserDeletedMember {user :: User, groupInfo :: GroupInfo, member :: GroupMember} + | CRGroupsList {user :: User, groups :: [GroupInfo]} + | CRSentGroupInvitation {user :: User, groupInfo :: GroupInfo, contact :: Contact, member :: GroupMember} + | CRFileTransferStatus User (FileTransfer, [Integer]) -- TODO refactor this type to FileTransferStatus + | CRUserProfile {user :: User, profile :: Profile} + | CRUserProfileNoChange {user :: User} | CRVersionInfo {version :: String} - | CRInvitation {connReqInvitation :: ConnReqInvitation} - | CRSentConfirmation - | CRSentInvitation {customUserProfile :: Maybe Profile} - | CRContactUpdated {fromContact :: Contact, toContact :: Contact} - | CRContactsMerged {intoContact :: Contact, mergedContact :: Contact} - | CRContactDeleted {contact :: Contact} - | CRChatCleared {chatInfo :: AChatInfo} - | CRUserContactLinkCreated {connReqContact :: ConnReqContact} - | CRUserContactLinkDeleted - | CRReceivedContactRequest {contactRequest :: UserContactRequest} - | CRAcceptingContactRequest {contact :: Contact} - | CRContactAlreadyExists {contact :: Contact} - | CRContactRequestAlreadyAccepted {contact :: Contact} - | CRLeftMemberUser {groupInfo :: GroupInfo} - | CRGroupDeletedUser {groupInfo :: GroupInfo} - | CRRcvFileAccepted {chatItem :: AChatItem} - | CRRcvFileAcceptedSndCancelled {rcvFileTransfer :: RcvFileTransfer} - | CRRcvFileStart {chatItem :: AChatItem} - | CRRcvFileComplete {chatItem :: AChatItem} - | CRRcvFileCancelled {rcvFileTransfer :: RcvFileTransfer} - | CRRcvFileSndCancelled {rcvFileTransfer :: RcvFileTransfer} - | CRSndFileStart {chatItem :: AChatItem, sndFileTransfer :: SndFileTransfer} - | CRSndFileComplete {chatItem :: AChatItem, sndFileTransfer :: SndFileTransfer} + | CRInvitation {user :: User, connReqInvitation :: ConnReqInvitation} + | CRSentConfirmation {user :: User} + | CRSentInvitation {user :: User, customUserProfile :: Maybe Profile} + | CRContactUpdated {user :: User, fromContact :: Contact, toContact :: Contact} + | CRContactsMerged {user :: User, intoContact :: Contact, mergedContact :: Contact} + | CRContactDeleted {user :: User, contact :: Contact} + | CRChatCleared {user :: User, chatInfo :: AChatInfo} + | CRUserContactLinkCreated {user :: User, connReqContact :: ConnReqContact} + | CRUserContactLinkDeleted {user :: User} + | CRReceivedContactRequest {user :: User, contactRequest :: UserContactRequest} + | CRAcceptingContactRequest {user :: User, contact :: Contact} + | CRContactAlreadyExists {user :: User, contact :: Contact} + | CRContactRequestAlreadyAccepted {user :: User, contact :: Contact} + | CRLeftMemberUser {user :: User, groupInfo :: GroupInfo} + | CRGroupDeletedUser {user :: User, groupInfo :: GroupInfo} + | CRRcvFileAccepted {user :: User, chatItem :: AChatItem} + | CRRcvFileAcceptedSndCancelled {user :: User, rcvFileTransfer :: RcvFileTransfer} + | CRRcvFileStart {user :: User, chatItem :: AChatItem} + | CRRcvFileComplete {user :: User, chatItem :: AChatItem} + | CRRcvFileCancelled {user :: User, rcvFileTransfer :: RcvFileTransfer} + | CRRcvFileSndCancelled {user :: User, rcvFileTransfer :: RcvFileTransfer} + | CRSndFileStart {user :: User, chatItem :: AChatItem, sndFileTransfer :: SndFileTransfer} + | CRSndFileComplete {user :: User, chatItem :: AChatItem, sndFileTransfer :: SndFileTransfer} | CRSndFileCancelled {chatItem :: AChatItem, sndFileTransfer :: SndFileTransfer} - | CRSndFileRcvCancelled {chatItem :: AChatItem, sndFileTransfer :: SndFileTransfer} - | CRSndGroupFileCancelled {chatItem :: AChatItem, fileTransferMeta :: FileTransferMeta, sndFileTransfers :: [SndFileTransfer]} - | CRUserProfileUpdated {fromProfile :: Profile, toProfile :: Profile} - | CRContactAliasUpdated {toContact :: Contact} - | CRConnectionAliasUpdated {toConnection :: PendingContactConnection} - | CRContactPrefsUpdated {fromContact :: Contact, toContact :: Contact} - | CRContactConnecting {contact :: Contact} - | CRContactConnected {contact :: Contact, userCustomProfile :: Maybe Profile} - | CRContactAnotherClient {contact :: Contact} - | CRSubscriptionEnd {connectionEntity :: ConnectionEntity} - | CRContactsDisconnected {server :: SMPServer, contactRefs :: [ContactRef]} - | CRContactsSubscribed {server :: SMPServer, contactRefs :: [ContactRef]} + | CRSndFileRcvCancelled {user :: User, chatItem :: AChatItem, sndFileTransfer :: SndFileTransfer} + | CRSndGroupFileCancelled {user :: User, chatItem :: AChatItem, fileTransferMeta :: FileTransferMeta, sndFileTransfers :: [SndFileTransfer]} + | CRUserProfileUpdated {user :: User, fromProfile :: Profile, toProfile :: Profile} + | CRContactAliasUpdated {user :: User, toContact :: Contact} + | CRConnectionAliasUpdated {user :: User, toConnection :: PendingContactConnection} + | CRContactPrefsUpdated {user :: User, fromContact :: Contact, toContact :: Contact} + | CRContactConnecting {user :: User, contact :: Contact} + | CRContactConnected {user :: User, contact :: Contact, userCustomProfile :: Maybe Profile} + | CRContactAnotherClient {user :: User, contact :: Contact} + | CRSubscriptionEnd {user :: User, connectionEntity :: ConnectionEntity} + | CRContactsDisconnected {user :: User, server :: SMPServer, contactRefs :: [ContactRef]} + | CRContactsSubscribed {user :: User, server :: SMPServer, contactRefs :: [ContactRef]} | CRContactSubError {contact :: Contact, chatError :: ChatError} | CRContactSubSummary {contactSubscriptions :: [ContactSubStatus]} | CRUserContactSubSummary {userContactSubscriptions :: [UserContactSubStatus]} | CRHostConnected {protocol :: AProtocolType, transportHost :: TransportHost} | CRHostDisconnected {protocol :: AProtocolType, transportHost :: TransportHost} | CRGroupInvitation {groupInfo :: GroupInfo} - | CRReceivedGroupInvitation {groupInfo :: GroupInfo, contact :: Contact, memberRole :: GroupMemberRole} - | CRUserJoinedGroup {groupInfo :: GroupInfo, hostMember :: GroupMember} - | CRJoinedGroupMember {groupInfo :: GroupInfo, member :: GroupMember} - | CRJoinedGroupMemberConnecting {groupInfo :: GroupInfo, hostMember :: GroupMember, member :: GroupMember} - | CRMemberRole {groupInfo :: GroupInfo, byMember :: GroupMember, member :: GroupMember, fromRole :: GroupMemberRole, toRole :: GroupMemberRole} - | CRMemberRoleUser {groupInfo :: GroupInfo, member :: GroupMember, fromRole :: GroupMemberRole, toRole :: GroupMemberRole} - | CRConnectedToGroupMember {groupInfo :: GroupInfo, member :: GroupMember} - | CRDeletedMember {groupInfo :: GroupInfo, byMember :: GroupMember, deletedMember :: GroupMember} - | CRDeletedMemberUser {groupInfo :: GroupInfo, member :: GroupMember} - | CRLeftMember {groupInfo :: GroupInfo, member :: GroupMember} + | CRReceivedGroupInvitation {user :: User, groupInfo :: GroupInfo, contact :: Contact, memberRole :: GroupMemberRole} + | CRUserJoinedGroup {user :: User, groupInfo :: GroupInfo, hostMember :: GroupMember} + | CRJoinedGroupMember {user :: User, groupInfo :: GroupInfo, member :: GroupMember} + | CRJoinedGroupMemberConnecting {user :: User, groupInfo :: GroupInfo, hostMember :: GroupMember, member :: GroupMember} + | CRMemberRole {user :: User, groupInfo :: GroupInfo, byMember :: GroupMember, member :: GroupMember, fromRole :: GroupMemberRole, toRole :: GroupMemberRole} + | CRMemberRoleUser {user :: User, groupInfo :: GroupInfo, member :: GroupMember, fromRole :: GroupMemberRole, toRole :: GroupMemberRole} + | CRConnectedToGroupMember {user :: User, groupInfo :: GroupInfo, member :: GroupMember} + | CRDeletedMember {user :: User, groupInfo :: GroupInfo, byMember :: GroupMember, deletedMember :: GroupMember} + | CRDeletedMemberUser {user :: User, groupInfo :: GroupInfo, member :: GroupMember} + | CRLeftMember {user :: User, groupInfo :: GroupInfo, member :: GroupMember} | CRGroupEmpty {groupInfo :: GroupInfo} | CRGroupRemoved {groupInfo :: GroupInfo} - | CRGroupDeleted {groupInfo :: GroupInfo, member :: GroupMember} - | CRGroupUpdated {fromGroup :: GroupInfo, toGroup :: GroupInfo, member_ :: Maybe GroupMember} - | CRGroupProfile {groupInfo :: GroupInfo} - | CRGroupLinkCreated {groupInfo :: GroupInfo, connReqContact :: ConnReqContact} - | CRGroupLink {groupInfo :: GroupInfo, connReqContact :: ConnReqContact} - | CRGroupLinkDeleted {groupInfo :: GroupInfo} - | CRAcceptingGroupJoinRequest {groupInfo :: GroupInfo, contact :: Contact} + | CRGroupDeleted {user :: User, groupInfo :: GroupInfo, member :: GroupMember} + | CRGroupUpdated {user :: User, fromGroup :: GroupInfo, toGroup :: GroupInfo, member_ :: Maybe GroupMember} + | CRGroupProfile {user :: User, groupInfo :: GroupInfo} + | CRGroupLinkCreated {user :: User, groupInfo :: GroupInfo, connReqContact :: ConnReqContact} + | CRGroupLink {user :: User, groupInfo :: GroupInfo, connReqContact :: ConnReqContact} + | CRGroupLinkDeleted {user :: User, groupInfo :: GroupInfo} + | CRAcceptingGroupJoinRequest {user :: User, groupInfo :: GroupInfo, contact :: Contact} | CRMemberSubError {groupInfo :: GroupInfo, member :: GroupMember, chatError :: ChatError} | CRMemberSubSummary {memberSubscriptions :: [MemberSubStatus]} | CRGroupSubscribed {groupInfo :: GroupInfo} | CRPendingSubSummary {pendingSubscriptions :: [PendingSubStatus]} | CRSndFileSubError {sndFileTransfer :: SndFileTransfer, chatError :: ChatError} | CRRcvFileSubError {rcvFileTransfer :: RcvFileTransfer, chatError :: ChatError} - | CRCallInvitation {callInvitation :: RcvCallInvitation} - | CRCallOffer {contact :: Contact, callType :: CallType, offer :: WebRTCSession, sharedKey :: Maybe C.Key, askConfirmation :: Bool} - | CRCallAnswer {contact :: Contact, answer :: WebRTCSession} - | CRCallExtraInfo {contact :: Contact, extraInfo :: WebRTCExtraInfo} - | CRCallEnded {contact :: Contact} - | CRCallInvitations {callInvitations :: [RcvCallInvitation]} + | CRCallInvitation {user :: User, callInvitation :: RcvCallInvitation} + | CRCallOffer {user :: User, contact :: Contact, callType :: CallType, offer :: WebRTCSession, sharedKey :: Maybe C.Key, askConfirmation :: Bool} + | CRCallAnswer {user :: User, contact :: Contact, answer :: WebRTCSession} + | CRCallExtraInfo {user :: User, contact :: Contact, extraInfo :: WebRTCExtraInfo} + | CRCallEnded {user :: User, contact :: Contact} + | CRCallInvitations {user :: User, callInvitations :: [RcvCallInvitation]} | CRUserContactLinkSubscribed | CRUserContactLinkSubError {chatError :: ChatError} | CRNtfTokenStatus {status :: NtfTknStatus} | CRNtfToken {token :: DeviceToken, status :: NtfTknStatus, ntfMode :: NotificationsMode} - | CRNtfMessages {connEntity :: Maybe ConnectionEntity, msgTs :: Maybe UTCTime, ntfMessages :: [NtfMsgInfo]} - | CRNewContactConnection {connection :: PendingContactConnection} - | CRContactConnectionDeleted {connection :: PendingContactConnection} + | CRNtfMessages {user :: User, connEntity :: Maybe ConnectionEntity, msgTs :: Maybe UTCTime, ntfMessages :: [NtfMsgInfo]} + | CRNewContactConnection {user :: User, connection :: PendingContactConnection} + | CRContactConnectionDeleted {user :: User, connection :: PendingContactConnection} | CRSQLResult {rows :: [Text]} | CRDebugLocks {chatLockName :: Maybe String, agentLocks :: AgentLocks} | CRAgentStats {agentStats :: [[String]]} - | CRMessageError {severity :: Text, errorMessage :: Text} - | CRChatCmdError {chatError :: ChatError} - | CRChatError {chatError :: ChatError} + | CRMessageError {user :: User, severity :: Text, errorMessage :: Text} + | CRChatCmdError {user_ :: Maybe User, chatError :: ChatError} + | CRChatError {user_ :: Maybe User, chatError :: ChatError} deriving (Show, Generic) instance ToJSON ChatResponse where @@ -551,7 +557,8 @@ instance ToJSON ChatError where data ChatErrorType = CENoActiveUser - | CEActiveUserExists + | CENoConnectionUser {agentConnId :: AgentConnId} + | CEActiveUserExists -- TODO delete | CEChatNotStarted | CEChatNotStopped | CEChatStoreChanged @@ -627,8 +634,8 @@ throwDBError = throwError . ChatErrorDatabase type ChatMonad m = (MonadUnliftIO m, MonadReader ChatController m, MonadError ChatError m) -chatCmdError :: String -> ChatResponse -chatCmdError = CRChatCmdError . ChatError . CECommandError +chatCmdError :: Maybe User -> String -> ChatResponse +chatCmdError user = CRChatCmdError user . ChatError . CECommandError setActive :: (MonadUnliftIO m, MonadReader ChatController m) => ActiveTo -> m () setActive to = asks activeTo >>= atomically . (`writeTVar` to) diff --git a/src/Simplex/Chat/Store.hs b/src/Simplex/Chat/Store.hs index 796ba0053..e00722236 100644 --- a/src/Simplex/Chat/Store.hs +++ b/src/Simplex/Chat/Store.hs @@ -28,6 +28,9 @@ module Simplex.Chat.Store createUser, getUsers, setActiveUser, + getSetActiveUser, + getUserIdByName, + getUserByAConnId, createDirectConnection, createConnReqConnection, getProfileById, @@ -440,15 +443,16 @@ createUser db Profile {displayName, fullName, image, preferences = userPreferenc getUsers :: DB.Connection -> IO [User] getUsers db = - map toUser - <$> DB.query_ - db - [sql| - SELECT u.user_id, u.contact_id, p.contact_profile_id, u.active_user, u.local_display_name, p.full_name, p.image, p.preferences - FROM users u - JOIN contacts c ON u.contact_id = c.contact_id - JOIN contact_profiles p ON c.contact_profile_id = p.contact_profile_id - |] + map toUser <$> DB.query_ db userQuery + +userQuery :: Query +userQuery = + [sql| + SELECT u.user_id, u.contact_id, cp.contact_profile_id, u.active_user, u.local_display_name, cp.full_name, cp.image, cp.preferences + FROM users u + JOIN contacts ct ON ct.contact_id = u.contact_id + JOIN contact_profiles cp ON cp.contact_profile_id = ct.contact_profile_id + |] toUser :: (UserId, ContactId, ProfileId, Bool, ContactName, Text, Maybe ImageData, Maybe Preferences) -> User toUser (userId, userContactId, profileId, activeUser, displayName, fullName, image, userPreferences) = @@ -460,6 +464,26 @@ setActiveUser db userId = do DB.execute_ db "UPDATE users SET active_user = 0" DB.execute db "UPDATE users SET active_user = 1 WHERE user_id = ?" (Only userId) +getSetActiveUser :: DB.Connection -> UserId -> ExceptT StoreError IO User +getSetActiveUser db userId = do + liftIO $ setActiveUser db userId + getUser_ db userId + +getUser_ :: DB.Connection -> UserId -> ExceptT StoreError IO User +getUser_ db userId = + ExceptT . firstRow toUser (SEUserNotFound userId) $ + DB.query db (userQuery <> " WHERE u.user_id = ?") (Only userId) + +getUserIdByName :: DB.Connection -> UserName -> ExceptT StoreError IO Int64 +getUserIdByName db uName = + ExceptT . firstRow fromOnly (SEUserNotFoundByName uName) $ + DB.query db "SELECT user_id FROM users WHERE local_display_name = ?" (Only uName) + +getUserByAConnId :: DB.Connection -> AgentConnId -> IO (Maybe User) +getUserByAConnId db agentConnId = + maybeFirstRow toUser $ + DB.query db (userQuery <> " JOIN connections c ON c.user_id = u.user_id WHERE c.agent_conn_id = ?") (Only agentConnId) + createConnReqConnection :: DB.Connection -> UserId -> ConnId -> ConnReqUriHash -> XContactId -> Maybe Profile -> Maybe GroupLinkId -> IO PendingContactConnection createConnReqConnection db userId acId cReqHash xContactId incognitoProfile groupLinkId = do createdAt <- getCurrentTime @@ -4803,7 +4827,9 @@ randomBytes gVar = atomically . stateTVar gVar . randomBytesGenerate -- These error type constructors must be added to mobile apps data StoreError = SEDuplicateName - | SEContactNotFound {contactId :: Int64} + | SEUserNotFound {userId :: UserId} + | SEUserNotFoundByName {contactName :: ContactName} + | SEContactNotFound {contactId :: ContactId} | SEContactNotFoundByName {contactName :: ContactName} | SEContactNotReady {contactName :: ContactName} | SEDuplicateContactLink diff --git a/src/Simplex/Chat/Terminal/Input.hs b/src/Simplex/Chat/Terminal/Input.hs index e5b531762..1ea3dc0cd 100644 --- a/src/Simplex/Chat/Terminal/Input.hs +++ b/src/Simplex/Chat/Terminal/Input.hs @@ -43,7 +43,8 @@ runInputLoop ct@ChatTerminal {termState, liveMessageState} cc = forever $ do unless (isMessage cmd) $ echo s r <- runReaderT (execChatCommand bs) cc case r of - CRChatCmdError _ -> when (isMessage cmd) $ echo s + CRChatCmdError _ _ -> when (isMessage cmd) $ echo s + CRChatError _ _ -> when (isMessage cmd) $ echo s _ -> pure () printRespToTerminal ct cc False r startLiveMessage cmd r @@ -58,7 +59,7 @@ runInputLoop ct@ChatTerminal {termState, liveMessageState} cc = forever $ do Right SendMessageBroadcast {} -> True _ -> False startLiveMessage :: Either a ChatCommand -> ChatResponse -> IO () - startLiveMessage (Right (SendLiveMessage chatName msg)) (CRNewChatItem (AChatItem cType SMDSnd _ ChatItem {meta = CIMeta {itemId}})) = do + startLiveMessage (Right (SendLiveMessage chatName msg)) (CRNewChatItem _ (AChatItem cType SMDSnd _ ChatItem {meta = CIMeta {itemId}})) = do whenM (isNothing <$> readTVarIO liveMessageState) $ do let s = T.unpack $ safeDecodeUtf8 msg int = case cType of SCTGroup -> 5000000; _ -> 3000000 :: Int @@ -111,7 +112,7 @@ sendUpdatedLiveMessage :: ChatController -> String -> LiveMessage -> Bool -> IO sendUpdatedLiveMessage cc sentMsg LiveMessage {chatName, chatItemId} live = do let bs = encodeUtf8 $ T.pack sentMsg cmd = UpdateLiveMessage chatName chatItemId live bs - either CRChatCmdError id <$> runExceptT (processChatCommand cmd) `runReaderT` cc + either (CRChatCmdError Nothing) id <$> runExceptT (processChatCommand cmd) `runReaderT` cc runTerminalInput :: ChatTerminal -> ChatController -> IO () runTerminalInput ct cc = withChatTerm ct $ do diff --git a/src/Simplex/Chat/Terminal/Output.hs b/src/Simplex/Chat/Terminal/Output.hs index 854bc3898..32f2aa91c 100644 --- a/src/Simplex/Chat/Terminal/Output.hs +++ b/src/Simplex/Chat/Terminal/Output.hs @@ -95,8 +95,8 @@ runTerminalOutput ct cc@ChatController {outputQ, showLiveItems} = do forever $ do (_, r) <- atomically $ readTBQueue outputQ case r of - CRNewChatItem ci -> markChatItemRead ci - CRChatItemUpdated ci -> markChatItemRead ci + CRNewChatItem _ ci -> markChatItemRead ci + CRChatItemUpdated _ ci -> markChatItemRead ci _ -> pure () liveItems <- readTVarIO showLiveItems printRespToTerminal ct cc liveItems r diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index 30ede6ee8..d89a78029 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -216,6 +216,8 @@ instance ToJSON ConnReqUriHash where data ContactOrRequest = CORContact Contact | CORRequest UserContactRequest +type UserName = Text + type ContactName = Text type GroupName = Text diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index b4cab70b7..ef7dac841 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -59,35 +59,36 @@ serializeChatResponse user_ ts = unlines . map unStyle . responseToView user_ Fa responseToView :: Maybe User -> Bool -> Bool -> CurrentTime -> ChatResponse -> [StyledString] responseToView user_ testView liveItems ts = \case CRActiveUser User {profile} -> viewUserProfile $ fromLocalProfile profile + CRUsersList users -> viewUsersList users CRChatStarted -> ["chat started"] CRChatRunning -> ["chat is running"] CRChatStopped -> ["chat stopped"] CRChatSuspended -> ["chat suspended"] - CRApiChats chats -> if testView then testViewChats chats else [plain . bshow $ J.encode chats] - CRApiChat chat -> if testView then testViewChat chat else [plain . bshow $ J.encode chat] + CRApiChats _u chats -> if testView then testViewChats chats else [plain . bshow $ J.encode chats] + CRApiChat _u chat -> if testView then testViewChat chat else [plain . bshow $ J.encode chat] CRApiParsedMarkdown ft -> [plain . bshow $ J.encode ft] - CRUserSMPServers smpServers _ -> viewSMPServers (L.toList smpServers) testView + CRUserSMPServers _u smpServers _ -> viewSMPServers (L.toList smpServers) testView CRSmpTestResult testFailure -> viewSMPTestResult testFailure - CRChatItemTTL ttl -> viewChatItemTTL ttl + CRChatItemTTL _u ttl -> viewChatItemTTL ttl CRNetworkConfig cfg -> viewNetworkConfig cfg - CRContactInfo ct cStats customUserProfile -> viewContactInfo ct cStats customUserProfile - CRGroupMemberInfo g m cStats -> viewGroupMemberInfo g m cStats - CRContactSwitch ct progress -> viewContactSwitch ct progress - CRGroupMemberSwitch g m progress -> viewGroupMemberSwitch g m progress - CRConnectionVerified verified code -> [plain $ if verified then "connection verified" else "connection not verified, current code is " <> code] - CRContactCode ct code -> viewContactCode ct code testView - CRGroupMemberCode g m code -> viewGroupMemberCode g m code testView - CRNewChatItem (AChatItem _ _ chat item) -> unmuted chat item $ viewChatItem chat item False ts - CRChatItems chatItems -> concatMap (\(AChatItem _ _ chat item) -> viewChatItem chat item True ts) chatItems - CRChatItemId itemId -> [plain $ maybe "no item" show itemId] - CRChatItemStatusUpdated _ -> [] - CRChatItemUpdated (AChatItem _ _ chat item) -> unmuted chat item $ viewItemUpdate chat item liveItems ts - CRChatItemDeleted (AChatItem _ _ chat deletedItem) toItem byUser timed -> unmuted chat deletedItem $ viewItemDelete chat deletedItem (isJust toItem) byUser timed ts - CRChatItemDeletedNotFound Contact {localDisplayName = c} _ -> [ttyFrom $ c <> "> [deleted - original message not found]"] - CRBroadcastSent mc n t -> viewSentBroadcast mc n ts t - CRMsgIntegrityError mErr -> viewMsgIntegrityError mErr + CRContactInfo _u ct cStats customUserProfile -> viewContactInfo ct cStats customUserProfile + CRGroupMemberInfo _u g m cStats -> viewGroupMemberInfo g m cStats + CRContactSwitch _u ct progress -> viewContactSwitch ct progress + CRGroupMemberSwitch _u g m progress -> viewGroupMemberSwitch g m progress + CRConnectionVerified _u verified code -> [plain $ if verified then "connection verified" else "connection not verified, current code is " <> code] + CRContactCode _u ct code -> viewContactCode ct code testView + CRGroupMemberCode _u g m code -> viewGroupMemberCode g m code testView + CRNewChatItem _u (AChatItem _ _ chat item) -> unmuted chat item $ viewChatItem chat item False ts + CRChatItems _u chatItems -> concatMap (\(AChatItem _ _ chat item) -> viewChatItem chat item True ts) chatItems + CRChatItemId _u itemId -> [plain $ maybe "no item" show itemId] + CRChatItemStatusUpdated _u _ -> [] + CRChatItemUpdated _u (AChatItem _ _ chat item) -> unmuted chat item $ viewItemUpdate chat item liveItems ts + CRChatItemDeleted _u (AChatItem _ _ chat deletedItem) toItem byUser timed -> unmuted chat deletedItem $ viewItemDelete chat deletedItem (isJust toItem) byUser timed ts + CRChatItemDeletedNotFound _u Contact {localDisplayName = c} _ -> [ttyFrom $ c <> "> [deleted - original message not found]"] + CRBroadcastSent _u mc n t -> viewSentBroadcast mc n ts t + CRMsgIntegrityError _u mErr -> viewMsgIntegrityError mErr CRCmdAccepted _ -> [] - CRCmdOk -> ["ok"] + CRCmdOk _u -> ["ok"] CRChatHelp section -> case section of HSMain -> chatHelpInfo HSFiles -> filesHelpInfo @@ -97,65 +98,64 @@ responseToView user_ testView liveItems ts = \case HSMarkdown -> markdownInfo HSSettings -> settingsInfo CRWelcome user -> chatWelcome user - CRContactsList cs -> viewContactsList cs - CRUserContactLink UserContactLink {connReqContact, autoAccept} -> connReqContact_ "Your chat address:" connReqContact <> autoAcceptStatus_ autoAccept - CRUserContactLinkUpdated UserContactLink {autoAccept} -> autoAcceptStatus_ autoAccept - CRContactRequestRejected UserContactRequest {localDisplayName = c} -> [ttyContact c <> ": contact request rejected"] - CRGroupCreated g -> viewGroupCreated g - CRGroupMembers g -> viewGroupMembers g - CRGroupsList gs -> viewGroupsList gs - CRSentGroupInvitation g c _ -> + CRContactsList _u cs -> viewContactsList cs + CRUserContactLink _u UserContactLink {connReqContact, autoAccept} -> connReqContact_ "Your chat address:" connReqContact <> autoAcceptStatus_ autoAccept + CRUserContactLinkUpdated _u UserContactLink {autoAccept} -> autoAcceptStatus_ autoAccept + CRContactRequestRejected _u UserContactRequest {localDisplayName = c} -> [ttyContact c <> ": contact request rejected"] + CRGroupCreated _u g -> viewGroupCreated g + CRGroupMembers _u g -> viewGroupMembers g + CRGroupsList _u gs -> viewGroupsList gs + CRSentGroupInvitation _u g c _ -> if viaGroupLink . contactConn $ c then [ttyContact' c <> " invited to group " <> ttyGroup' g <> " via your group link"] else ["invitation to join the group " <> ttyGroup' g <> " sent to " <> ttyContact' c] - CRFileTransferStatus ftStatus -> viewFileTransferStatus ftStatus - CRUserProfile p -> viewUserProfile p - CRUserProfileNoChange -> ["user profile did not change"] + CRFileTransferStatus _u ftStatus -> viewFileTransferStatus ftStatus + CRUserProfile _u p -> viewUserProfile p + CRUserProfileNoChange _u -> ["user profile did not change"] CRVersionInfo _ -> [plain versionStr, plain updateStr] - CRChatCmdError e -> viewChatError e - CRInvitation cReq -> viewConnReqInvitation cReq - CRSentConfirmation -> ["confirmation sent!"] - CRSentInvitation customUserProfile -> viewSentInvitation customUserProfile testView - CRContactDeleted c -> [ttyContact' c <> ": contact is deleted"] - CRChatCleared chatInfo -> viewChatCleared chatInfo - CRAcceptingContactRequest c -> [ttyFullContact c <> ": accepting contact request..."] - CRContactAlreadyExists c -> [ttyFullContact c <> ": contact already exists"] - CRContactRequestAlreadyAccepted c -> [ttyFullContact c <> ": sent you a duplicate contact request, but you are already connected, no action needed"] - CRUserContactLinkCreated cReq -> connReqContact_ "Your new chat address is created!" cReq - CRUserContactLinkDeleted -> viewUserContactLinkDeleted - CRUserAcceptedGroupSent _g _ -> [] -- [ttyGroup' g <> ": joining the group..."] - CRUserDeletedMember g m -> [ttyGroup' g <> ": you removed " <> ttyMember m <> " from the group"] - CRLeftMemberUser g -> [ttyGroup' g <> ": you left the group"] <> groupPreserved g - CRGroupDeletedUser g -> [ttyGroup' g <> ": you deleted the group"] - CRRcvFileAccepted ci -> savingFile' ci - CRRcvFileAcceptedSndCancelled ft -> viewRcvFileSndCancelled ft - CRSndGroupFileCancelled _ ftm fts -> viewSndGroupFileCancelled ftm fts - CRRcvFileCancelled ft -> receivingFile_ "cancelled" ft - CRUserProfileUpdated p p' -> viewUserProfileUpdated p p' - CRContactPrefsUpdated {fromContact, toContact} -> case user_ of + CRInvitation _u cReq -> viewConnReqInvitation cReq + CRSentConfirmation _u -> ["confirmation sent!"] + CRSentInvitation _u customUserProfile -> viewSentInvitation customUserProfile testView + CRContactDeleted _u c -> [ttyContact' c <> ": contact is deleted"] + CRChatCleared _u chatInfo -> viewChatCleared chatInfo + CRAcceptingContactRequest _u c -> [ttyFullContact c <> ": accepting contact request..."] + CRContactAlreadyExists _u c -> [ttyFullContact c <> ": contact already exists"] + CRContactRequestAlreadyAccepted _u c -> [ttyFullContact c <> ": sent you a duplicate contact request, but you are already connected, no action needed"] + CRUserContactLinkCreated _u cReq -> connReqContact_ "Your new chat address is created!" cReq + CRUserContactLinkDeleted _u -> viewUserContactLinkDeleted + CRUserAcceptedGroupSent _u _g _ -> [] -- [ttyGroup' g <> ": joining the group..."] + CRUserDeletedMember _u g m -> [ttyGroup' g <> ": you removed " <> ttyMember m <> " from the group"] + CRLeftMemberUser _u g -> [ttyGroup' g <> ": you left the group"] <> groupPreserved g + CRGroupDeletedUser _u g -> [ttyGroup' g <> ": you deleted the group"] + CRRcvFileAccepted _u ci -> savingFile' ci + CRRcvFileAcceptedSndCancelled _u ft -> viewRcvFileSndCancelled ft + CRSndGroupFileCancelled _u _ ftm fts -> viewSndGroupFileCancelled ftm fts + CRRcvFileCancelled _u ft -> receivingFile_ "cancelled" ft + CRUserProfileUpdated _u p p' -> viewUserProfileUpdated p p' + CRContactPrefsUpdated {user = _u, fromContact, toContact} -> case user_ of Just user -> viewUserContactPrefsUpdated user fromContact toContact _ -> ["unexpected chat event CRContactPrefsUpdated without current user"] - CRContactAliasUpdated c -> viewContactAliasUpdated c - CRConnectionAliasUpdated c -> viewConnectionAliasUpdated c - CRContactUpdated {fromContact = c, toContact = c'} -> case user_ of + CRContactAliasUpdated _u c -> viewContactAliasUpdated c + CRConnectionAliasUpdated _u c -> viewConnectionAliasUpdated c + CRContactUpdated {user = _u, fromContact = c, toContact = c'} -> case user_ of Just user -> viewContactUpdated c c' <> viewContactPrefsUpdated user c c' _ -> ["unexpected chat event CRContactUpdated without current user"] - CRContactsMerged intoCt mergedCt -> viewContactsMerged intoCt mergedCt - CRReceivedContactRequest UserContactRequest {localDisplayName = c, profile} -> viewReceivedContactRequest c profile - CRRcvFileStart ci -> receivingFile_' "started" ci - CRRcvFileComplete ci -> receivingFile_' "completed" ci - CRRcvFileSndCancelled ft -> viewRcvFileSndCancelled ft - CRSndFileStart _ ft -> sendingFile_ "started" ft - CRSndFileComplete _ ft -> sendingFile_ "completed" ft + CRContactsMerged _u intoCt mergedCt -> viewContactsMerged intoCt mergedCt + CRReceivedContactRequest _u UserContactRequest {localDisplayName = c, profile} -> viewReceivedContactRequest c profile + CRRcvFileStart _u ci -> receivingFile_' "started" ci + CRRcvFileComplete _u ci -> receivingFile_' "completed" ci + CRRcvFileSndCancelled _u ft -> viewRcvFileSndCancelled ft + CRSndFileStart _u _ ft -> sendingFile_ "started" ft + CRSndFileComplete _u _ ft -> sendingFile_ "completed" ft CRSndFileCancelled _ ft -> sendingFile_ "cancelled" ft - CRSndFileRcvCancelled _ ft@SndFileTransfer {recipientDisplayName = c} -> + CRSndFileRcvCancelled _u _ ft@SndFileTransfer {recipientDisplayName = c} -> [ttyContact c <> " cancelled receiving " <> sndFile ft] - CRContactConnecting _ -> [] - CRContactConnected ct userCustomProfile -> viewContactConnected ct userCustomProfile testView - CRContactAnotherClient c -> [ttyContact' c <> ": contact is connected to another client"] - CRSubscriptionEnd acEntity -> [sShow (connId (entityConnection acEntity :: Connection)) <> ": END"] - CRContactsDisconnected srv cs -> [plain $ "server disconnected " <> showSMPServer srv <> " (" <> contactList cs <> ")"] - CRContactsSubscribed srv cs -> [plain $ "server connected " <> showSMPServer srv <> " (" <> contactList cs <> ")"] + CRContactConnecting _u _ -> [] + CRContactConnected _u ct userCustomProfile -> viewContactConnected ct userCustomProfile testView + CRContactAnotherClient _u c -> [ttyContact' c <> ": contact is connected to another client"] + CRSubscriptionEnd _u acEntity -> [sShow (connId (entityConnection acEntity :: Connection)) <> ": END"] + CRContactsDisconnected _u srv cs -> [plain $ "server disconnected " <> showSMPServer srv <> " (" <> contactList cs <> ")"] + CRContactsSubscribed _u srv cs -> [plain $ "server connected " <> showSMPServer srv <> " (" <> contactList cs <> ")"] CRContactSubError c e -> [ttyContact' c <> ": contact error " <> sShow e] CRContactSubSummary summary -> [sShow (length subscribed) <> " contacts connected (use " <> highlight' "/cs" <> " for the list)" | not (null subscribed)] <> viewErrorsSummary errors " contact errors" @@ -169,27 +169,27 @@ responseToView user_ testView liveItems ts = \case 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 CRGroupInvitation g -> [groupInvitation' g] - CRReceivedGroupInvitation g c role -> viewReceivedGroupInvitation g c role - CRUserJoinedGroup g _ -> viewUserJoinedGroup g - CRJoinedGroupMember g m -> viewJoinedGroupMember g m + CRReceivedGroupInvitation _u g c role -> viewReceivedGroupInvitation g c role + CRUserJoinedGroup _u g _ -> viewUserJoinedGroup g + CRJoinedGroupMember _u g m -> viewJoinedGroupMember g m CRHostConnected p h -> [plain $ "connected to " <> viewHostEvent p h] CRHostDisconnected p h -> [plain $ "disconnected from " <> viewHostEvent p h] - CRJoinedGroupMemberConnecting g host m -> [ttyGroup' g <> ": " <> ttyMember host <> " added " <> ttyFullMember m <> " to the group (connecting...)"] - CRConnectedToGroupMember g m -> [ttyGroup' g <> ": " <> connectedMember m <> " is connected"] - CRMemberRole g by m r r' -> viewMemberRoleChanged g by m r r' - CRMemberRoleUser g m r r' -> viewMemberRoleUserChanged g m r r' - CRDeletedMemberUser g by -> [ttyGroup' g <> ": " <> ttyMember by <> " removed you from the group"] <> groupPreserved g - CRDeletedMember g by m -> [ttyGroup' g <> ": " <> ttyMember by <> " removed " <> ttyMember m <> " from the group"] - CRLeftMember g m -> [ttyGroup' g <> ": " <> ttyMember m <> " left the group"] + CRJoinedGroupMemberConnecting _u g host m -> [ttyGroup' g <> ": " <> ttyMember host <> " added " <> ttyFullMember m <> " to the group (connecting...)"] + CRConnectedToGroupMember _u g m -> [ttyGroup' g <> ": " <> connectedMember m <> " is connected"] + CRMemberRole _u g by m r r' -> viewMemberRoleChanged g by m r r' + CRMemberRoleUser _u g m r r' -> viewMemberRoleUserChanged g m r r' + CRDeletedMemberUser _u g by -> [ttyGroup' g <> ": " <> ttyMember by <> " removed you from the group"] <> groupPreserved g + CRDeletedMember _u g by m -> [ttyGroup' g <> ": " <> ttyMember by <> " removed " <> ttyMember m <> " from the group"] + CRLeftMember _u g m -> [ttyGroup' g <> ": " <> ttyMember m <> " left the group"] CRGroupEmpty g -> [ttyFullGroup g <> ": group is empty"] CRGroupRemoved g -> [ttyFullGroup g <> ": you are no longer a member or group deleted"] - CRGroupDeleted g m -> [ttyGroup' g <> ": " <> ttyMember m <> " deleted the group", "use " <> highlight ("/d #" <> groupName' g) <> " to delete the local copy of the group"] - CRGroupUpdated g g' m -> viewGroupUpdated g g' m - CRGroupProfile g -> viewGroupProfile g - CRGroupLinkCreated g cReq -> groupLink_ "Group link is created!" g cReq - CRGroupLink g cReq -> groupLink_ "Group link:" g cReq - CRGroupLinkDeleted g -> viewGroupLinkDeleted g - CRAcceptingGroupJoinRequest g c -> [ttyFullContact c <> ": accepting request to join group " <> ttyGroup' g <> "..."] + CRGroupDeleted _u g m -> [ttyGroup' g <> ": " <> ttyMember m <> " deleted the group", "use " <> highlight ("/d #" <> groupName' g) <> " to delete the local copy of the group"] + CRGroupUpdated _u g g' m -> viewGroupUpdated g g' m + CRGroupProfile _u g -> viewGroupProfile g + CRGroupLinkCreated _u g cReq -> groupLink_ "Group link is created!" g cReq + CRGroupLink _u g cReq -> groupLink_ "Group link:" g cReq + CRGroupLinkDeleted _u g -> viewGroupLinkDeleted g + CRAcceptingGroupJoinRequest _ g c -> [ttyFullContact c <> ": accepting request to join group " <> ttyGroup' g <> "..."] CRMemberSubError g m e -> [ttyGroup' g <> " member " <> ttyMember m <> " error: " <> sShow e] CRMemberSubSummary summary -> viewErrorsSummary (filter (isJust . memberError) summary) " group member errors" CRGroupSubscribed g -> viewGroupSubscribed g @@ -198,16 +198,16 @@ responseToView user_ testView liveItems ts = \case ["sent file " <> sShow fileId <> " (" <> plain fileName <> ") error: " <> sShow e] CRRcvFileSubError RcvFileTransfer {fileId, fileInvitation = FileInvitation {fileName}} e -> ["received file " <> sShow fileId <> " (" <> plain fileName <> ") error: " <> sShow e] - CRCallInvitation RcvCallInvitation {contact, callType, sharedKey} -> viewCallInvitation contact callType sharedKey - CRCallOffer {contact, callType, offer, sharedKey} -> viewCallOffer contact callType offer sharedKey - CRCallAnswer {contact, answer} -> viewCallAnswer contact answer - CRCallExtraInfo {contact} -> ["call extra info from " <> ttyContact' contact] - CRCallEnded {contact} -> ["call with " <> ttyContact' contact <> " ended"] - CRCallInvitations _ -> [] + CRCallInvitation _u RcvCallInvitation {contact, callType, sharedKey} -> viewCallInvitation contact callType sharedKey + CRCallOffer {user = _u, contact, callType, offer, sharedKey} -> viewCallOffer contact callType offer sharedKey + CRCallAnswer {user = _u, contact, answer} -> viewCallAnswer contact answer + CRCallExtraInfo {user = _u, contact} -> ["call extra info from " <> ttyContact' contact] + CRCallEnded {user = _u, contact} -> ["call with " <> ttyContact' contact <> " ended"] + CRCallInvitations _u _ -> [] CRUserContactLinkSubscribed -> ["Your address is active! To show: " <> highlight' "/sa"] CRUserContactLinkSubError e -> ["user address error: " <> sShow e, "to delete your address: " <> highlight' "/da"] - CRNewContactConnection _ -> [] - CRContactConnectionDeleted PendingContactConnection {pccConnId} -> ["connection :" <> sShow pccConnId <> " deleted"] + CRNewContactConnection _u _ -> [] + CRContactConnectionDeleted _u PendingContactConnection {pccConnId} -> ["connection :" <> sShow pccConnId <> " deleted"] CRNtfTokenStatus status -> ["device token status: " <> plain (smpEncode status)] CRNtfToken _ status mode -> ["device token status: " <> plain (smpEncode status) <> ", notifications mode: " <> plain (strEncode mode)] CRNtfMessages {} -> [] @@ -217,8 +217,9 @@ responseToView user_ testView liveItems ts = \case plain $ "agent locks: " <> LB.unpack (J.encode agentLocks) ] CRAgentStats stats -> map (plain . intercalate ",") stats - CRMessageError prefix err -> [plain prefix <> ": " <> plain err] - CRChatError e -> viewChatError e + CRMessageError _u prefix err -> [plain prefix <> ": " <> plain err] + CRChatCmdError _u e -> viewChatError e + CRChatError _u e -> viewChatError e where testViewChats :: [AChat] -> [StyledString] testViewChats chats = [sShow $ map toChatView chats] @@ -256,6 +257,13 @@ responseToView user_ testView liveItems ts = \case | muted chat chatItem = [] | otherwise = s +viewUsersList :: [User] -> [StyledString] +viewUsersList = + let ldn = T.toLower . (localDisplayName :: User -> ContactName) + in map (\user@User {profile = LocalProfile {displayName, fullName}} -> ttyFullName displayName fullName <> active user) . sortOn ldn + where + active User {activeUser} = if activeUser then highlight' " (active)" else "" + muted :: ChatInfo c -> ChatItem c d -> Bool muted chat ChatItem {chatDir} = case (chat, chatDir) of (DirectChat Contact {chatSettings = DisableNtfs}, CIDirectRcv) -> True @@ -1130,6 +1138,7 @@ viewChatError :: ChatError -> [StyledString] viewChatError = \case ChatError err -> case err of CENoActiveUser -> ["error: active user is required"] + CENoConnectionUser _agentConnId -> [] -- ["error: connection has no user, conn id: " <> sShow agentConnId] CEActiveUserExists -> ["error: active user already exists"] CEChatNotStarted -> ["error: chat not started"] CEChatNotStopped -> ["error: chat not stopped"] @@ -1179,6 +1188,7 @@ viewChatError = \case -- e -> ["chat error: " <> sShow e] ChatErrorStore err -> case err of SEDuplicateName -> ["this display name is already used by user, contact or group"] + SEUserNotFoundByName u -> ["no user " <> ttyContact u] SEContactNotFoundByName c -> ["no contact " <> ttyContact c] SEContactNotReady c -> ["contact " <> ttyContact c <> " is not active yet"] SEGroupNotFoundByName g -> ["no group " <> ttyGroup g] diff --git a/tests/MobileTests.hs b/tests/MobileTests.hs index 29c3f2d98..ada96f42b 100644 --- a/tests/MobileTests.hs +++ b/tests/MobileTests.hs @@ -25,9 +25,9 @@ noActiveUser = "{\"resp\":{\"type\":\"chatCmdError\",\"chatError\":{\"type\":\"e activeUserExists :: String #if defined(darwin_HOST_OS) && defined(swiftJSON) -activeUserExists = "{\"resp\":{\"chatCmdError\":{\"chatError\":{\"error\":{\"errorType\":{\"activeUserExists\":{}}}}}}}" +activeUserExists = "{\"resp\":{\"chatCmdError\":{\"chatError\":{\"errorStore\":{\"storeError\":{\"duplicateName\":{}}}}}}}" #else -activeUserExists = "{\"resp\":{\"type\":\"chatCmdError\",\"chatError\":{\"type\":\"error\",\"errorType\":{\"type\":\"activeUserExists\"}}}}" +activeUserExists = "{\"resp\":{\"type\":\"chatCmdError\",\"chatError\":{\"type\":\"errorStore\",\"storeError\":{\"type\":\"duplicateName\"}}}}" #endif activeUser :: String @@ -85,7 +85,7 @@ testChatApiNoUser = withTmpFiles $ do Left (DBMErrorNotADatabase _) <- chatMigrateInit testDBPrefix "myKey" chatSendCmd cc "/u" `shouldReturn` noActiveUser chatSendCmd cc "/_start" `shouldReturn` noActiveUser - chatSendCmd cc "/u alice Alice" `shouldReturn` activeUser + chatSendCmd cc "/create user alice Alice" `shouldReturn` activeUser chatSendCmd cc "/_start" `shouldReturn` chatStarted testChatApi :: IO () @@ -98,7 +98,7 @@ testChatApi = withTmpFiles $ do Left (DBMErrorNotADatabase _) <- chatMigrateInit dbPrefix "" Left (DBMErrorNotADatabase _) <- chatMigrateInit dbPrefix "anotherKey" chatSendCmd cc "/u" `shouldReturn` activeUser - chatSendCmd cc "/u alice Alice" `shouldReturn` activeUserExists + chatSendCmd cc "/create user alice Alice" `shouldReturn` activeUserExists chatSendCmd cc "/_start" `shouldReturn` chatStarted chatRecvMsg cc `shouldReturn` contactSubSummary chatRecvMsg cc `shouldReturn` userContactSubSummary