mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2024-12-17 17:20:21 +01:00
Merge branch 'master' into contacts-ui-feature-branch
This commit is contained in:
+240
-99
@@ -1,3 +1,4 @@
|
||||
{-# LANGUAGE BangPatterns #-}
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
@@ -93,7 +94,7 @@ import qualified Simplex.FileTransfer.Description as FD
|
||||
import Simplex.FileTransfer.Protocol (FileParty (..), FilePartyI)
|
||||
import qualified Simplex.FileTransfer.Transport as XFTP
|
||||
import Simplex.Messaging.Agent as Agent
|
||||
import Simplex.Messaging.Agent.Client (AgentStatsKey (..), SubInfo (..), agentClientStore, getAgentWorkersDetails, getAgentWorkersSummary, ipAddressProtected, temporaryAgentError, withLockMap)
|
||||
import Simplex.Messaging.Agent.Client (AgentStatsKey (..), SubInfo (..), agentClientStore, getAgentQueuesInfo, getAgentWorkersDetails, getAgentWorkersSummary, getNetworkConfig', ipAddressProtected, temporaryAgentError, withLockMap)
|
||||
import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..), createAgentStore, defaultAgentConfig)
|
||||
import Simplex.Messaging.Agent.Lock (withLock)
|
||||
import Simplex.Messaging.Agent.Protocol
|
||||
@@ -102,7 +103,7 @@ import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..), Migrati
|
||||
import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..))
|
||||
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
|
||||
import qualified Simplex.Messaging.Agent.Store.SQLite.Migrations as Migrations
|
||||
import Simplex.Messaging.Client (ProxyClientError (..), defaultNetworkConfig)
|
||||
import Simplex.Messaging.Client (ProxyClientError (..), NetworkConfig (..), defaultNetworkConfig)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..))
|
||||
import qualified Simplex.Messaging.Crypto.File as CF
|
||||
@@ -217,7 +218,7 @@ newChatController
|
||||
ChatDatabase {chatStore, agentStore}
|
||||
user
|
||||
cfg@ChatConfig {agentConfig = aCfg, defaultServers, inlineFiles, deviceNameForRemote}
|
||||
ChatOpts {coreOptions = CoreChatOpts {smpServers, xftpServers, networkConfig, logLevel, logConnections, logServerHosts, logFile, tbqSize, highlyAvailable}, deviceName, optFilesFolder, optTempDirectory, showReactions, allowInstantFiles, autoAcceptFileSize}
|
||||
ChatOpts {coreOptions = CoreChatOpts {smpServers, xftpServers, simpleNetCfg, logLevel, logConnections, logServerHosts, logFile, tbqSize, highlyAvailable}, deviceName, optFilesFolder, optTempDirectory, showReactions, allowInstantFiles, autoAcceptFileSize}
|
||||
backgroundMode = do
|
||||
let inlineFiles' = if allowInstantFiles || autoAcceptFileSize > 0 then inlineFiles else inlineFiles {sendChunks = 0, receiveInstant = False}
|
||||
config = cfg {logLevel, showReactions, tbqSize, subscriptionEvents = logConnections, hostEvents = logServerHosts, defaultServers = configServers, inlineFiles = inlineFiles', autoAcceptFileSize, highlyAvailable}
|
||||
@@ -228,6 +229,7 @@ newChatController
|
||||
smpAgent <- getSMPAgentClient aCfg {tbqSize} servers agentStore backgroundMode
|
||||
agentAsync <- newTVarIO Nothing
|
||||
random <- liftIO C.newRandom
|
||||
eventSeq <- newTVarIO 0
|
||||
inputQ <- newTBQueueIO tbqSize
|
||||
outputQ <- newTBQueueIO tbqSize
|
||||
connNetworkStatuses <- atomically TM.empty
|
||||
@@ -265,6 +267,7 @@ newChatController
|
||||
chatStore,
|
||||
chatStoreChanged,
|
||||
random,
|
||||
eventSeq,
|
||||
inputQ,
|
||||
outputQ,
|
||||
connNetworkStatuses,
|
||||
@@ -300,7 +303,7 @@ newChatController
|
||||
let DefaultAgentServers {smp = defSmp, xftp = defXftp} = defaultServers
|
||||
smp' = fromMaybe defSmp (nonEmpty smpServers)
|
||||
xftp' = fromMaybe defXftp (nonEmpty xftpServers)
|
||||
in defaultServers {smp = smp', xftp = xftp', netCfg = networkConfig}
|
||||
in defaultServers {smp = smp', xftp = xftp', netCfg = updateNetworkConfig defaultNetworkConfig simpleNetCfg}
|
||||
agentServers :: ChatConfig -> IO InitialAgentServers
|
||||
agentServers config@ChatConfig {defaultServers = defServers@DefaultAgentServers {ntf, netCfg}} = do
|
||||
users <- withTransaction chatStore getUsers
|
||||
@@ -318,6 +321,13 @@ newChatController
|
||||
userServers :: User -> IO (NonEmpty (ProtoServerWithAuth p))
|
||||
userServers user' = activeAgentServers config protocol <$> withTransaction chatStore (`getProtocolServers` user')
|
||||
|
||||
updateNetworkConfig :: NetworkConfig -> SimpleNetCfg -> NetworkConfig
|
||||
updateNetworkConfig cfg SimpleNetCfg {socksProxy, smpProxyMode_, smpProxyFallback_, tcpTimeout_, logTLSErrors} =
|
||||
let cfg1 = maybe cfg (\smpProxyMode -> cfg {smpProxyMode}) smpProxyMode_
|
||||
cfg2 = maybe cfg1 (\smpProxyFallback -> cfg1 {smpProxyFallback}) smpProxyFallback_
|
||||
cfg3 = maybe cfg2 (\tcpTimeout -> cfg2 {tcpTimeout, tcpConnectTimeout = (tcpTimeout * 3) `div` 2}) tcpTimeout_
|
||||
in cfg3 {socksProxy, logTLSErrors}
|
||||
|
||||
withChatLock :: String -> CM a -> CM a
|
||||
withChatLock name action = asks chatLock >>= \l -> withLock l name action
|
||||
|
||||
@@ -1339,7 +1349,11 @@ processChatCommand' vr = \case
|
||||
processChatCommand $ APIGetChatItemTTL userId
|
||||
APISetNetworkConfig cfg -> withUser' $ \_ -> lift (withAgent' (`setNetworkConfig` cfg)) >> ok_
|
||||
APIGetNetworkConfig -> withUser' $ \_ ->
|
||||
lift $ CRNetworkConfig <$> withAgent' getNetworkConfig
|
||||
CRNetworkConfig <$> lift getNetworkConfig
|
||||
SetNetworkConfig netCfg -> do
|
||||
cfg <- lift getNetworkConfig
|
||||
void . processChatCommand $ APISetNetworkConfig $ updateNetworkConfig cfg netCfg
|
||||
pure $ CRNetworkConfig cfg
|
||||
APISetNetworkInfo info -> lift (withAgent' (`setUserNetworkInfo` info)) >> ok_
|
||||
ReconnectAllServers -> withUser' $ \_ -> lift (withAgent' reconnectAllServers) >> ok_
|
||||
APISetChatSettings (ChatRef cType chatId) chatSettings -> withUser $ \user -> case cType of
|
||||
@@ -1376,6 +1390,11 @@ processChatCommand' vr = \case
|
||||
forM customUserProfileId $ \profileId -> withStore (\db -> getProfileById db userId profileId)
|
||||
connectionStats <- mapM (withAgent . flip getConnectionServers) (contactConnId ct)
|
||||
pure $ CRContactInfo user ct connectionStats (fmap fromLocalProfile incognitoProfile)
|
||||
APIContactQueueInfo contactId -> withUser $ \user -> do
|
||||
ct@Contact {activeConn} <- withStore $ \db -> getContact db vr user contactId
|
||||
case activeConn of
|
||||
Just conn -> getConnQueueInfo user conn
|
||||
Nothing -> throwChatError $ CEContactNotActive ct
|
||||
APIGroupInfo gId -> withUser $ \user -> do
|
||||
(g, s) <- withStore $ \db -> (,) <$> getGroupInfo db vr user gId <*> liftIO (getGroupSummary db user gId)
|
||||
pure $ CRGroupInfo user g s
|
||||
@@ -1383,6 +1402,11 @@ processChatCommand' vr = \case
|
||||
(g, m) <- withStore $ \db -> (,) <$> getGroupInfo db vr user gId <*> getGroupMember db vr user gId gMemberId
|
||||
connectionStats <- mapM (withAgent . flip getConnectionServers) (memberConnId m)
|
||||
pure $ CRGroupMemberInfo user g m connectionStats
|
||||
APIGroupMemberQueueInfo gId gMemberId -> withUser $ \user -> do
|
||||
GroupMember {activeConn} <- withStore $ \db -> getGroupMember db vr user gId gMemberId
|
||||
case activeConn of
|
||||
Just conn -> getConnQueueInfo user conn
|
||||
Nothing -> throwChatError CEGroupMemberNotActive
|
||||
APISwitchContact contactId -> withUser $ \user -> do
|
||||
ct <- withStore $ \db -> getContact db vr user contactId
|
||||
case contactConnId ct of
|
||||
@@ -1469,14 +1493,14 @@ processChatCommand' vr = \case
|
||||
ct@Contact {activeConn} <- withStore $ \db -> getContact db vr user contactId
|
||||
case activeConn of
|
||||
Just conn -> do
|
||||
withStore' $ \db -> setConnectionAuthErrCounter db user conn 0
|
||||
withStore' $ \db -> setAuthErrCounter db user conn 0
|
||||
ok user
|
||||
Nothing -> throwChatError $ CEContactNotActive ct
|
||||
APIEnableGroupMember gId gMemberId -> withUser $ \user -> do
|
||||
GroupMember {activeConn} <- withStore $ \db -> getGroupMember db vr user gId gMemberId
|
||||
case activeConn of
|
||||
Just conn -> do
|
||||
withStore' $ \db -> setConnectionAuthErrCounter db user conn 0
|
||||
withStore' $ \db -> setAuthErrCounter db user conn 0
|
||||
ok user
|
||||
_ -> throwChatError CEGroupMemberNotActive
|
||||
SetShowMessages cName ntfOn -> updateChatSettings cName (\cs -> cs {enableNtfs = ntfOn})
|
||||
@@ -1494,6 +1518,8 @@ processChatCommand' vr = \case
|
||||
groupId <- withStore $ \db -> getGroupIdByName db user gName
|
||||
processChatCommand $ APIGroupInfo groupId
|
||||
GroupMemberInfo gName mName -> withMemberName gName mName APIGroupMemberInfo
|
||||
ContactQueueInfo cName -> withContactName cName APIContactQueueInfo
|
||||
GroupMemberQueueInfo gName mName -> withMemberName gName mName APIGroupMemberQueueInfo
|
||||
SwitchContact cName -> withContactName cName APISwitchContact
|
||||
SwitchGroupMember gName mName -> withMemberName gName mName APISwitchGroupMember
|
||||
AbortSwitchContact cName -> withContactName cName APIAbortSwitchContact
|
||||
@@ -1547,8 +1573,8 @@ processChatCommand' vr = \case
|
||||
let chatV = agentToChatVersion agentV
|
||||
dm <- encodeConnInfoPQ pqSup' chatV $ XInfo profileToSend
|
||||
connId <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True cReq pqSup'
|
||||
conn <- withStore' $ \db -> createDirectConnection db user connId cReq ConnJoined (incognitoProfile $> profileToSend) subMode chatV pqSup'
|
||||
void . withAgent $ \a -> joinConnection a (aUserId user) (Just connId) True cReq dm pqSup' subMode
|
||||
conn@PendingContactConnection {pccConnId} <- withStore' $ \db -> createDirectConnection db user connId cReq ConnJoined (incognitoProfile $> profileToSend) subMode chatV pqSup'
|
||||
joinPreparedAgentConnection user pccConnId connId cReq dm pqSup' subMode
|
||||
pure $ CRSentConfirmation user conn
|
||||
APIConnect userId incognito (Just (ACR SCMContact cReq)) -> withUserId userId $ \user -> connectViaContact user incognito cReq
|
||||
APIConnect _ _ Nothing -> throwChatError CEInvalidConnReq
|
||||
@@ -1701,7 +1727,7 @@ processChatCommand' vr = \case
|
||||
sndMsgs <- lift $ createSndMessages idsEvts
|
||||
let msgReqs_ :: NonEmpty (Either ChatError MsgReq) = L.zipWith (fmap . ctMsgReq) ctConns sndMsgs
|
||||
(errs, ctSndMsgs :: [(Contact, SndMessage)]) <-
|
||||
lift $ partitionEithers . L.toList . zipWith3' combineResults ctConns sndMsgs <$> deliverMessagesB msgReqs_
|
||||
partitionEithers . L.toList . zipWith3' combineResults ctConns sndMsgs <$> deliverMessagesB msgReqs_
|
||||
timestamp <- liftIO getCurrentTime
|
||||
lift . void $ withStoreBatch' $ \db -> map (createCI db user timestamp) ctSndMsgs
|
||||
pure CRBroadcastSent {user, msgContent = mc, successes = length ctSndMsgs, failures = length errs, timestamp}
|
||||
@@ -1803,12 +1829,20 @@ processChatCommand' vr = \case
|
||||
dm <- encodeConnInfo $ XGrpAcpt membershipMemId
|
||||
agentConnId <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True connRequest PQSupportOff
|
||||
let chatV = vr `peerConnChatVersion` peerChatVRange
|
||||
withStore' $ \db -> do
|
||||
createMemberConnection db userId fromMember agentConnId chatV peerChatVRange subMode
|
||||
cId <- withStore' $ \db -> do
|
||||
Connection {connId = cId} <- createMemberConnection db userId fromMember agentConnId chatV peerChatVRange subMode
|
||||
updateGroupMemberStatus db userId fromMember GSMemAccepted
|
||||
updateGroupMemberStatus db userId membership GSMemAccepted
|
||||
void . withAgent $ \a -> joinConnection a (aUserId user) (Just agentConnId) True connRequest dm PQSupportOff subMode
|
||||
updateCIGroupInvitationStatus user g CIGISAccepted `catchChatError` \_ -> pure ()
|
||||
pure cId
|
||||
void (withAgent $ \a -> joinConnection a (aUserId user) (Just agentConnId) True connRequest dm PQSupportOff subMode)
|
||||
`catchChatError` \e -> do
|
||||
withStore' $ \db -> do
|
||||
deleteConnectionRecord db user cId
|
||||
updateGroupMemberStatus db userId fromMember GSMemInvited
|
||||
updateGroupMemberStatus db userId membership GSMemInvited
|
||||
withAgent $ \a -> deleteConnectionAsync a False agentConnId
|
||||
throwError e
|
||||
updateCIGroupInvitationStatus user g CIGISAccepted `catchChatError` (toView . CRChatError (Just user))
|
||||
pure $ CRUserAcceptedGroupSent user g {membership = membership {memberStatus = GSMemAccepted}} Nothing
|
||||
Nothing -> throwChatError $ CEContactNotActive ct
|
||||
APIMemberRole groupId memberId memRole -> withUser $ \user -> do
|
||||
@@ -2236,6 +2270,7 @@ processChatCommand' vr = \case
|
||||
SubInfo {server, subError = Just e} -> M.alter (Just . maybe [e] (e :)) server m
|
||||
_ -> m
|
||||
GetAgentSubsDetails -> lift $ CRAgentSubsDetails <$> withAgent' getAgentSubscriptions
|
||||
GetAgentQueuesInfo -> lift $ CRAgentQueuesInfo <$> withAgent' getAgentQueuesInfo
|
||||
-- CustomChatCommand is unsupported, it can be processed in preCmdHook
|
||||
-- in a modified CLI app or core - the hook should return Either ChatResponse ChatCommand
|
||||
CustomChatCommand _cmd -> withUser $ \user -> pure $ chatCmdError (Just user) "not supported"
|
||||
@@ -2335,8 +2370,8 @@ processChatCommand' vr = \case
|
||||
-- [incognito] generate profile to send
|
||||
incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing
|
||||
subMode <- chatReadVar subscriptionMode
|
||||
conn <- withStore' $ \db -> createConnReqConnection db userId connId cReqHash xContactId incognitoProfile groupLinkId subMode chatV pqSup
|
||||
joinContact user connId cReq incognitoProfile xContactId inGroup pqSup chatV
|
||||
conn@PendingContactConnection {pccConnId} <- withStore' $ \db -> createConnReqConnection db userId connId cReqHash xContactId incognitoProfile groupLinkId subMode chatV pqSup
|
||||
joinContact user pccConnId connId cReq incognitoProfile xContactId inGroup pqSup chatV
|
||||
pure $ CRSentInvitation user conn incognitoProfile
|
||||
connectContactViaAddress :: User -> IncognitoEnabled -> Contact -> ConnectionRequestUri 'CMContact -> CM ChatResponse
|
||||
connectContactViaAddress user incognito ct cReq =
|
||||
@@ -2348,8 +2383,8 @@ processChatCommand' vr = \case
|
||||
-- [incognito] generate profile to send
|
||||
incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing
|
||||
subMode <- chatReadVar subscriptionMode
|
||||
ct' <- withStore $ \db -> createAddressContactConnection db vr user ct connId cReqHash newXContactId incognitoProfile subMode chatV pqSup
|
||||
joinContact user connId cReq incognitoProfile newXContactId False pqSup chatV
|
||||
(pccConnId, ct') <- withStore $ \db -> createAddressContactConnection db vr user ct connId cReqHash newXContactId incognitoProfile subMode chatV pqSup
|
||||
joinContact user pccConnId connId cReq incognitoProfile newXContactId False pqSup chatV
|
||||
pure $ CRSentInvitationToContact user ct' incognitoProfile
|
||||
prepareContact :: User -> ConnectionRequestUri 'CMContact -> PQSupport -> CM (ConnId, VersionChat)
|
||||
prepareContact user cReq pqSup = do
|
||||
@@ -2362,12 +2397,19 @@ processChatCommand' vr = \case
|
||||
let chatV = agentToChatVersion agentV
|
||||
connId <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True cReq pqSup
|
||||
pure (connId, chatV)
|
||||
joinContact :: User -> ConnId -> ConnectionRequestUri 'CMContact -> Maybe Profile -> XContactId -> Bool -> PQSupport -> VersionChat -> CM ()
|
||||
joinContact user connId cReq incognitoProfile xContactId inGroup pqSup chatV = do
|
||||
joinContact :: User -> Int64 -> ConnId -> ConnectionRequestUri 'CMContact -> Maybe Profile -> XContactId -> Bool -> PQSupport -> VersionChat -> CM ()
|
||||
joinContact user pccConnId connId cReq incognitoProfile xContactId inGroup pqSup chatV = do
|
||||
let profileToSend = userProfileToSend user incognitoProfile Nothing inGroup
|
||||
dm <- encodeConnInfoPQ pqSup chatV (XContact profileToSend $ Just xContactId)
|
||||
subMode <- chatReadVar subscriptionMode
|
||||
void . withAgent $ \a -> joinConnection a (aUserId user) (Just connId) True cReq dm pqSup subMode
|
||||
joinPreparedAgentConnection user pccConnId connId cReq dm pqSup subMode
|
||||
joinPreparedAgentConnection :: User -> Int64 -> ConnId -> ConnectionRequestUri m -> ByteString -> PQSupport -> SubscriptionMode -> CM ()
|
||||
joinPreparedAgentConnection user pccConnId connId cReq connInfo pqSup subMode = do
|
||||
void (withAgent $ \a -> joinConnection a (aUserId user) (Just connId) True cReq connInfo pqSup subMode)
|
||||
`catchChatError` \e -> do
|
||||
withStore' $ \db -> deleteConnectionRecord db user pccConnId
|
||||
withAgent $ \a -> deleteConnectionAsync a False connId
|
||||
throwError e
|
||||
contactMember :: Contact -> [GroupMember] -> Maybe GroupMember
|
||||
contactMember Contact {contactId} =
|
||||
find $ \GroupMember {memberContactId = cId, memberStatus = s} ->
|
||||
@@ -2397,7 +2439,7 @@ processChatCommand' vr = \case
|
||||
Just changedCts -> do
|
||||
let idsEvts = L.map ctSndEvent changedCts
|
||||
msgReqs_ <- lift $ L.zipWith ctMsgReq changedCts <$> createSndMessages idsEvts
|
||||
(errs, cts) <- lift $ partitionEithers . L.toList . L.zipWith (second . const) changedCts <$> deliverMessagesB msgReqs_
|
||||
(errs, cts) <- partitionEithers . L.toList . L.zipWith (second . const) changedCts <$> deliverMessagesB msgReqs_
|
||||
unless (null errs) $ toView $ CRChatErrors (Just user) errs
|
||||
let changedCts' = filter (\ChangedProfileContact {ct, ct'} -> directOrUsed ct' && mergedPreferences ct' /= mergedPreferences ct) cts
|
||||
lift $ createContactsSndFeatureItems user' changedCts'
|
||||
@@ -2776,6 +2818,9 @@ processChatCommand' vr = \case
|
||||
pure CIFile {fileId, fileName = takeFileName filePath, fileSize, fileSource = Just cf, fileStatus = CIFSSndStored, fileProtocol = FPLocal}
|
||||
let ci = mkChatItem cd ciId content ciFile_ Nothing Nothing itemForwarded Nothing False createdAt Nothing createdAt
|
||||
pure . CRNewChatItem user $ AChatItem SCTLocal SMDSnd (LocalChat nf) ci
|
||||
getConnQueueInfo user Connection {connId, agentConnId = AgentConnId acId} = do
|
||||
msgInfo <- withStore' (`getLastRcvMsgInfo` connId)
|
||||
CRQueueInfo user msgInfo <$> withAgent (`getConnectionQueueInfo` acId)
|
||||
|
||||
contactCITimed :: Contact -> CM (Maybe CITimed)
|
||||
contactCITimed ct = sndContactCITimed False ct Nothing
|
||||
@@ -3160,7 +3205,7 @@ receiveViaCompleteFD user fileId RcvFileDescr {fileDescrText, fileDescrComplete}
|
||||
pure $ filter (`notElem` knownSrvs) srvs
|
||||
ipProtectedForSrvs :: [XFTPServer] -> CM Bool
|
||||
ipProtectedForSrvs srvs = do
|
||||
netCfg <- lift $ withAgent' getNetworkConfig
|
||||
netCfg <- lift getNetworkConfig
|
||||
pure $ all (ipAddressProtected netCfg) srvs
|
||||
relaysNotApproved :: [XFTPServer] -> CM ()
|
||||
relaysNotApproved unknownSrvs = do
|
||||
@@ -3168,6 +3213,9 @@ receiveViaCompleteFD user fileId RcvFileDescr {fileDescrText, fileDescrComplete}
|
||||
forM_ aci_ $ \aci -> toView $ CRChatItemUpdated user aci
|
||||
throwChatError $ CEFileNotApproved fileId unknownSrvs
|
||||
|
||||
getNetworkConfig :: CM' NetworkConfig
|
||||
getNetworkConfig = withAgent' $ liftIO . getNetworkConfig'
|
||||
|
||||
resetRcvCIFileStatus :: User -> FileTransferId -> CIFileStatus 'MDRcv -> CM (Maybe AChatItem)
|
||||
resetRcvCIFileStatus user fileId ciFileStatus = do
|
||||
vr <- chatVersionRange
|
||||
@@ -3316,7 +3364,10 @@ deleteGroupLink_ user gInfo conn = do
|
||||
agentSubscriber :: CM' ()
|
||||
agentSubscriber = do
|
||||
q <- asks $ subQ . smpAgent
|
||||
forever $ atomically (readTBQueue q) >>= process
|
||||
forever (atomically (readTBQueue q) >>= process)
|
||||
`E.catchAny` \e -> do
|
||||
toView' $ CRChatError Nothing $ ChatErrorAgent (CRITICAL True $ "Message reception stopped: " <> show e) Nothing
|
||||
E.throwIO e
|
||||
where
|
||||
process :: (ACorrId, EntityId, APartyCmd 'Agent) -> CM' ()
|
||||
process (corrId, entId, APC e msg) = run $ case e of
|
||||
@@ -3936,17 +3987,22 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
-- TODO only acknowledge without saving message?
|
||||
-- probably this branch is never executed, so there should be no reason
|
||||
-- to save message if contact hasn't been created yet - chat item isn't created anyway
|
||||
withAckMessage' agentConnId meta $
|
||||
withAckMessage' "new contact msg" agentConnId meta $
|
||||
void $
|
||||
saveDirectRcvMSG conn meta msgBody
|
||||
SENT msgId _proxy ->
|
||||
SENT msgId _proxy -> do
|
||||
void $ continueSending connEntity conn
|
||||
sentMsgDeliveryEvent conn msgId
|
||||
OK ->
|
||||
-- [async agent commands] continuation on receiving OK
|
||||
when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure ()
|
||||
QCONT ->
|
||||
void $ continueSending connEntity conn
|
||||
MWARN _ err ->
|
||||
processConnMWARN connEntity conn err
|
||||
MERR _ err -> do
|
||||
toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity)
|
||||
incAuthErrCounter connEntity conn err
|
||||
processConnMERR connEntity conn err
|
||||
MERRS _ err -> do
|
||||
-- error cannot be AUTH error here
|
||||
toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity)
|
||||
@@ -3967,14 +4023,18 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
forM_ contData $ \(hostConnId, xGrpMemIntroCont) ->
|
||||
sendXGrpMemInv hostConnId (Just directConnReq) xGrpMemIntroCont
|
||||
CRContactUri _ -> throwChatError $ CECommandError "unexpected ConnectionRequestUri type"
|
||||
MSG msgMeta _msgFlags msgBody ->
|
||||
withAckMessage agentConnId msgMeta True $ do
|
||||
MSG msgMeta _msgFlags msgBody -> do
|
||||
tags <- newTVarIO []
|
||||
withAckMessage "contact msg" agentConnId msgMeta True (Just tags) $ \eInfo -> do
|
||||
let MsgMeta {pqEncryption} = msgMeta
|
||||
(ct', conn') <- updateContactPQRcv user ct conn pqEncryption
|
||||
checkIntegrityCreateItem (CDDirectRcv ct') msgMeta `catchChatError` \_ -> pure ()
|
||||
(conn'', msg@RcvMessage {chatMsgEvent = ACME _ event}) <- saveDirectRcvMSG conn' msgMeta msgBody
|
||||
let tag = toCMEventTag event
|
||||
atomically $ writeTVar tags [tshow tag]
|
||||
logInfo $ "contact msg=" <> tshow tag <> " " <> eInfo
|
||||
let ct'' = ct' {activeConn = Just conn''} :: Contact
|
||||
assertDirectAllowed user MDRcv ct'' $ toCMEventTag event
|
||||
assertDirectAllowed user MDRcv ct'' tag
|
||||
case event of
|
||||
XMsgNew mc -> newContentMessage ct'' mc msg msgMeta
|
||||
XMsgFileDescr sharedMsgId fileDescr -> messageFileDescription ct'' sharedMsgId fileDescr
|
||||
@@ -3999,9 +4059,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
BFileChunk sharedMsgId chunk -> bFileChunk ct'' sharedMsgId chunk msgMeta
|
||||
_ -> messageError $ "unsupported message: " <> T.pack (show event)
|
||||
let Contact {chatSettings = ChatSettings {sendRcpts}} = ct''
|
||||
pure $ fromMaybe (sendRcptsContacts user) sendRcpts && hasDeliveryReceipt (toCMEventTag event)
|
||||
pure $ fromMaybe (sendRcptsContacts user) sendRcpts && hasDeliveryReceipt tag
|
||||
RCVD msgMeta msgRcpt ->
|
||||
withAckMessage' agentConnId msgMeta $
|
||||
withAckMessage' "contact rcvd" agentConnId msgMeta $
|
||||
directMsgReceived ct conn msgMeta msgRcpt
|
||||
CONF confId pqSupport _ connInfo -> do
|
||||
conn' <- processCONFpqSupport conn pqSupport
|
||||
@@ -4073,6 +4133,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
let connectedIncognito = contactConnIncognito ct || incognitoMembership gInfo
|
||||
when (memberCategory m == GCPreMember) $ probeMatchingContactsAndMembers ct connectedIncognito True
|
||||
SENT msgId proxy -> do
|
||||
void $ continueSending connEntity conn
|
||||
sentMsgDeliveryEvent conn msgId
|
||||
checkSndInlineFTComplete conn msgId
|
||||
ci_ <- withStore $ \db -> do
|
||||
@@ -4113,12 +4174,15 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
OK ->
|
||||
-- [async agent commands] continuation on receiving OK
|
||||
when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure ()
|
||||
MWARN msgId err ->
|
||||
QCONT ->
|
||||
void $ continueSending connEntity conn
|
||||
MWARN msgId err -> do
|
||||
updateDirectItemStatus ct conn msgId (CISSndWarning $ agentSndError err)
|
||||
processConnMWARN connEntity conn err
|
||||
MERR msgId err -> do
|
||||
updateDirectItemStatus ct conn msgId (CISSndError $ agentSndError err)
|
||||
toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity)
|
||||
incAuthErrCounter connEntity conn err
|
||||
processConnMERR connEntity conn err
|
||||
MERRS msgIds err -> do
|
||||
-- error cannot be AUTH error here
|
||||
updateDirectItemsStatus ct conn (L.toList msgIds) (CISSndError $ agentSndError err)
|
||||
@@ -4380,19 +4444,26 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
void $ sendDirectMemberMessage imConn (XGrpMemCon memberId) groupId
|
||||
_ -> messageWarning "sendXGrpMemCon: member category GCPreMember or GCPostMember is expected"
|
||||
MSG msgMeta _msgFlags msgBody -> do
|
||||
withAckMessage agentConnId msgMeta True $ do
|
||||
tags <- newTVarIO []
|
||||
withAckMessage "group msg" agentConnId msgMeta True (Just tags) $ \eInfo -> do
|
||||
checkIntegrityCreateItem (CDGroupRcv gInfo m) msgMeta `catchChatError` \_ -> pure ()
|
||||
forM_ aChatMsgs $ \case
|
||||
Right (ACMsg _ chatMsg) ->
|
||||
processEvent chatMsg `catchChatError` \e -> toView $ CRChatError (Just user) e
|
||||
Left e -> toView $ CRChatError (Just user) (ChatError . CEException $ "error parsing chat message: " <> e)
|
||||
forwardMsg_ `catchChatError` \_ -> pure ()
|
||||
processEvent tags eInfo chatMsg `catchChatError` \e -> toView $ CRChatError (Just user) e
|
||||
Left e -> do
|
||||
atomically $ modifyTVar' tags ("error" :)
|
||||
logInfo $ "group msg=error " <> eInfo <> " " <> tshow e
|
||||
toView $ CRChatError (Just user) (ChatError . CEException $ "error parsing chat message: " <> e)
|
||||
forwardMsg_ `catchChatError` (toView . CRChatError (Just user))
|
||||
checkSendRcpt $ rights aChatMsgs
|
||||
where
|
||||
aChatMsgs = parseChatMessages msgBody
|
||||
brokerTs = metaBrokerTs msgMeta
|
||||
processEvent :: MsgEncodingI e => ChatMessage e -> CM ()
|
||||
processEvent chatMsg = do
|
||||
processEvent :: TVar [Text] -> Text -> MsgEncodingI e => ChatMessage e -> CM ()
|
||||
processEvent tags eInfo chatMsg@ChatMessage {chatMsgEvent} = do
|
||||
let tag = toCMEventTag chatMsgEvent
|
||||
atomically $ modifyTVar' tags (tshow tag :)
|
||||
logInfo $ "group msg=" <> tshow tag <> " " <> eInfo
|
||||
(m', conn', msg@RcvMessage {chatMsgEvent = ACME _ event}) <- saveGroupRcvMsg user groupId m conn msgMeta msgBody chatMsg
|
||||
case event of
|
||||
XMsgNew mc -> memberCanSend m' $ newGroupContentMessage gInfo m' mc msg brokerTs False
|
||||
@@ -4423,7 +4494,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
XInfoProbeCheck probeHash -> xInfoProbeCheck (COMGroupMember m') probeHash
|
||||
XInfoProbeOk probe -> xInfoProbeOk (COMGroupMember m') probe
|
||||
BFileChunk sharedMsgId chunk -> bFileChunkGroup gInfo sharedMsgId chunk msgMeta
|
||||
_ -> messageError $ "unsupported message: " <> T.pack (show event)
|
||||
_ -> messageError $ "unsupported message: " <> tshow event
|
||||
checkSendRcpt :: [AChatMessage] -> CM Bool
|
||||
checkSendRcpt aMsgs = do
|
||||
currentMemCount <- withStore' $ \db -> getGroupCurrentMembersCount db user gInfo
|
||||
@@ -4457,12 +4528,14 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
sendGroupMessage' user gInfo ms msg
|
||||
_ -> pure ()
|
||||
RCVD msgMeta msgRcpt ->
|
||||
withAckMessage' agentConnId msgMeta $
|
||||
withAckMessage' "group rcvd" agentConnId msgMeta $
|
||||
groupMsgReceived gInfo m conn msgMeta msgRcpt
|
||||
SENT msgId proxy -> do
|
||||
continued <- continueSending connEntity conn
|
||||
sentMsgDeliveryEvent conn msgId
|
||||
checkSndInlineFTComplete conn msgId
|
||||
updateGroupItemStatus gInfo m conn msgId (CISSndSent SSPComplete) (Just $ isJust proxy)
|
||||
when continued $ sendPendingGroupMessages user m conn
|
||||
SWITCH qd phase cStats -> do
|
||||
toView $ CRGroupMemberSwitch user gInfo m (SwitchProgress qd phase cStats)
|
||||
when (phase `elem` [SPStarted, SPCompleted]) $ case qd of
|
||||
@@ -4498,13 +4571,17 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
OK ->
|
||||
-- [async agent commands] continuation on receiving OK
|
||||
when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure ()
|
||||
MWARN msgId err ->
|
||||
QCONT -> do
|
||||
continued <- continueSending connEntity conn
|
||||
when continued $ sendPendingGroupMessages user m conn
|
||||
MWARN msgId err -> do
|
||||
withStore' $ \db -> updateGroupItemErrorStatus db msgId (groupMemberId' m) (CISSndWarning $ agentSndError err)
|
||||
processConnMWARN connEntity conn err
|
||||
MERR msgId err -> do
|
||||
withStore' $ \db -> updateGroupItemErrorStatus db msgId (groupMemberId' m) (CISSndError $ agentSndError err)
|
||||
-- group errors are silenced to reduce load on UI event log
|
||||
-- toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity)
|
||||
incAuthErrCounter connEntity conn err
|
||||
processConnMERR connEntity conn err
|
||||
MERRS msgIds err -> do
|
||||
let newStatus = CISSndError $ agentSndError err
|
||||
-- error cannot be AUTH error here
|
||||
@@ -4581,7 +4658,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
lookupChatItemByFileId db vr user fileId
|
||||
toView $ CRSndFileRcvCancelled user ci ft
|
||||
_ -> throwChatError $ CEFileSend fileId err
|
||||
MSG meta _ _ -> withAckMessage' agentConnId meta $ pure ()
|
||||
MSG meta _ _ ->
|
||||
withAckMessage' "file msg" agentConnId meta $ pure ()
|
||||
OK ->
|
||||
-- [async agent commands] continuation on receiving OK
|
||||
when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure ()
|
||||
@@ -4633,7 +4711,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure ()
|
||||
MERR _ err -> do
|
||||
toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity)
|
||||
incAuthErrCounter connEntity conn err
|
||||
processConnMERR connEntity conn err
|
||||
ERR err -> do
|
||||
toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity)
|
||||
when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure ()
|
||||
@@ -4657,7 +4735,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
RcvChunkOk ->
|
||||
if B.length chunk /= fromInteger chunkSize
|
||||
then badRcvFileChunk ft "incorrect chunk size"
|
||||
else withAckMessage' agentConnId meta $ appendFileChunk ft chunkNo chunk False
|
||||
else withAckMessage' "file msg" agentConnId meta $ appendFileChunk ft chunkNo chunk False
|
||||
RcvChunkFinal ->
|
||||
if B.length chunk > fromInteger chunkSize
|
||||
then badRcvFileChunk ft "incorrect chunk size"
|
||||
@@ -4671,7 +4749,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
getChatItemByFileId db vr user fileId
|
||||
toView $ CRRcvFileComplete user ci
|
||||
forM_ conn_ $ \conn -> deleteAgentConnectionAsync user (aConnId conn)
|
||||
RcvChunkDuplicate -> withAckMessage' agentConnId meta $ pure ()
|
||||
RcvChunkDuplicate -> withAckMessage' "file msg" agentConnId meta $ pure ()
|
||||
RcvChunkError -> badRcvFileChunk ft $ "incorrect chunk number " <> show chunkNo
|
||||
|
||||
processUserContactRequest :: ACommand 'Agent e -> ConnectionEntity -> Connection -> UserContact -> CM ()
|
||||
@@ -4685,7 +4763,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
_ -> pure ()
|
||||
MERR _ err -> do
|
||||
toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity)
|
||||
incAuthErrCounter connEntity conn err
|
||||
processConnMERR connEntity conn err
|
||||
ERR err -> do
|
||||
toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity)
|
||||
when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure ()
|
||||
@@ -4725,17 +4803,40 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
| memberRole <= GRObserver = messageError "member is not allowed to send messages"
|
||||
| otherwise = a
|
||||
|
||||
incAuthErrCounter :: ConnectionEntity -> Connection -> AgentErrorType -> CM ()
|
||||
incAuthErrCounter connEntity conn err = do
|
||||
processConnMERR :: ConnectionEntity -> Connection -> AgentErrorType -> CM ()
|
||||
processConnMERR connEntity conn err = do
|
||||
case err of
|
||||
SMP _ SMP.AUTH -> do
|
||||
authErrCounter' <- withStore' $ \db -> incConnectionAuthErrCounter db user conn
|
||||
authErrCounter' <- withStore' $ \db -> incAuthErrCounter db user conn
|
||||
when (authErrCounter' >= authErrDisableCount) $ case connEntity of
|
||||
RcvDirectMsgConnection ctConn (Just ct) -> do
|
||||
toView $ CRContactDisabled user ct {activeConn = Just ctConn {authErrCounter = authErrCounter'}}
|
||||
_ -> toView $ CRConnectionDisabled connEntity
|
||||
SMP _ SMP.QUOTA ->
|
||||
unless (connInactive conn) $ do
|
||||
withStore' $ \db -> setQuotaErrCounter db user conn quotaErrSetOnMERR
|
||||
toView $ CRConnectionInactive connEntity True
|
||||
_ -> pure ()
|
||||
|
||||
processConnMWARN :: ConnectionEntity -> Connection -> AgentErrorType -> CM ()
|
||||
processConnMWARN connEntity conn err = do
|
||||
case err of
|
||||
SMP _ SMP.QUOTA ->
|
||||
unless (connInactive conn) $ do
|
||||
quotaErrCounter' <- withStore' $ \db -> incQuotaErrCounter db user conn
|
||||
when (quotaErrCounter' >= quotaErrInactiveCount) $
|
||||
toView $ CRConnectionInactive connEntity True
|
||||
_ -> pure ()
|
||||
|
||||
continueSending :: ConnectionEntity -> Connection -> CM Bool
|
||||
continueSending connEntity conn =
|
||||
if connInactive conn
|
||||
then do
|
||||
withStore' $ \db -> setQuotaErrCounter db user conn 0
|
||||
toView $ CRConnectionInactive connEntity False
|
||||
pure True
|
||||
else pure False
|
||||
|
||||
-- TODO v5.7 / v6.0 - together with deprecating old group protocol establishing direct connections?
|
||||
-- we could save command records only for agent APIs we process continuations for (INV)
|
||||
withCompletedCommand :: forall e. AEntityI e => Connection -> ACommand 'Agent e -> (CommandData -> CM ()) -> CM ()
|
||||
@@ -4755,25 +4856,45 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
withStore' $ \db -> updateCommandStatus db user cmdId CSError
|
||||
throwChatError . CEAgentCommandError $ msg
|
||||
|
||||
withAckMessage' :: ConnId -> MsgMeta -> CM () -> CM ()
|
||||
withAckMessage' cId msgMeta action = do
|
||||
withAckMessage cId msgMeta False $ action $> False
|
||||
withAckMessage' :: Text -> ConnId -> MsgMeta -> CM () -> CM ()
|
||||
withAckMessage' label cId msgMeta action = do
|
||||
withAckMessage label cId msgMeta False Nothing $ \_ -> action $> False
|
||||
|
||||
withAckMessage :: ConnId -> MsgMeta -> Bool -> CM Bool -> CM ()
|
||||
withAckMessage cId msgMeta showCritical action =
|
||||
withAckMessage :: Text -> ConnId -> MsgMeta -> Bool -> Maybe (TVar [Text]) -> (Text -> CM Bool) -> CM ()
|
||||
withAckMessage label cId msgMeta showCritical tags action = do
|
||||
-- [async agent commands] command should be asynchronous
|
||||
-- TODO catching error and sending ACK after an error, particularly if it is a database error, will result in the message not processed (and no notification to the user).
|
||||
-- Possible solutions are:
|
||||
-- 1) retry processing several times
|
||||
-- 2) stabilize database
|
||||
-- 3) show screen of death to the user asking to restart
|
||||
tryChatError action >>= \case
|
||||
Right withRcpt -> ackMsg msgMeta $ if withRcpt then Just "" else Nothing
|
||||
eInfo <- eventInfo
|
||||
logInfo $ label <> ": " <> eInfo
|
||||
tryChatError (action eInfo) >>= \case
|
||||
Right withRcpt ->
|
||||
withLog (eInfo <> " ok") $ ackMsg msgMeta $ if withRcpt then Just "" else Nothing
|
||||
-- If showCritical is True, then these errors don't result in ACK and show user visible alert
|
||||
-- This prevents losing the message that failed to be processed.
|
||||
Left (ChatErrorStore SEDBBusyError {message}) | showCritical -> throwError $ ChatErrorAgent (CRITICAL True message) Nothing
|
||||
Left e -> ackMsg msgMeta Nothing >> throwError e
|
||||
Left e -> do
|
||||
withLog (eInfo <> " error: " <> tshow e) $ ackMsg msgMeta Nothing
|
||||
throwError e
|
||||
where
|
||||
eventInfo = do
|
||||
v <- asks eventSeq
|
||||
eId <- atomically $ stateTVar v $ \i -> (i + 1, i + 1)
|
||||
pure $ "conn_id=" <> tshow cId <> " event_id=" <> tshow eId
|
||||
withLog eInfo' ack = do
|
||||
ts <- showTags
|
||||
logInfo $ T.unwords [label, "ack:", ts, eInfo']
|
||||
ack
|
||||
logInfo $ T.unwords [label, "ack=success:", ts, eInfo']
|
||||
showTags = do
|
||||
ts <- maybe (pure []) readTVarIO tags
|
||||
pure $ case ts of
|
||||
[] -> "no_chat_messages"
|
||||
[t] -> "chat_message=" <> t
|
||||
_ -> "chat_message_batch=" <> T.intercalate "," (reverse ts)
|
||||
ackMsg :: MsgMeta -> Maybe MsgReceiptInfo -> CM ()
|
||||
ackMsg MsgMeta {recipient = (msgId, _)} rcpt = withAgent $ \a -> ackMessageAsync a "" cId msgId rcpt
|
||||
|
||||
@@ -6506,16 +6627,20 @@ sendGroupMemberMessages user conn events groupId = do
|
||||
let idsEvts = L.map (GroupId groupId,) events
|
||||
(errs, msgs) <- lift $ partitionEithers . L.toList <$> createSndMessages idsEvts
|
||||
unless (null errs) $ toView $ CRChatErrors (Just user) errs
|
||||
forM_ (L.nonEmpty msgs) $ \msgs' -> do
|
||||
-- TODO v5.7 based on version (?)
|
||||
-- let shouldCompress = False
|
||||
-- let batched = if shouldCompress then batchSndMessagesBinary msgs' else batchSndMessagesJSON msgs'
|
||||
let batched = batchSndMessagesJSON msgs'
|
||||
let (errs', msgBatches) = partitionEithers batched
|
||||
-- shouldn't happen, as large messages would have caused createNewSndMessage to throw SELargeMsg
|
||||
unless (null errs') $ toView $ CRChatErrors (Just user) errs'
|
||||
forM_ msgBatches $ \batch ->
|
||||
processSndMessageBatch conn batch `catchChatError` (toView . CRChatError (Just user))
|
||||
forM_ (L.nonEmpty msgs) $ \msgs' ->
|
||||
batchSendGroupMemberMessages user conn msgs'
|
||||
|
||||
batchSendGroupMemberMessages :: User -> Connection -> NonEmpty SndMessage -> CM ()
|
||||
batchSendGroupMemberMessages user conn msgs = do
|
||||
-- TODO v5.7 based on version (?)
|
||||
-- let shouldCompress = False
|
||||
-- let batched = if shouldCompress then batchSndMessagesBinary msgs' else batchSndMessagesJSON msgs'
|
||||
let batched = batchSndMessagesJSON msgs
|
||||
let (errs', msgBatches) = partitionEithers batched
|
||||
-- shouldn't happen, as large messages would have caused createNewSndMessage to throw SELargeMsg
|
||||
unless (null errs') $ toView $ CRChatErrors (Just user) errs'
|
||||
forM_ msgBatches $ \batch ->
|
||||
processSndMessageBatch conn batch `catchChatError` (toView . CRChatError (Just user))
|
||||
|
||||
processSndMessageBatch :: Connection -> MsgBatch -> CM ()
|
||||
processSndMessageBatch conn@Connection {connId} (MsgBatch batchBody sndMsgs) = do
|
||||
@@ -6561,21 +6686,21 @@ deliverMessage conn cmEventTag msgBody msgId = do
|
||||
|
||||
deliverMessage' :: Connection -> MsgFlags -> MsgBody -> MessageId -> CM (Int64, PQEncryption)
|
||||
deliverMessage' conn msgFlags msgBody msgId =
|
||||
lift (deliverMessages ((conn, msgFlags, msgBody, msgId) :| [])) >>= \case
|
||||
deliverMessages ((conn, msgFlags, msgBody, msgId) :| []) >>= \case
|
||||
r :| [] -> liftEither r
|
||||
rs -> throwChatError $ CEInternalError $ "deliverMessage: expected 1 result, got " <> show (length rs)
|
||||
|
||||
type MsgReq = (Connection, MsgFlags, MsgBody, MessageId)
|
||||
|
||||
deliverMessages :: NonEmpty MsgReq -> CM' (NonEmpty (Either ChatError (Int64, PQEncryption)))
|
||||
deliverMessages :: NonEmpty MsgReq -> CM (NonEmpty (Either ChatError (Int64, PQEncryption)))
|
||||
deliverMessages msgs = deliverMessagesB $ L.map Right msgs
|
||||
|
||||
deliverMessagesB :: NonEmpty (Either ChatError MsgReq) -> CM' (NonEmpty (Either ChatError (Int64, PQEncryption)))
|
||||
deliverMessagesB :: NonEmpty (Either ChatError MsgReq) -> CM (NonEmpty (Either ChatError (Int64, PQEncryption)))
|
||||
deliverMessagesB msgReqs = do
|
||||
msgReqs' <- liftIO compressBodies
|
||||
sent <- L.zipWith prepareBatch msgReqs' <$> withAgent' (`sendMessagesB` L.map toAgent msgReqs')
|
||||
void $ withStoreBatch' $ \db -> map (updatePQSndEnabled db) (rights . L.toList $ sent)
|
||||
withStoreBatch $ \db -> L.map (bindRight $ createDelivery db) sent
|
||||
sent <- L.zipWith prepareBatch msgReqs' <$> withAgent (`sendMessagesB` L.map toAgent msgReqs')
|
||||
lift . void $ withStoreBatch' $ \db -> map (updatePQSndEnabled db) (rights . L.toList $ sent)
|
||||
lift . withStoreBatch $ \db -> L.map (bindRight $ createDelivery db) sent
|
||||
where
|
||||
compressBodies =
|
||||
forME msgReqs $ \mr@(conn@Connection {pqSupport, connChatVersion = v}, msgFlags, msgBody, msgId) ->
|
||||
@@ -6607,6 +6732,8 @@ deliverMessagesB msgReqs = do
|
||||
where
|
||||
updatePQ = updateConnPQSndEnabled db connId pqSndEnabled'
|
||||
|
||||
-- TODO combine profile update and message into one batch
|
||||
-- Take into account that it may not fit, and that we currently don't support sending multiple messages to the same connection in one call.
|
||||
sendGroupMessage :: MsgEncodingI e => User -> GroupInfo -> [GroupMember] -> ChatMsgEvent e -> CM (SndMessage, [GroupMember])
|
||||
sendGroupMessage user gInfo members chatMsgEvent = do
|
||||
when shouldSendProfileUpdate $
|
||||
@@ -6634,10 +6761,11 @@ sendGroupMessage' user GroupInfo {groupId} members chatMsgEvent = do
|
||||
msg@SndMessage {msgId, msgBody} <- createSndMessage chatMsgEvent (GroupId groupId)
|
||||
recipientMembers <- liftIO $ shuffleMembers (filter memberCurrent members)
|
||||
let msgFlags = MsgFlags {notification = hasNotification $ toCMEventTag chatMsgEvent}
|
||||
(toSend, pending) = foldr addMember ([], []) recipientMembers
|
||||
(toSend, pending, _, dups) = foldr addMember ([], [], S.empty, 0 :: Int) recipientMembers
|
||||
-- TODO PQ either somehow ensure that group members connections cannot have pqSupport/pqEncryption or pass Off's here
|
||||
msgReqs = map (\(_, conn) -> (conn, msgFlags, msgBody, msgId)) toSend
|
||||
delivered <- maybe (pure []) (fmap L.toList . lift . deliverMessages) $ L.nonEmpty msgReqs
|
||||
when (dups /= 0) $ logError $ "sendGroupMessage: " <> tshow dups <> " duplicate members"
|
||||
delivered <- maybe (pure []) (fmap L.toList . deliverMessages) $ L.nonEmpty msgReqs
|
||||
let errors = lefts delivered
|
||||
unless (null errors) $ toView $ CRChatErrors (Just user) errors
|
||||
stored <- lift . withStoreBatch' $ \db -> map (\m -> createPendingGroupMessage db (groupMemberId' m) msgId Nothing) pending
|
||||
@@ -6650,10 +6778,16 @@ sendGroupMessage' user GroupInfo {groupId} members chatMsgEvent = do
|
||||
liftM2 (<>) (shuffle adminMs) (shuffle otherMs)
|
||||
where
|
||||
isAdmin GroupMember {memberRole} = memberRole >= GRAdmin
|
||||
addMember m (toSend, pending) = case memberSendAction chatMsgEvent members m of
|
||||
Just (MSASend conn) -> ((m, conn) : toSend, pending)
|
||||
Just MSAPending -> (toSend, m : pending)
|
||||
Nothing -> (toSend, pending)
|
||||
addMember m acc@(toSend, pending, !mIds, !dups) = case memberSendAction chatMsgEvent members m of
|
||||
Just a
|
||||
| mId `S.member` mIds -> (toSend, pending, mIds, dups + 1)
|
||||
| otherwise -> case a of
|
||||
MSASend conn -> ((m, conn) : toSend, pending, mIds', dups)
|
||||
MSAPending -> (toSend, m : pending, mIds', dups)
|
||||
Nothing -> acc
|
||||
where
|
||||
mId = groupMemberId' m
|
||||
mIds' = S.insert mId mIds
|
||||
filterSent :: [Either ChatError a] -> [mem] -> (mem -> GroupMember) -> [GroupMember]
|
||||
filterSent rs ms mem = [mem m | (Right _, m) <- zip rs ms]
|
||||
|
||||
@@ -6664,6 +6798,7 @@ memberSendAction chatMsgEvent members m@GroupMember {invitedByGroupMemberId} = c
|
||||
Nothing -> pendingOrForwarded
|
||||
Just conn@Connection {connStatus}
|
||||
| connDisabled conn || connStatus == ConnDeleted -> Nothing
|
||||
| connInactive conn -> Just MSAPending
|
||||
| connStatus == ConnSndReady || connStatus == ConnReady -> Just (MSASend conn)
|
||||
| otherwise -> pendingOrForwarded
|
||||
where
|
||||
@@ -6694,21 +6829,20 @@ sendGroupMemberMessage user m@GroupMember {groupMemberId} chatMsgEvent groupId i
|
||||
MSASend conn -> deliverMessage conn (toCMEventTag chatMsgEvent) msgBody msgId >> postDeliver
|
||||
MSAPending -> withStore' $ \db -> createPendingGroupMessage db groupMemberId msgId introId_
|
||||
|
||||
-- TODO ensure order - pending messages interleave with user input messages
|
||||
sendPendingGroupMessages :: User -> GroupMember -> Connection -> CM ()
|
||||
sendPendingGroupMessages user GroupMember {groupMemberId, localDisplayName} conn = do
|
||||
pendingMessages <- withStore' $ \db -> getPendingGroupMessages db groupMemberId
|
||||
-- TODO ensure order - pending messages interleave with user input messages
|
||||
forM_ pendingMessages $ \pgm ->
|
||||
processPendingMessage pgm `catchChatError` (toView . CRChatError (Just user))
|
||||
sendPendingGroupMessages user GroupMember {groupMemberId} conn = do
|
||||
pgms <- withStore' $ \db -> getPendingGroupMessages db groupMemberId
|
||||
forM_ (L.nonEmpty pgms) $ \pgms' -> do
|
||||
let msgs = L.map (\(sndMsg, _, _) -> sndMsg) pgms'
|
||||
batchSendGroupMemberMessages user conn msgs
|
||||
lift . void . withStoreBatch' $ \db -> L.map (\SndMessage {msgId} -> deletePendingGroupMessage db groupMemberId msgId) msgs
|
||||
lift . void . withStoreBatch' $ \db -> L.map (\(_, tag, introId_) -> updateIntro_ db tag introId_) pgms'
|
||||
where
|
||||
processPendingMessage PendingGroupMessage {msgId, cmEventTag = ACMEventTag _ tag, msgBody, introId_} = do
|
||||
void $ deliverMessage conn tag msgBody msgId
|
||||
withStore' $ \db -> deletePendingGroupMessage db groupMemberId msgId
|
||||
case tag of
|
||||
XGrpMemFwd_ -> case introId_ of
|
||||
Just introId -> withStore' $ \db -> updateIntroStatus db introId GMIntroInvForwarded
|
||||
_ -> throwChatError $ CEGroupMemberIntroNotFound localDisplayName
|
||||
_ -> pure ()
|
||||
updateIntro_ :: DB.Connection -> ACMEventTag -> Maybe Int64 -> IO ()
|
||||
updateIntro_ db tag introId_ = case (tag, introId_) of
|
||||
(ACMEventTag _ XGrpMemFwd_, Just introId) -> updateIntroStatus db introId GMIntroInvForwarded
|
||||
_ -> pure ()
|
||||
|
||||
-- TODO [batch send] refactor direct message processing same as groups (e.g. checkIntegrity before processing)
|
||||
saveDirectRcvMSG :: Connection -> MsgMeta -> MsgBody -> CM (Connection, RcvMessage)
|
||||
@@ -7245,7 +7379,7 @@ chatCommandP =
|
||||
"/ttl" $> GetChatItemTTL,
|
||||
"/_network info " *> (APISetNetworkInfo <$> jsonP),
|
||||
"/_network " *> (APISetNetworkConfig <$> jsonP),
|
||||
("/network " <|> "/net ") *> (APISetNetworkConfig <$> netCfgP),
|
||||
("/network " <|> "/net ") *> (SetNetworkConfig <$> netCfgP),
|
||||
("/network" <|> "/net") $> APIGetNetworkConfig,
|
||||
"/reconnect" $> ReconnectAllServers,
|
||||
"/_settings " *> (APISetChatSettings <$> chatRefP <* A.space <*> jsonP),
|
||||
@@ -7256,6 +7390,10 @@ chatCommandP =
|
||||
("/info #" <|> "/i #") *> (GroupMemberInfo <$> displayName <* A.space <* char_ '@' <*> displayName),
|
||||
("/info #" <|> "/i #") *> (ShowGroupInfo <$> displayName),
|
||||
("/info " <|> "/i ") *> char_ '@' *> (ContactInfo <$> displayName),
|
||||
"/_queue info #" *> (APIGroupMemberQueueInfo <$> A.decimal <* A.space <*> A.decimal),
|
||||
"/_queue info @" *> (APIContactQueueInfo <$> A.decimal),
|
||||
("/queue info #" <|> "/qi #") *> (GroupMemberQueueInfo <$> displayName <* A.space <* char_ '@' <*> displayName),
|
||||
("/queue info " <|> "/qi ") *> char_ '@' *> (ContactQueueInfo <$> displayName),
|
||||
"/_switch #" *> (APISwitchGroupMember <$> A.decimal <* A.space <*> A.decimal),
|
||||
"/_switch @" *> (APISwitchContact <$> A.decimal),
|
||||
"/_abort switch #" *> (APIAbortSwitchGroupMember <$> A.decimal <* A.space <*> A.decimal),
|
||||
@@ -7432,6 +7570,7 @@ chatCommandP =
|
||||
"/get workers" $> GetAgentWorkers,
|
||||
"/get workers details" $> GetAgentWorkersDetails,
|
||||
"/get msgs" $> GetAgentMsgCounts,
|
||||
"/get queues" $> GetAgentQueuesInfo,
|
||||
"//" *> (CustomChatCommand <$> A.takeByteString)
|
||||
]
|
||||
where
|
||||
@@ -7553,10 +7692,12 @@ chatCommandP =
|
||||
<|> ("no" $> TMEDisableKeepTTL)
|
||||
netCfgP = do
|
||||
socksProxy <- "socks=" *> ("off" $> Nothing <|> "on" $> Just defaultSocksProxy <|> Just <$> strP)
|
||||
smpProxyMode_ <- optional $ " smp-proxy=" *> strP
|
||||
smpProxyFallback_ <- optional $ " smp-proxy-fallback=" *> strP
|
||||
t_ <- optional $ " timeout=" *> A.decimal
|
||||
logErrors <- " log=" *> onOffP <|> pure False
|
||||
let tcpTimeout = 1000000 * fromMaybe (maybe 5 (const 10) socksProxy) t_
|
||||
pure $ fullNetworkConfig socksProxy tcpTimeout logErrors
|
||||
logTLSErrors <- " log=" *> onOffP <|> pure False
|
||||
let tcpTimeout_ = (1000000 *) <$> t_
|
||||
pure $ SimpleNetCfg {socksProxy, smpProxyMode_, smpProxyFallback_, tcpTimeout_, logTLSErrors}
|
||||
dbKeyP = nonEmptyKey <$?> strP
|
||||
nonEmptyKey k@(DBEncryptionKey s) = if BA.null s then Left "empty key" else Right k
|
||||
dbEncryptionConfig currentKey newKey = DBEncryptionConfig {currentKey, newKey, keepKey = Just False}
|
||||
|
||||
@@ -26,6 +26,7 @@ import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
import qualified Database.SQLite3 as SQL
|
||||
import Simplex.Chat.Controller
|
||||
import Simplex.Chat.Util ()
|
||||
import Simplex.Messaging.Agent.Client (agentClientStore)
|
||||
import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore (..), closeSQLiteStore, keyString, sqlString, storeKey)
|
||||
import Simplex.Messaging.Util
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
{-# LANGUAGE StrictData #-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
{-# OPTIONS_GHC -fno-warn-implicit-lift #-}
|
||||
|
||||
module Simplex.Chat.Controller where
|
||||
|
||||
@@ -67,13 +68,14 @@ import Simplex.Chat.Types.UITheme
|
||||
import Simplex.Chat.Util (liftIOEither)
|
||||
import Simplex.FileTransfer.Description (FileDescriptionURI)
|
||||
import Simplex.Messaging.Agent (AgentClient, SubscriptionsInfo)
|
||||
import Simplex.Messaging.Agent.Client (AgentLocks, AgentWorkersDetails (..), AgentWorkersSummary (..), ProtocolTestFailure, UserNetworkInfo)
|
||||
import Simplex.Messaging.Agent.Client (AgentLocks, AgentQueuesInfo (..), AgentWorkersDetails (..), AgentWorkersSummary (..), ProtocolTestFailure, UserNetworkInfo)
|
||||
import Simplex.Messaging.Agent.Env.SQLite (AgentConfig, NetworkConfig)
|
||||
import Simplex.Messaging.Agent.Lock
|
||||
import Simplex.Messaging.Agent.Protocol
|
||||
import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation, SQLiteStore, UpMigration, withTransaction)
|
||||
import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..))
|
||||
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
|
||||
import Simplex.Messaging.Client (SMPProxyMode (..), SMPProxyFallback (..))
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Crypto.File (CryptoFile (..))
|
||||
import qualified Simplex.Messaging.Crypto.File as CF
|
||||
@@ -82,9 +84,10 @@ import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Notifications.Protocol (DeviceToken (..), NtfTknStatus)
|
||||
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, parseAll, parseString, sumTypeJSON)
|
||||
import Simplex.Messaging.Protocol (AProtoServerWithAuth, AProtocolType (..), CorrId, NtfServer, ProtoServerWithAuth, ProtocolTypeI, QueueId, SMPMsgMeta (..), SProtocolType, SubscriptionMode (..), UserProtocol, XFTPServer, XFTPServerWithAuth, userProtocol)
|
||||
import Simplex.Messaging.Server.QueueStore.QueueInfo
|
||||
import Simplex.Messaging.TMap (TMap)
|
||||
import Simplex.Messaging.Transport (TLS, simplexMQVersion)
|
||||
import Simplex.Messaging.Transport.Client (TransportHost)
|
||||
import Simplex.Messaging.Transport.Client (SocksProxy, TransportHost)
|
||||
import Simplex.Messaging.Util (allFinally, catchAllErrors, catchAllErrors', tryAllErrors, tryAllErrors', (<$$>))
|
||||
import Simplex.RemoteControl.Client
|
||||
import Simplex.RemoteControl.Invitation (RCSignedInvitation, RCVerifiedInvitation)
|
||||
@@ -205,6 +208,7 @@ data ChatController = ChatController
|
||||
chatStore :: SQLiteStore,
|
||||
chatStoreChanged :: TVar Bool, -- if True, chat should be fully restarted
|
||||
random :: TVar ChaChaDRG,
|
||||
eventSeq :: TVar Int,
|
||||
inputQ :: TBQueue String,
|
||||
outputQ :: TBQueue (Maybe CorrId, Maybe RemoteHostId, ChatResponse),
|
||||
connNetworkStatuses :: TMap AgentConnId NetworkStatus,
|
||||
@@ -348,6 +352,7 @@ data ChatCommand
|
||||
| GetChatItemTTL
|
||||
| APISetNetworkConfig NetworkConfig
|
||||
| APIGetNetworkConfig
|
||||
| SetNetworkConfig SimpleNetCfg
|
||||
| APISetNetworkInfo UserNetworkInfo
|
||||
| ReconnectAllServers
|
||||
| APISetChatSettings ChatRef ChatSettings
|
||||
@@ -355,6 +360,8 @@ data ChatCommand
|
||||
| APIContactInfo ContactId
|
||||
| APIGroupInfo GroupId
|
||||
| APIGroupMemberInfo GroupId GroupMemberId
|
||||
| APIContactQueueInfo ContactId
|
||||
| APIGroupMemberQueueInfo GroupId GroupMemberId
|
||||
| APISwitchContact ContactId
|
||||
| APISwitchGroupMember GroupId GroupMemberId
|
||||
| APIAbortSwitchContact ContactId
|
||||
@@ -373,6 +380,8 @@ data ChatCommand
|
||||
| ContactInfo ContactName
|
||||
| ShowGroupInfo GroupName
|
||||
| GroupMemberInfo GroupName ContactName
|
||||
| ContactQueueInfo ContactName
|
||||
| GroupMemberQueueInfo GroupName ContactName
|
||||
| SwitchContact ContactName
|
||||
| SwitchGroupMember GroupName ContactName
|
||||
| AbortSwitchContact ContactName
|
||||
@@ -502,6 +511,7 @@ data ChatCommand
|
||||
| GetAgentWorkers
|
||||
| GetAgentWorkersDetails
|
||||
| GetAgentMsgCounts
|
||||
| GetAgentQueuesInfo
|
||||
| -- The parser will return this command for strings that start from "//".
|
||||
-- This command should be processed in preCmdHook
|
||||
CustomChatCommand ByteString
|
||||
@@ -566,6 +576,7 @@ data ChatResponse
|
||||
| CRContactInfo {user :: User, contact :: Contact, connectionStats_ :: Maybe ConnectionStats, customUserProfile :: Maybe Profile}
|
||||
| CRGroupInfo {user :: User, groupInfo :: GroupInfo, groupSummary :: GroupSummary}
|
||||
| CRGroupMemberInfo {user :: User, groupInfo :: GroupInfo, member :: GroupMember, connectionStats_ :: Maybe ConnectionStats}
|
||||
| CRQueueInfo {user :: User, rcvMsgInfo :: Maybe RcvMsgInfo, queueInfo :: QueueInfo}
|
||||
| CRContactSwitchStarted {user :: User, contact :: Contact, connectionStats :: ConnectionStats}
|
||||
| CRGroupMemberSwitchStarted {user :: User, groupInfo :: GroupInfo, member :: GroupMember, connectionStats :: ConnectionStats}
|
||||
| CRContactSwitchAborted {user :: User, contact :: Contact, connectionStats :: ConnectionStats}
|
||||
@@ -748,8 +759,10 @@ data ChatResponse
|
||||
| CRAgentSubs {activeSubs :: Map Text Int, pendingSubs :: Map Text Int, removedSubs :: Map Text [String]}
|
||||
| CRAgentSubsDetails {agentSubs :: SubscriptionsInfo}
|
||||
| CRAgentMsgCounts {msgCounts :: [(Text, (Int, Int))]}
|
||||
| CRAgentQueuesInfo {agentQueuesInfo :: AgentQueuesInfo}
|
||||
| CRContactDisabled {user :: User, contact :: Contact}
|
||||
| CRConnectionDisabled {connectionEntity :: ConnectionEntity}
|
||||
| CRConnectionInactive {connectionEntity :: ConnectionEntity, inactive :: Bool}
|
||||
| CRAgentRcvQueueDeleted {agentConnId :: AgentConnId, server :: SMPServer, agentQueueId :: AgentQueueId, agentError_ :: Maybe AgentErrorType}
|
||||
| CRAgentConnDeleted {agentConnId :: AgentConnId}
|
||||
| CRAgentUserDeleted {agentUserId :: Int64}
|
||||
@@ -950,6 +963,18 @@ data AppFilePathsConfig = AppFilePathsConfig
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
data SimpleNetCfg = SimpleNetCfg
|
||||
{ socksProxy :: Maybe SocksProxy,
|
||||
smpProxyMode_ :: Maybe SMPProxyMode,
|
||||
smpProxyFallback_ :: Maybe SMPProxyFallback,
|
||||
tcpTimeout_ :: Maybe Int,
|
||||
logTLSErrors :: Bool
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
defaultSimpleNetCfg :: SimpleNetCfg
|
||||
defaultSimpleNetCfg = SimpleNetCfg Nothing Nothing Nothing Nothing False
|
||||
|
||||
data ContactSubStatus = ContactSubStatus
|
||||
{ contact :: Contact,
|
||||
contactError :: Maybe ChatError
|
||||
@@ -1115,7 +1140,6 @@ data ChatErrorType
|
||||
| CECantBlockMemberForSelf {groupInfo :: GroupInfo, member :: GroupMember, setShowMessages :: Bool}
|
||||
| CEGroupMemberUserRemoved
|
||||
| CEGroupMemberNotFound
|
||||
| CEGroupMemberIntroNotFound {contactName :: ContactName}
|
||||
| CEGroupCantResendInvitation {groupInfo :: GroupInfo, contactName :: ContactName}
|
||||
| CEGroupInternal {message :: String}
|
||||
| CEFileNotFound {message :: String}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
{-# LANGUAGE TypeOperators #-}
|
||||
{-# LANGUAGE UndecidableInstances #-}
|
||||
{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-}
|
||||
{-# OPTIONS_GHC -fno-warn-operator-whitespace #-}
|
||||
|
||||
module Simplex.Chat.Messages where
|
||||
|
||||
@@ -455,10 +456,10 @@ deriving instance Show ACIReaction
|
||||
data JSONCIReaction c d = JSONCIReaction {chatInfo :: ChatInfo c, chatReaction :: CIReaction c d}
|
||||
|
||||
type family ChatTypeQuotable (a :: ChatType) :: Constraint where
|
||||
ChatTypeQuotable CTDirect = ()
|
||||
ChatTypeQuotable CTGroup = ()
|
||||
ChatTypeQuotable 'CTDirect = ()
|
||||
ChatTypeQuotable 'CTGroup = ()
|
||||
ChatTypeQuotable a =
|
||||
(Int ~ Bool, TypeError (Type.Text "ChatType " :<>: ShowType a :<>: Type.Text " cannot be quoted"))
|
||||
(Int ~ Bool, TypeError ('Type.Text "ChatType " ':<>: 'ShowType a ':<>: 'Type.Text " cannot be quoted"))
|
||||
|
||||
data CIQDirection (c :: ChatType) where
|
||||
CIQDirectSnd :: CIQDirection 'CTDirect
|
||||
@@ -944,13 +945,6 @@ data RcvMessage = RcvMessage
|
||||
forwardedByMember :: Maybe GroupMemberId
|
||||
}
|
||||
|
||||
data PendingGroupMessage = PendingGroupMessage
|
||||
{ msgId :: MessageId,
|
||||
cmEventTag :: ACMEventTag,
|
||||
msgBody :: MsgBody,
|
||||
introId_ :: Maybe Int64
|
||||
}
|
||||
|
||||
type MessageId = Int64
|
||||
|
||||
data ConnOrGroupId = ConnectionId Int64 | GroupId Int64
|
||||
@@ -968,6 +962,15 @@ data RcvMsgDelivery = RcvMsgDelivery
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
data RcvMsgInfo = RcvMsgInfo
|
||||
{ msgId :: Int64,
|
||||
msgDeliveryId :: Int64,
|
||||
msgDeliveryStatus :: Text,
|
||||
agentMsgId :: AgentMsgId,
|
||||
agentMsgMeta :: Text
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
data MsgMetaJSON = MsgMetaJSON
|
||||
{ integrity :: Text,
|
||||
rcvId :: Int64,
|
||||
@@ -1338,3 +1341,5 @@ $(JQ.deriveJSON defaultJSON ''MsgMetaJSON)
|
||||
|
||||
msgMetaJson :: MsgMeta -> Text
|
||||
msgMetaJson = decodeLatin1 . LB.toStrict . J.encode . msgMetaToJson
|
||||
|
||||
$(JQ.deriveJSON defaultJSON ''RcvMsgInfo)
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
|
||||
module Simplex.Chat.Migrations.M20240528_quota_err_counter where
|
||||
|
||||
import Database.SQLite.Simple (Query)
|
||||
import Database.SQLite.Simple.QQ (sql)
|
||||
|
||||
m20240528_quota_err_counter :: Query
|
||||
m20240528_quota_err_counter =
|
||||
[sql|
|
||||
ALTER TABLE connections ADD COLUMN quota_err_counter INTEGER NOT NULL DEFAULT 0;
|
||||
|]
|
||||
|
||||
down_m20240528_quota_err_counter :: Query
|
||||
down_m20240528_quota_err_counter =
|
||||
[sql|
|
||||
ALTER TABLE connections DROP COLUMN quota_err_counter;
|
||||
|]
|
||||
@@ -289,6 +289,7 @@ CREATE TABLE connections(
|
||||
pq_encryption INTEGER NOT NULL DEFAULT 0,
|
||||
pq_snd_enabled INTEGER,
|
||||
pq_rcv_enabled INTEGER,
|
||||
quota_err_counter INTEGER NOT NULL DEFAULT 0,
|
||||
FOREIGN KEY(snd_file_id, connection_id)
|
||||
REFERENCES snd_files(file_id, connection_id)
|
||||
ON DELETE CASCADE
|
||||
|
||||
@@ -48,7 +48,6 @@ import Simplex.Chat.Types
|
||||
import Simplex.Messaging.Agent.Client (agentClientStore)
|
||||
import Simplex.Messaging.Agent.Env.SQLite (createAgentStore)
|
||||
import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..), MigrationError, closeSQLiteStore, reopenSQLiteStore)
|
||||
import Simplex.Messaging.Client (defaultNetworkConfig)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, sumTypeJSON)
|
||||
@@ -192,7 +191,7 @@ mobileChatOpts dbFilePrefix =
|
||||
dbKey = "", -- for API database is already opened, and the key in options is not used
|
||||
smpServers = [],
|
||||
xftpServers = [],
|
||||
networkConfig = defaultNetworkConfig,
|
||||
simpleNetCfg = defaultSimpleNetCfg,
|
||||
logLevel = CLLImportant,
|
||||
logConnections = False,
|
||||
logServerHosts = True,
|
||||
|
||||
+29
-17
@@ -13,7 +13,6 @@ module Simplex.Chat.Options
|
||||
coreChatOptsP,
|
||||
getChatOpts,
|
||||
protocolServersP,
|
||||
fullNetworkConfig,
|
||||
)
|
||||
where
|
||||
|
||||
@@ -22,15 +21,17 @@ import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||
import Data.ByteArray (ScrubbedBytes)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
import Data.Text.Encoding (encodeUtf8)
|
||||
import Numeric.Natural (Natural)
|
||||
import Options.Applicative
|
||||
import Simplex.Chat.Controller (ChatLogLevel (..), updateStr, versionNumber, versionString)
|
||||
import Simplex.Chat.Controller (ChatLogLevel (..), SimpleNetCfg (..), updateStr, versionNumber, versionString)
|
||||
import Simplex.FileTransfer.Description (mb)
|
||||
import Simplex.Messaging.Client (NetworkConfig (..), defaultNetworkConfig)
|
||||
import Simplex.Messaging.Client (SMPProxyMode (..), SMPProxyFallback (..))
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Parsers (parseAll)
|
||||
import Simplex.Messaging.Protocol (ProtoServerWithAuth, ProtocolTypeI, SMPServerWithAuth, XFTPServerWithAuth)
|
||||
import Simplex.Messaging.Transport.Client (SocksProxy, defaultSocksProxy)
|
||||
import Simplex.Messaging.Transport.Client (defaultSocksProxy)
|
||||
import System.FilePath (combine)
|
||||
|
||||
data ChatOpts = ChatOpts
|
||||
@@ -55,7 +56,7 @@ data CoreChatOpts = CoreChatOpts
|
||||
dbKey :: ScrubbedBytes,
|
||||
smpServers :: [SMPServerWithAuth],
|
||||
xftpServers :: [XFTPServerWithAuth],
|
||||
networkConfig :: NetworkConfig,
|
||||
simpleNetCfg :: SimpleNetCfg,
|
||||
logLevel :: ChatLogLevel,
|
||||
logConnections :: Bool,
|
||||
logServerHosts :: Bool,
|
||||
@@ -123,18 +124,34 @@ coreChatOptsP appDir defaultDbFileName = do
|
||||
socksProxy <-
|
||||
flag' (Just defaultSocksProxy) (short 'x' <> help "Use local SOCKS5 proxy at :9050")
|
||||
<|> option
|
||||
parseSocksProxy
|
||||
strParse
|
||||
( long "socks-proxy"
|
||||
<> metavar "SOCKS5"
|
||||
<> help "Use SOCKS5 proxy at `ipv4:port` or `:port`"
|
||||
<> value Nothing
|
||||
)
|
||||
smpProxyMode_ <-
|
||||
optional $
|
||||
option
|
||||
strParse
|
||||
( long "smp-proxy"
|
||||
<> metavar "SMP_PROXY_MODE"
|
||||
<> help "Use private message routing: always, unknown, unprotected, never (default)"
|
||||
)
|
||||
smpProxyFallback_ <-
|
||||
optional $
|
||||
option
|
||||
strParse
|
||||
( long "smp-proxy-fallback"
|
||||
<> metavar "SMP_PROXY_FALLBACK_MODE"
|
||||
<> help "Allow downgrade and connect directly: no, [when IP address is] protected, yes (default)"
|
||||
)
|
||||
t <-
|
||||
option
|
||||
auto
|
||||
( long "tcp-timeout"
|
||||
<> metavar "TIMEOUT"
|
||||
<> help "TCP timeout, seconds (default: 5/10 without/with SOCKS5 proxy)"
|
||||
<> help "TCP timeout, seconds (default: 7/15 without/with SOCKS5 proxy)"
|
||||
<> value 0
|
||||
)
|
||||
logLevel <-
|
||||
@@ -149,7 +166,7 @@ coreChatOptsP appDir defaultDbFileName = do
|
||||
logTLSErrors <-
|
||||
switch
|
||||
( long "log-tls-errors"
|
||||
<> help "Log TLS errors (also enabled with `-l debug`)"
|
||||
<> help "Log TLS errors"
|
||||
)
|
||||
logConnections <-
|
||||
switch
|
||||
@@ -194,7 +211,7 @@ coreChatOptsP appDir defaultDbFileName = do
|
||||
dbKey,
|
||||
smpServers,
|
||||
xftpServers,
|
||||
networkConfig = fullNetworkConfig socksProxy (useTcpTimeout socksProxy t) (logTLSErrors || logLevel == CLLDebug),
|
||||
simpleNetCfg = SimpleNetCfg {socksProxy, smpProxyMode_, smpProxyFallback_, tcpTimeout_ = Just $ useTcpTimeout socksProxy t, logTLSErrors},
|
||||
logLevel,
|
||||
logConnections = logConnections || logLevel <= CLLInfo,
|
||||
logServerHosts = logServerHosts || logLevel <= CLLInfo,
|
||||
@@ -204,7 +221,7 @@ coreChatOptsP appDir defaultDbFileName = do
|
||||
highlyAvailable
|
||||
}
|
||||
where
|
||||
useTcpTimeout p t = 1000000 * if t > 0 then t else maybe 5 (const 10) p
|
||||
useTcpTimeout p t = 1000000 * if t > 0 then t else maybe 7 (const 15) p
|
||||
defaultDbFilePath = combine appDir defaultDbFileName
|
||||
|
||||
chatOptsP :: FilePath -> FilePath -> Parser ChatOpts
|
||||
@@ -321,16 +338,11 @@ chatOptsP appDir defaultDbFileName = do
|
||||
maintenance
|
||||
}
|
||||
|
||||
fullNetworkConfig :: Maybe SocksProxy -> Int -> Bool -> NetworkConfig
|
||||
fullNetworkConfig socksProxy tcpTimeout logTLSErrors =
|
||||
let tcpConnectTimeout = (tcpTimeout * 3) `div` 2
|
||||
in defaultNetworkConfig {socksProxy, tcpTimeout, tcpConnectTimeout, logTLSErrors}
|
||||
|
||||
parseProtocolServers :: ProtocolTypeI p => ReadM [ProtoServerWithAuth p]
|
||||
parseProtocolServers = eitherReader $ parseAll protocolServersP . B.pack
|
||||
|
||||
parseSocksProxy :: ReadM (Maybe SocksProxy)
|
||||
parseSocksProxy = eitherReader $ parseAll strP . B.pack
|
||||
strParse :: StrEncoding a => ReadM a
|
||||
strParse = eitherReader $ parseAll strP . encodeUtf8 . T.pack
|
||||
|
||||
parseServerPort :: ReadM (Maybe String)
|
||||
parseServerPort = eitherReader $ parseAll serverPortP . B.pack
|
||||
|
||||
@@ -72,11 +72,11 @@ import UnliftIO.Directory (copyFile, createDirectoryIfMissing, doesDirectoryExis
|
||||
|
||||
-- when acting as host
|
||||
minRemoteCtrlVersion :: AppVersion
|
||||
minRemoteCtrlVersion = AppVersion [5, 7, 0, 3]
|
||||
minRemoteCtrlVersion = AppVersion [5, 8, 0, 4]
|
||||
|
||||
-- when acting as controller
|
||||
minRemoteHostVersion :: AppVersion
|
||||
minRemoteHostVersion = AppVersion [5, 7, 0, 3]
|
||||
minRemoteHostVersion = AppVersion [5, 8, 0, 4]
|
||||
|
||||
currentAppVersion :: AppVersion
|
||||
currentAppVersion = AppVersion SC.version
|
||||
|
||||
@@ -14,6 +14,7 @@ module Simplex.Chat.Store.Connections
|
||||
getContactConnEntityByConnReqHash,
|
||||
getConnectionsToSubscribe,
|
||||
unsetConnectionToSubscribe,
|
||||
deleteConnectionRecord,
|
||||
)
|
||||
where
|
||||
|
||||
@@ -84,7 +85,7 @@ getConnectionEntity db vr user@User {userId, userContactId} agentConnId = do
|
||||
[sql|
|
||||
SELECT connection_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, via_group_link, group_link_id, custom_user_profile_id,
|
||||
conn_status, conn_type, contact_conn_initiated, local_alias, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id,
|
||||
created_at, security_code, security_code_verified_at, pq_support, pq_encryption, pq_snd_enabled, pq_rcv_enabled, auth_err_counter,
|
||||
created_at, security_code, security_code_verified_at, pq_support, pq_encryption, pq_snd_enabled, pq_rcv_enabled, auth_err_counter, quota_err_counter,
|
||||
conn_chat_version, peer_chat_min_version, peer_chat_max_version
|
||||
FROM connections
|
||||
WHERE user_id = ? AND agent_conn_id = ?
|
||||
@@ -225,3 +226,7 @@ getConnectionsToSubscribe db vr = do
|
||||
|
||||
unsetConnectionToSubscribe :: DB.Connection -> IO ()
|
||||
unsetConnectionToSubscribe db = DB.execute_ db "UPDATE connections SET to_subscribe = 0 WHERE to_subscribe = 1"
|
||||
|
||||
deleteConnectionRecord :: DB.Connection -> User -> Int64 -> IO ()
|
||||
deleteConnectionRecord db User {userId} cId = do
|
||||
DB.execute db "DELETE FROM connections WHERE user_id = ? AND connection_id = ?" (userId, cId)
|
||||
|
||||
@@ -51,8 +51,10 @@ module Simplex.Chat.Store.Direct
|
||||
updateContactStatus,
|
||||
updateGroupUnreadChat,
|
||||
setConnectionVerified,
|
||||
incConnectionAuthErrCounter,
|
||||
setConnectionAuthErrCounter,
|
||||
incAuthErrCounter,
|
||||
setAuthErrCounter,
|
||||
incQuotaErrCounter,
|
||||
setQuotaErrCounter,
|
||||
getUserContacts,
|
||||
createOrUpdateContactRequest,
|
||||
getContactRequest',
|
||||
@@ -130,11 +132,11 @@ deletePendingContactConnection db userId connId =
|
||||
|]
|
||||
(userId, connId, ConnContact)
|
||||
|
||||
createAddressContactConnection :: DB.Connection -> VersionRangeChat -> User -> Contact -> ConnId -> ConnReqUriHash -> XContactId -> Maybe Profile -> SubscriptionMode -> VersionChat -> PQSupport -> ExceptT StoreError IO Contact
|
||||
createAddressContactConnection :: DB.Connection -> VersionRangeChat -> User -> Contact -> ConnId -> ConnReqUriHash -> XContactId -> Maybe Profile -> SubscriptionMode -> VersionChat -> PQSupport -> ExceptT StoreError IO (Int64, Contact)
|
||||
createAddressContactConnection db vr user@User {userId} Contact {contactId} acId cReqHash xContactId incognitoProfile subMode chatV pqSup = do
|
||||
PendingContactConnection {pccConnId} <- liftIO $ createConnReqConnection db userId acId cReqHash xContactId incognitoProfile Nothing subMode chatV pqSup
|
||||
liftIO $ DB.execute db "UPDATE connections SET contact_id = ? WHERE connection_id = ?" (contactId, pccConnId)
|
||||
getContact db vr user contactId
|
||||
(pccConnId,) <$> getContact db vr user contactId
|
||||
|
||||
createConnReqConnection :: DB.Connection -> UserId -> ConnId -> ConnReqUriHash -> XContactId -> Maybe Profile -> Maybe GroupLinkId -> SubscriptionMode -> VersionChat -> PQSupport -> IO PendingContactConnection
|
||||
createConnReqConnection db userId acId cReqHash xContactId incognitoProfile groupLinkId subMode chatV pqSup = do
|
||||
@@ -183,7 +185,7 @@ getContactByConnReqHash db vr user@User {userId} cReqHash =
|
||||
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.chat_deleted, ct.custom_data,
|
||||
-- Connection
|
||||
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias,
|
||||
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter,
|
||||
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter,
|
||||
c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version
|
||||
FROM contacts ct
|
||||
JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id
|
||||
@@ -473,19 +475,32 @@ setConnectionVerified db User {userId} connId code = do
|
||||
updatedAt <- getCurrentTime
|
||||
DB.execute db "UPDATE connections SET security_code = ?, security_code_verified_at = ?, updated_at = ? WHERE user_id = ? AND connection_id = ?" (code, code $> updatedAt, updatedAt, userId, connId)
|
||||
|
||||
incConnectionAuthErrCounter :: DB.Connection -> User -> Connection -> IO Int
|
||||
incConnectionAuthErrCounter db User {userId} Connection {connId, authErrCounter} = do
|
||||
incAuthErrCounter :: DB.Connection -> User -> Connection -> IO Int
|
||||
incAuthErrCounter db User {userId} Connection {connId, authErrCounter} = do
|
||||
updatedAt <- getCurrentTime
|
||||
(counter_ :: Maybe Int) <- maybeFirstRow fromOnly $ DB.query db "SELECT auth_err_counter FROM connections WHERE user_id = ? AND connection_id = ?" (userId, connId)
|
||||
let counter' = fromMaybe authErrCounter counter_ + 1
|
||||
DB.execute db "UPDATE connections SET auth_err_counter = ?, updated_at = ? WHERE user_id = ? AND connection_id = ?" (counter', updatedAt, userId, connId)
|
||||
pure counter'
|
||||
|
||||
setConnectionAuthErrCounter :: DB.Connection -> User -> Connection -> Int -> IO ()
|
||||
setConnectionAuthErrCounter db User {userId} Connection {connId} counter = do
|
||||
setAuthErrCounter :: DB.Connection -> User -> Connection -> Int -> IO ()
|
||||
setAuthErrCounter db User {userId} Connection {connId} counter = do
|
||||
updatedAt <- getCurrentTime
|
||||
DB.execute db "UPDATE connections SET auth_err_counter = ?, updated_at = ? WHERE user_id = ? AND connection_id = ?" (counter, updatedAt, userId, connId)
|
||||
|
||||
incQuotaErrCounter :: DB.Connection -> User -> Connection -> IO Int
|
||||
incQuotaErrCounter db User {userId} Connection {connId, quotaErrCounter} = do
|
||||
updatedAt <- getCurrentTime
|
||||
(counter_ :: Maybe Int) <- maybeFirstRow fromOnly $ DB.query db "SELECT quota_err_counter FROM connections WHERE user_id = ? AND connection_id = ?" (userId, connId)
|
||||
let counter' = fromMaybe quotaErrCounter counter_ + 1
|
||||
DB.execute db "UPDATE connections SET quota_err_counter = ?, updated_at = ? WHERE user_id = ? AND connection_id = ?" (counter', updatedAt, userId, connId)
|
||||
pure counter'
|
||||
|
||||
setQuotaErrCounter :: DB.Connection -> User -> Connection -> Int -> IO ()
|
||||
setQuotaErrCounter db User {userId} Connection {connId} counter = do
|
||||
updatedAt <- getCurrentTime
|
||||
DB.execute db "UPDATE connections SET quota_err_counter = ?, updated_at = ? WHERE user_id = ? AND connection_id = ?" (counter, updatedAt, userId, connId)
|
||||
|
||||
updateContactProfile_ :: DB.Connection -> UserId -> ProfileId -> Profile -> IO ()
|
||||
updateContactProfile_ db userId profileId profile = do
|
||||
currentTs <- getCurrentTime
|
||||
@@ -609,7 +624,7 @@ createOrUpdateContactRequest db vr user@User {userId} userContactLinkId invId (V
|
||||
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.chat_deleted, ct.custom_data,
|
||||
-- Connection
|
||||
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias,
|
||||
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter,
|
||||
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter,
|
||||
c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version
|
||||
FROM contacts ct
|
||||
JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id
|
||||
@@ -794,7 +809,7 @@ getContact_ db vr user@User {userId} contactId deleted =
|
||||
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.chat_deleted, ct.custom_data,
|
||||
-- Connection
|
||||
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias,
|
||||
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter,
|
||||
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter,
|
||||
c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version
|
||||
FROM contacts ct
|
||||
JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id
|
||||
@@ -848,7 +863,7 @@ getContactConnections db vr userId Contact {contactId} =
|
||||
[sql|
|
||||
SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
|
||||
c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id,
|
||||
c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter,
|
||||
c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter,
|
||||
c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version
|
||||
FROM connections c
|
||||
JOIN contacts ct ON ct.contact_id = c.contact_id
|
||||
@@ -866,7 +881,7 @@ getConnectionById db vr User {userId} connId = ExceptT $ do
|
||||
[sql|
|
||||
SELECT connection_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, via_group_link, group_link_id, custom_user_profile_id,
|
||||
conn_status, conn_type, contact_conn_initiated, local_alias, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id,
|
||||
created_at, security_code, security_code_verified_at, pq_support, pq_encryption, pq_snd_enabled, pq_rcv_enabled, auth_err_counter,
|
||||
created_at, security_code, security_code_verified_at, pq_support, pq_encryption, pq_snd_enabled, pq_rcv_enabled, auth_err_counter, quota_err_counter,
|
||||
conn_chat_version, peer_chat_min_version, peer_chat_max_version
|
||||
FROM connections
|
||||
WHERE user_id = ? AND connection_id = ?
|
||||
|
||||
@@ -201,7 +201,7 @@ getGroupLinkConnection db vr User {userId} groupInfo@GroupInfo {groupId} =
|
||||
[sql|
|
||||
SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
|
||||
c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id,
|
||||
c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter,
|
||||
c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter,
|
||||
c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version
|
||||
FROM connections c
|
||||
JOIN user_contact_links uc ON c.user_contact_link_id = uc.user_contact_link_id
|
||||
@@ -287,7 +287,7 @@ getGroupAndMember db User {userId, userContactId} groupMemberId vr =
|
||||
m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences,
|
||||
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
|
||||
c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id,
|
||||
c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter,
|
||||
c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter,
|
||||
c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version
|
||||
FROM group_members m
|
||||
JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id)
|
||||
@@ -705,7 +705,7 @@ groupMemberQuery =
|
||||
m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences,
|
||||
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
|
||||
c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id,
|
||||
c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter,
|
||||
c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter,
|
||||
c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version
|
||||
FROM group_members m
|
||||
JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id)
|
||||
@@ -944,10 +944,10 @@ getMemberInvitation db User {userId} groupMemberId =
|
||||
fmap join . maybeFirstRow fromOnly $
|
||||
DB.query db "SELECT sent_inv_queue_info FROM group_members WHERE group_member_id = ? AND user_id = ?" (groupMemberId, userId)
|
||||
|
||||
createMemberConnection :: DB.Connection -> UserId -> GroupMember -> ConnId -> VersionChat -> VersionRangeChat -> SubscriptionMode -> IO ()
|
||||
createMemberConnection :: DB.Connection -> UserId -> GroupMember -> ConnId -> VersionChat -> VersionRangeChat -> SubscriptionMode -> IO Connection
|
||||
createMemberConnection db userId GroupMember {groupMemberId} agentConnId chatV peerChatVRange subMode = do
|
||||
currentTs <- getCurrentTime
|
||||
void $ createMemberConnection_ db userId groupMemberId agentConnId chatV peerChatVRange Nothing 0 currentTs subMode
|
||||
createMemberConnection_ db userId groupMemberId agentConnId chatV peerChatVRange Nothing 0 currentTs subMode
|
||||
|
||||
createMemberConnectionAsync :: DB.Connection -> User -> GroupMemberId -> (CommandId, ConnId) -> VersionChat -> VersionRangeChat -> SubscriptionMode -> IO ()
|
||||
createMemberConnectionAsync db user@User {userId} groupMemberId (cmdId, agentConnId) chatV peerChatVRange subMode = do
|
||||
@@ -1090,20 +1090,33 @@ createIntroductions db chatV members toMember = do
|
||||
then pure []
|
||||
else do
|
||||
currentTs <- getCurrentTime
|
||||
mapM (insertIntro_ currentTs) reMembers
|
||||
catMaybes <$> mapM (createIntro_ currentTs) reMembers
|
||||
where
|
||||
insertIntro_ :: UTCTime -> GroupMember -> IO GroupMemberIntro
|
||||
insertIntro_ ts reMember = do
|
||||
DB.execute
|
||||
db
|
||||
[sql|
|
||||
INSERT INTO group_member_intros
|
||||
(re_group_member_id, to_group_member_id, intro_status, intro_chat_protocol_version, created_at, updated_at)
|
||||
VALUES (?,?,?,?,?,?)
|
||||
|]
|
||||
(groupMemberId' reMember, groupMemberId' toMember, GMIntroPending, chatV, ts, ts)
|
||||
introId <- insertedRowId db
|
||||
pure GroupMemberIntro {introId, reMember, toMember, introStatus = GMIntroPending, introInvitation = Nothing}
|
||||
createIntro_ :: UTCTime -> GroupMember -> IO (Maybe GroupMemberIntro)
|
||||
createIntro_ ts reMember =
|
||||
-- when members connect concurrently, host would try to create introductions between them in both directions;
|
||||
-- this check avoids creating second (redundant) introduction
|
||||
checkInverseIntro >>= \case
|
||||
Just _ -> pure Nothing
|
||||
Nothing -> do
|
||||
DB.execute
|
||||
db
|
||||
[sql|
|
||||
INSERT INTO group_member_intros
|
||||
(re_group_member_id, to_group_member_id, intro_status, intro_chat_protocol_version, created_at, updated_at)
|
||||
VALUES (?,?,?,?,?,?)
|
||||
|]
|
||||
(groupMemberId' reMember, groupMemberId' toMember, GMIntroPending, chatV, ts, ts)
|
||||
introId <- insertedRowId db
|
||||
pure $ Just GroupMemberIntro {introId, reMember, toMember, introStatus = GMIntroPending, introInvitation = Nothing}
|
||||
where
|
||||
checkInverseIntro :: IO (Maybe Int64)
|
||||
checkInverseIntro =
|
||||
maybeFirstRow fromOnly $
|
||||
DB.query
|
||||
db
|
||||
"SELECT 1 FROM group_member_intros WHERE re_group_member_id = ? AND to_group_member_id = ? LIMIT 1"
|
||||
(groupMemberId' toMember, groupMemberId' reMember)
|
||||
|
||||
updateIntroStatus :: DB.Connection -> Int64 -> GroupMemberIntroStatus -> IO ()
|
||||
updateIntroStatus db introId introStatus = do
|
||||
@@ -1313,7 +1326,7 @@ getViaGroupMember db vr User {userId, userContactId} Contact {contactId} =
|
||||
m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences,
|
||||
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
|
||||
c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id,
|
||||
c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter,
|
||||
c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter,
|
||||
c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version
|
||||
FROM group_members m
|
||||
JOIN contacts ct ON ct.contact_id = m.contact_id
|
||||
@@ -1957,7 +1970,8 @@ createMemberContact
|
||||
pqEncryption = PQEncOff,
|
||||
pqSndEnabled = Nothing,
|
||||
pqRcvEnabled = Nothing,
|
||||
authErrCounter = 0
|
||||
authErrCounter = 0,
|
||||
quotaErrCounter = 0
|
||||
}
|
||||
mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito ctConn
|
||||
pure Contact {contactId, localDisplayName, profile = memberProfile, activeConn = Just ctConn, viaGroup = Nothing, contactUsed = True, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, contactGroupMemberId = Just groupMemberId, contactGrpInvSent = False, uiThemes = Nothing, chatDeleted = False, customData = Nothing}
|
||||
@@ -2090,7 +2104,8 @@ createMemberContactConn_
|
||||
pqEncryption = PQEncOff,
|
||||
pqSndEnabled = Nothing,
|
||||
pqRcvEnabled = Nothing,
|
||||
authErrCounter = 0
|
||||
authErrCounter = 0,
|
||||
quotaErrCounter = 0
|
||||
}
|
||||
|
||||
updateMemberProfile :: DB.Connection -> User -> GroupMember -> Profile -> ExceptT StoreError IO GroupMember
|
||||
|
||||
@@ -23,6 +23,7 @@ module Simplex.Chat.Store.Messages
|
||||
createNewSndMessage,
|
||||
createSndMsgDelivery,
|
||||
createNewMessageAndRcvMsgDelivery,
|
||||
getLastRcvMsgInfo,
|
||||
createNewRcvMessage,
|
||||
updateSndMsgDeliveryStatus,
|
||||
createPendingGroupMessage,
|
||||
@@ -226,6 +227,23 @@ createNewMessageAndRcvMsgDelivery db connOrGroupId newMessage sharedMsgId_ RcvMs
|
||||
(msgId, connId, agentMsgId, msgMetaJson agentMsgMeta, snd $ broker agentMsgMeta, currentTs, currentTs, MDSRcvAgent)
|
||||
pure msg
|
||||
|
||||
getLastRcvMsgInfo :: DB.Connection -> Int64 -> IO (Maybe RcvMsgInfo)
|
||||
getLastRcvMsgInfo db connId =
|
||||
maybeFirstRow rcvMsgInfo $
|
||||
DB.query
|
||||
db
|
||||
[sql|
|
||||
SELECT message_id, msg_delivery_id, delivery_status, agent_msg_id, agent_msg_meta
|
||||
FROM msg_deliveries
|
||||
WHERE connection_id = ? AND delivery_status IN (?, ?)
|
||||
ORDER BY created_at DESC, msg_delivery_id DESC
|
||||
LIMIT 1
|
||||
|]
|
||||
(connId, MDSRcvAgent, MDSRcvAcknowledged)
|
||||
where
|
||||
rcvMsgInfo (msgId, msgDeliveryId, msgDeliveryStatus, agentMsgId, agentMsgMeta) =
|
||||
RcvMsgInfo {msgId, msgDeliveryId, msgDeliveryStatus, agentMsgId, agentMsgMeta}
|
||||
|
||||
createNewRcvMessage :: forall e. MsgEncodingI e => DB.Connection -> ConnOrGroupId -> NewRcvMessage e -> Maybe SharedMsgId -> Maybe GroupMemberId -> Maybe GroupMemberId -> ExceptT StoreError IO RcvMessage
|
||||
createNewRcvMessage db connOrGroupId NewRcvMessage {chatMsgEvent, msgBody} sharedMsgId_ authorMember forwardedByMember =
|
||||
case connOrGroupId of
|
||||
@@ -285,22 +303,22 @@ createPendingGroupMessage db groupMemberId messageId introId_ = do
|
||||
|]
|
||||
(groupMemberId, messageId, introId_, currentTs, currentTs)
|
||||
|
||||
getPendingGroupMessages :: DB.Connection -> Int64 -> IO [PendingGroupMessage]
|
||||
getPendingGroupMessages :: DB.Connection -> Int64 -> IO [(SndMessage, ACMEventTag, Maybe Int64)]
|
||||
getPendingGroupMessages db groupMemberId =
|
||||
map pendingGroupMessage
|
||||
<$> DB.query
|
||||
db
|
||||
[sql|
|
||||
SELECT pgm.message_id, m.chat_msg_event, m.msg_body, pgm.group_member_intro_id
|
||||
SELECT pgm.message_id, m.shared_msg_id, m.msg_body, m.chat_msg_event, pgm.group_member_intro_id
|
||||
FROM pending_group_messages pgm
|
||||
JOIN messages m USING (message_id)
|
||||
WHERE pgm.group_member_id = ?
|
||||
ORDER BY pgm.message_id ASC
|
||||
ORDER BY pgm.created_at ASC, pgm.message_id ASC
|
||||
|]
|
||||
(Only groupMemberId)
|
||||
where
|
||||
pendingGroupMessage (msgId, cmEventTag, msgBody, introId_) =
|
||||
PendingGroupMessage {msgId, cmEventTag, msgBody, introId_}
|
||||
pendingGroupMessage (msgId, sharedMsgId, msgBody, cmEventTag, introId_) =
|
||||
(SndMessage {msgId, sharedMsgId, msgBody}, cmEventTag, introId_)
|
||||
|
||||
deletePendingGroupMessage :: DB.Connection -> Int64 -> MessageId -> IO ()
|
||||
deletePendingGroupMessage db groupMemberId messageId =
|
||||
@@ -838,7 +856,7 @@ toLocalChatItem currentTs ((itemId, itemTs, AMsgDirection msgDir, itemContentTex
|
||||
ciMeta content status =
|
||||
let itemDeleted' = case itemDeleted of
|
||||
DBCINotDeleted -> Nothing
|
||||
_ -> Just (CIDeleted @CTLocal deletedTs)
|
||||
_ -> Just (CIDeleted @'CTLocal deletedTs)
|
||||
itemEdited' = fromMaybe False itemEdited
|
||||
itemForwarded = toCIForwardedFrom forwardedFromRow
|
||||
in mkCIMeta itemId content itemText status sentViaProxy sharedMsgId itemForwarded itemDeleted' itemEdited' ciTimed itemLive currentTs itemTs Nothing createdAt updatedAt
|
||||
@@ -1458,7 +1476,7 @@ toDirectChatItem currentTs (((itemId, itemTs, AMsgDirection msgDir, itemContentT
|
||||
ciMeta content status =
|
||||
let itemDeleted' = case itemDeleted of
|
||||
DBCINotDeleted -> Nothing
|
||||
_ -> Just (CIDeleted @CTDirect deletedTs)
|
||||
_ -> Just (CIDeleted @'CTDirect deletedTs)
|
||||
itemEdited' = fromMaybe False itemEdited
|
||||
itemForwarded = toCIForwardedFrom forwardedFromRow
|
||||
in mkCIMeta itemId content itemText status sentViaProxy sharedMsgId itemForwarded itemDeleted' itemEdited' ciTimed itemLive currentTs itemTs Nothing createdAt updatedAt
|
||||
@@ -1520,7 +1538,7 @@ toGroupChatItem currentTs userContactId (((itemId, itemTs, AMsgDirection msgDir,
|
||||
DBCINotDeleted -> Nothing
|
||||
DBCIBlocked -> Just (CIBlocked deletedTs)
|
||||
DBCIBlockedByAdmin -> Just (CIBlockedByAdmin deletedTs)
|
||||
_ -> Just (maybe (CIDeleted @CTGroup deletedTs) (CIModerated deletedTs) deletedByGroupMember_)
|
||||
_ -> Just (maybe (CIDeleted @'CTGroup deletedTs) (CIModerated deletedTs) deletedByGroupMember_)
|
||||
itemEdited' = fromMaybe False itemEdited
|
||||
itemForwarded = toCIForwardedFrom forwardedFromRow
|
||||
in mkCIMeta itemId content itemText status sentViaProxy sharedMsgId itemForwarded itemDeleted' itemEdited' ciTimed itemLive currentTs itemTs forwardedByMember createdAt updatedAt
|
||||
@@ -1919,7 +1937,7 @@ markGroupChatItemDeleted db User {userId} GroupInfo {groupId} ci@ChatItem {meta}
|
||||
let itemId = chatItemId' ci
|
||||
(deletedByGroupMemberId, itemDeleted) = case byGroupMember_ of
|
||||
Just m@GroupMember {groupMemberId} -> (Just groupMemberId, Just $ CIModerated (Just deletedTs) m)
|
||||
_ -> (Nothing, Just $ CIDeleted @CTGroup (Just deletedTs))
|
||||
_ -> (Nothing, Just $ CIDeleted @'CTGroup (Just deletedTs))
|
||||
insertChatItemMessage_ db itemId msgId currentTs
|
||||
DB.execute
|
||||
db
|
||||
|
||||
@@ -109,6 +109,7 @@ import Simplex.Chat.Migrations.M20240430_ui_theme
|
||||
import Simplex.Chat.Migrations.M20240501_chat_deleted
|
||||
import Simplex.Chat.Migrations.M20240510_chat_items_via_proxy
|
||||
import Simplex.Chat.Migrations.M20240515_rcv_files_user_approved_relays
|
||||
import Simplex.Chat.Migrations.M20240528_quota_err_counter
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..))
|
||||
|
||||
schemaMigrations :: [(String, Query, Maybe Query)]
|
||||
@@ -217,7 +218,8 @@ schemaMigrations =
|
||||
("20240430_ui_theme", m20240430_ui_theme, Just down_m20240430_ui_theme),
|
||||
("20240501_chat_deleted", m20240501_chat_deleted, Just down_m20240501_chat_deleted),
|
||||
("20240510_chat_items_via_proxy", m20240510_chat_items_via_proxy, Just down_m20240510_chat_items_via_proxy),
|
||||
("20240515_rcv_files_user_approved_relays", m20240515_rcv_files_user_approved_relays, Just down_m20240515_rcv_files_user_approved_relays)
|
||||
("20240515_rcv_files_user_approved_relays", m20240515_rcv_files_user_approved_relays, Just down_m20240515_rcv_files_user_approved_relays),
|
||||
("20240528_quota_err_counter", m20240528_quota_err_counter, Just down_m20240528_quota_err_counter)
|
||||
]
|
||||
|
||||
-- | The list of migrations in ascending order by date
|
||||
|
||||
@@ -342,7 +342,7 @@ getUserAddressConnections db vr User {userId} = do
|
||||
[sql|
|
||||
SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
|
||||
c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id,
|
||||
c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter,
|
||||
c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter,
|
||||
c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version
|
||||
FROM connections c
|
||||
JOIN user_contact_links uc ON c.user_contact_link_id = uc.user_contact_link_id
|
||||
@@ -358,7 +358,7 @@ getUserContactLinks db vr User {userId} =
|
||||
[sql|
|
||||
SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
|
||||
c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id,
|
||||
c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter,
|
||||
c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter,
|
||||
c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version,
|
||||
uc.user_contact_link_id, uc.conn_req_contact, uc.group_id
|
||||
FROM connections c
|
||||
|
||||
@@ -163,12 +163,12 @@ toFileInfo (fileId, fileStatus, filePath) = CIFileInfo {fileId, fileStatus, file
|
||||
|
||||
type EntityIdsRow = (Maybe Int64, Maybe Int64, Maybe Int64, Maybe Int64, Maybe Int64)
|
||||
|
||||
type ConnectionRow = (Int64, ConnId, Int, Maybe Int64, Maybe Int64, Bool, Maybe GroupLinkId, Maybe Int64, ConnStatus, ConnType, Bool, LocalAlias) :. EntityIdsRow :. (UTCTime, Maybe Text, Maybe UTCTime, PQSupport, PQEncryption, Maybe PQEncryption, Maybe PQEncryption, Int, Maybe VersionChat, VersionChat, VersionChat)
|
||||
type ConnectionRow = (Int64, ConnId, Int, Maybe Int64, Maybe Int64, Bool, Maybe GroupLinkId, Maybe Int64, ConnStatus, ConnType, Bool, LocalAlias) :. EntityIdsRow :. (UTCTime, Maybe Text, Maybe UTCTime, PQSupport, PQEncryption, Maybe PQEncryption, Maybe PQEncryption, Int, Int, Maybe VersionChat, VersionChat, VersionChat)
|
||||
|
||||
type MaybeConnectionRow = (Maybe Int64, Maybe ConnId, Maybe Int, Maybe Int64, Maybe Int64, Maybe Bool, Maybe GroupLinkId, Maybe Int64, Maybe ConnStatus, Maybe ConnType, Maybe Bool, Maybe LocalAlias) :. EntityIdsRow :. (Maybe UTCTime, Maybe Text, Maybe UTCTime, Maybe PQSupport, Maybe PQEncryption, Maybe PQEncryption, Maybe PQEncryption, Maybe Int, Maybe VersionChat, Maybe VersionChat, Maybe VersionChat)
|
||||
type MaybeConnectionRow = (Maybe Int64, Maybe ConnId, Maybe Int, Maybe Int64, Maybe Int64, Maybe Bool, Maybe GroupLinkId, Maybe Int64, Maybe ConnStatus, Maybe ConnType, Maybe Bool, Maybe LocalAlias) :. EntityIdsRow :. (Maybe UTCTime, Maybe Text, Maybe UTCTime, Maybe PQSupport, Maybe PQEncryption, Maybe PQEncryption, Maybe PQEncryption, Maybe Int, Maybe Int, Maybe VersionChat, Maybe VersionChat, Maybe VersionChat)
|
||||
|
||||
toConnection :: VersionRangeChat -> ConnectionRow -> Connection
|
||||
toConnection vr ((connId, acId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, contactConnInitiated, localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (createdAt, code_, verifiedAt_, pqSupport, pqEncryption, pqSndEnabled, pqRcvEnabled, authErrCounter, chatV, minVer, maxVer)) =
|
||||
toConnection vr ((connId, acId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, contactConnInitiated, localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (createdAt, code_, verifiedAt_, pqSupport, pqEncryption, pqSndEnabled, pqRcvEnabled, authErrCounter, quotaErrCounter, chatV, minVer, maxVer)) =
|
||||
Connection
|
||||
{ connId,
|
||||
agentConnId = AgentConnId acId,
|
||||
@@ -191,6 +191,7 @@ toConnection vr ((connId, acId, connLevel, viaContact, viaUserContactLink, viaGr
|
||||
pqSndEnabled,
|
||||
pqRcvEnabled,
|
||||
authErrCounter,
|
||||
quotaErrCounter,
|
||||
createdAt
|
||||
}
|
||||
where
|
||||
@@ -203,8 +204,8 @@ toConnection vr ((connId, acId, connLevel, viaContact, viaUserContactLink, viaGr
|
||||
entityId_ ConnUserContact = userContactLinkId
|
||||
|
||||
toMaybeConnection :: VersionRangeChat -> MaybeConnectionRow -> Maybe Connection
|
||||
toMaybeConnection vr ((Just connId, Just agentConnId, Just connLevel, viaContact, viaUserContactLink, Just viaGroupLink, groupLinkId, customUserProfileId, Just connStatus, Just connType, Just contactConnInitiated, Just localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (Just createdAt, code_, verifiedAt_, Just pqSupport, Just pqEncryption, pqSndEnabled_, pqRcvEnabled_, Just authErrCounter, connChatVersion, Just minVer, Just maxVer)) =
|
||||
Just $ toConnection vr ((connId, agentConnId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, contactConnInitiated, localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (createdAt, code_, verifiedAt_, pqSupport, pqEncryption, pqSndEnabled_, pqRcvEnabled_, authErrCounter, connChatVersion, minVer, maxVer))
|
||||
toMaybeConnection vr ((Just connId, Just agentConnId, Just connLevel, viaContact, viaUserContactLink, Just viaGroupLink, groupLinkId, customUserProfileId, Just connStatus, Just connType, Just contactConnInitiated, Just localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (Just createdAt, code_, verifiedAt_, Just pqSupport, Just pqEncryption, pqSndEnabled_, pqRcvEnabled_, Just authErrCounter, Just quotaErrCounter, connChatVersion, Just minVer, Just maxVer)) =
|
||||
Just $ toConnection vr ((connId, agentConnId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, contactConnInitiated, localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (createdAt, code_, verifiedAt_, pqSupport, pqEncryption, pqSndEnabled_, pqRcvEnabled_, authErrCounter, quotaErrCounter, connChatVersion, minVer, maxVer))
|
||||
toMaybeConnection _ _ = Nothing
|
||||
|
||||
createConnection_ :: DB.Connection -> UserId -> ConnType -> Maybe Int64 -> ConnId -> VersionChat -> VersionRangeChat -> Maybe ContactId -> Maybe Int64 -> Maybe ProfileId -> Int -> UTCTime -> SubscriptionMode -> PQSupport -> IO Connection
|
||||
@@ -249,7 +250,8 @@ createConnection_ db userId connType entityId acId connChatVersion peerChatVRang
|
||||
pqEncryption = CR.pqSupportToEnc pqSup,
|
||||
pqSndEnabled = Nothing,
|
||||
pqRcvEnabled = Nothing,
|
||||
authErrCounter = 0
|
||||
authErrCounter = 0,
|
||||
quotaErrCounter = 0
|
||||
}
|
||||
where
|
||||
ent ct = if connType == ct then entityId else Nothing
|
||||
|
||||
@@ -6,15 +6,16 @@ module Simplex.Chat.Terminal.Main where
|
||||
import Control.Concurrent (forkIO, threadDelay)
|
||||
import Control.Concurrent.STM
|
||||
import Control.Monad
|
||||
import Data.Maybe (fromMaybe)
|
||||
import Data.Time.Clock (getCurrentTime)
|
||||
import Data.Time.LocalTime (getCurrentTimeZone)
|
||||
import Network.Socket
|
||||
import Simplex.Chat.Controller (ChatConfig, ChatController (..), ChatResponse (..), currentRemoteHost, versionNumber, versionString)
|
||||
import Simplex.Chat.Controller (ChatConfig, ChatController (..), ChatResponse (..), SimpleNetCfg (..), currentRemoteHost, versionNumber, versionString)
|
||||
import Simplex.Chat.Core
|
||||
import Simplex.Chat.Options
|
||||
import Simplex.Chat.Terminal
|
||||
import Simplex.Chat.View (serializeChatResponse)
|
||||
import Simplex.Messaging.Client (NetworkConfig (..))
|
||||
import Simplex.Chat.View (serializeChatResponse, smpProxyModeStr)
|
||||
import Simplex.Messaging.Client (NetworkConfig (..), defaultNetworkConfig)
|
||||
import System.Directory (getAppUserDataDirectory)
|
||||
import System.Exit (exitFailure)
|
||||
import System.Terminal (withTerminal)
|
||||
@@ -51,7 +52,7 @@ simplexChatCLI cfg server_ = do
|
||||
putStrLn $ serializeChatResponse (rh, Just user) ts tz rh r
|
||||
|
||||
welcome :: ChatOpts -> IO ()
|
||||
welcome ChatOpts {coreOptions = CoreChatOpts {dbFilePrefix, networkConfig}} =
|
||||
welcome ChatOpts {coreOptions = CoreChatOpts {dbFilePrefix, simpleNetCfg = SimpleNetCfg {socksProxy, smpProxyMode_, smpProxyFallback_}}} =
|
||||
mapM_
|
||||
putStrLn
|
||||
[ versionString versionNumber,
|
||||
@@ -59,6 +60,9 @@ welcome ChatOpts {coreOptions = CoreChatOpts {dbFilePrefix, networkConfig}} =
|
||||
maybe
|
||||
"direct network connection - use `/network` command or `-x` CLI option to connect via SOCKS5 at :9050"
|
||||
(("using SOCKS5 proxy " <>) . show)
|
||||
(socksProxy networkConfig),
|
||||
socksProxy,
|
||||
smpProxyModeStr
|
||||
(fromMaybe (smpProxyMode defaultNetworkConfig) smpProxyMode_)
|
||||
(fromMaybe (smpProxyFallback defaultNetworkConfig) smpProxyFallback_),
|
||||
"type \"/help\" or \"/h\" for usage info"
|
||||
]
|
||||
|
||||
@@ -1303,6 +1303,7 @@ data Connection = Connection
|
||||
pqSndEnabled :: Maybe PQEncryption,
|
||||
pqRcvEnabled :: Maybe PQEncryption,
|
||||
authErrCounter :: Int,
|
||||
quotaErrCounter :: Int, -- if exceeds limit messages to group members are created as pending; sending to contacts is unaffected by this
|
||||
createdAt :: UTCTime
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
@@ -1316,6 +1317,15 @@ authErrDisableCount = 10
|
||||
connDisabled :: Connection -> Bool
|
||||
connDisabled Connection {authErrCounter} = authErrCounter >= authErrDisableCount
|
||||
|
||||
quotaErrInactiveCount :: Int
|
||||
quotaErrInactiveCount = 5
|
||||
|
||||
quotaErrSetOnMERR :: Int
|
||||
quotaErrSetOnMERR = 999
|
||||
|
||||
connInactive :: Connection -> Bool
|
||||
connInactive Connection {quotaErrCounter} = quotaErrCounter >= quotaErrInactiveCount
|
||||
|
||||
data SecurityCode = SecurityCode {securityCode :: Text, verifiedAt :: UTCTime}
|
||||
deriving (Eq, Show)
|
||||
|
||||
|
||||
@@ -128,13 +128,16 @@ data UIColors = UIColors
|
||||
background :: Maybe UIColor,
|
||||
menus :: Maybe UIColor,
|
||||
title :: Maybe UIColor,
|
||||
accentVariant2 :: Maybe UIColor,
|
||||
sentMessage :: Maybe UIColor,
|
||||
receivedMessage :: Maybe UIColor
|
||||
sentReply :: Maybe UIColor,
|
||||
receivedMessage :: Maybe UIColor,
|
||||
receivedReply :: Maybe UIColor
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
defaultUIColors :: UIColors
|
||||
defaultUIColors = UIColors Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing
|
||||
defaultUIColors = UIColors Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing
|
||||
|
||||
newtype UIColor = UIColor String
|
||||
deriving (Eq, Show)
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
{-# LANGUAGE FlexibleInstances #-}
|
||||
{-# LANGUAGE InstanceSigs #-}
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
{-# OPTIONS_GHC -Wno-orphans #-}
|
||||
|
||||
module Simplex.Chat.Util (week, encryptFile, chunkSize, liftIOEither, shuffle) where
|
||||
|
||||
import Control.Exception (Exception)
|
||||
import Control.Monad
|
||||
import Control.Monad.Except
|
||||
import Control.Monad.IO.Class
|
||||
import Control.Monad.IO.Unlift (MonadUnliftIO (..))
|
||||
import Control.Monad.Reader
|
||||
import Data.Bifunctor (first)
|
||||
import qualified Data.ByteString.Lazy as LB
|
||||
import Data.List (sortBy)
|
||||
import Data.Ord (comparing)
|
||||
@@ -13,6 +21,7 @@ import Data.Word (Word16)
|
||||
import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..))
|
||||
import qualified Simplex.Messaging.Crypto.File as CF
|
||||
import System.Random (randomRIO)
|
||||
import qualified UnliftIO.Exception as E
|
||||
import UnliftIO.IO (IOMode (..), withFile)
|
||||
|
||||
week :: NominalDiffTime
|
||||
@@ -46,3 +55,24 @@ shuffle xs = map snd . sortBy (comparing fst) <$> mapM (\x -> (,x) <$> random) x
|
||||
liftIOEither :: (MonadIO m, MonadError e m) => IO (Either e a) -> m a
|
||||
liftIOEither a = liftIO a >>= liftEither
|
||||
{-# INLINE liftIOEither #-}
|
||||
|
||||
newtype InternalException e = InternalException {unInternalException :: e}
|
||||
deriving (Eq, Show)
|
||||
|
||||
instance Exception e => Exception (InternalException e)
|
||||
|
||||
instance Exception e => MonadUnliftIO (ExceptT e IO) where
|
||||
{-# INLINE withRunInIO #-}
|
||||
withRunInIO :: ((forall a. ExceptT e IO a -> IO a) -> IO b) -> ExceptT e IO b
|
||||
withRunInIO inner =
|
||||
ExceptT . fmap (first unInternalException) . E.try $
|
||||
withRunInIO $ \run ->
|
||||
inner $ run . (either (E.throwIO . InternalException) pure <=< runExceptT)
|
||||
|
||||
instance Exception e => MonadUnliftIO (ExceptT e (ReaderT r IO)) where
|
||||
{-# INLINE withRunInIO #-}
|
||||
withRunInIO :: ((forall a. ExceptT e (ReaderT r IO) a -> IO a) -> IO b) -> ExceptT e (ReaderT r IO) b
|
||||
withRunInIO inner =
|
||||
withExceptT unInternalException . ExceptT . E.try $
|
||||
withRunInIO $ \run ->
|
||||
inner $ run . (either (E.throwIO . InternalException) pure <=< runExceptT)
|
||||
|
||||
+56
-33
@@ -13,7 +13,6 @@ module Simplex.Chat.View where
|
||||
|
||||
import qualified Data.Aeson as J
|
||||
import qualified Data.Aeson.TH as JQ
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import qualified Data.ByteString.Lazy.Char8 as LB
|
||||
import Data.Char (isSpace, toUpper)
|
||||
@@ -56,6 +55,7 @@ import Simplex.Messaging.Agent.Client (ProtocolTestFailure (..), ProtocolTestSte
|
||||
import Simplex.Messaging.Agent.Env.SQLite (NetworkConfig (..))
|
||||
import Simplex.Messaging.Agent.Protocol
|
||||
import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..))
|
||||
import Simplex.Messaging.Client (SMPProxyMode (..), SMPProxyFallback)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..))
|
||||
import qualified Simplex.Messaging.Crypto.Ratchet as CR
|
||||
@@ -65,7 +65,7 @@ import Simplex.Messaging.Parsers (dropPrefix, taggedObjectJSON)
|
||||
import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType, ProtoServerWithAuth, ProtocolServer (..), ProtocolTypeI, SProtocolType (..))
|
||||
import qualified Simplex.Messaging.Protocol as SMP
|
||||
import Simplex.Messaging.Transport.Client (TransportHost (..))
|
||||
import Simplex.Messaging.Util (bshow, tshow)
|
||||
import Simplex.Messaging.Util (bshow, safeDecodeUtf8, tshow)
|
||||
import Simplex.Messaging.Version hiding (version)
|
||||
import Simplex.RemoteControl.Types (RCCtrlAddress (..))
|
||||
import System.Console.ANSI.Types
|
||||
@@ -90,10 +90,10 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe
|
||||
CRChatRunning -> ["chat is running"]
|
||||
CRChatStopped -> ["chat stopped"]
|
||||
CRChatSuspended -> ["chat suspended"]
|
||||
CRApiChats u chats -> ttyUser u $ if testView then testViewChats chats else [plain . bshow $ J.encode chats]
|
||||
CRApiChats u chats -> ttyUser u $ if testView then testViewChats chats else [viewJSON chats]
|
||||
CRChats chats -> viewChats ts tz chats
|
||||
CRApiChat u chat -> ttyUser u $ if testView then testViewChat chat else [plain . bshow $ J.encode chat]
|
||||
CRApiParsedMarkdown ft -> [plain . bshow $ J.encode ft]
|
||||
CRApiChat u chat -> ttyUser u $ if testView then testViewChat chat else [viewJSON chat]
|
||||
CRApiParsedMarkdown ft -> [viewJSON ft]
|
||||
CRUserProtoServers u userServers -> ttyUser u $ viewUserServers userServers testView
|
||||
CRServerTestResult u srv testFailure -> ttyUser u $ viewServerTestResult srv testFailure
|
||||
CRChatItemTTL u ttl -> ttyUser u $ viewChatItemTTL ttl
|
||||
@@ -101,6 +101,10 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe
|
||||
CRContactInfo u ct cStats customUserProfile -> ttyUser u $ viewContactInfo ct cStats customUserProfile
|
||||
CRGroupInfo u g s -> ttyUser u $ viewGroupInfo g s
|
||||
CRGroupMemberInfo u g m cStats -> ttyUser u $ viewGroupMemberInfo g m cStats
|
||||
CRQueueInfo _ msgInfo qInfo ->
|
||||
[ "last received msg: " <> maybe "none" viewJSON msgInfo,
|
||||
"server queue info: " <> viewJSON qInfo
|
||||
]
|
||||
CRContactSwitchStarted {} -> ["switch started"]
|
||||
CRGroupMemberSwitchStarted {} -> ["switch started"]
|
||||
CRContactSwitchAborted {} -> ["switch aborted"]
|
||||
@@ -220,7 +224,7 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe
|
||||
CRSndFileError u (Just ci) _ e -> ttyUser u $ uploadingFile "error" ci <> [plain e]
|
||||
CRSndFileRcvCancelled u _ ft@SndFileTransfer {recipientDisplayName = c} ->
|
||||
ttyUser u [ttyContact c <> " cancelled receiving " <> sndFile ft]
|
||||
CRStandaloneFileInfo info_ -> maybe ["no file information in URI"] (\j -> [plain . LB.toStrict $ J.encode j]) info_
|
||||
CRStandaloneFileInfo info_ -> maybe ["no file information in URI"] (\j -> [viewJSON j]) info_
|
||||
CRContactConnecting u _ -> ttyUser u []
|
||||
CRContactConnected u ct userCustomProfile -> ttyUser u $ viewContactConnected ct userCustomProfile testView
|
||||
CRContactAnotherClient u c -> ttyUser u [ttyContact' c <> ": contact is connected to another client"]
|
||||
@@ -322,7 +326,7 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe
|
||||
]
|
||||
CRRemoteFileStored rhId (CryptoFile filePath cfArgs_) ->
|
||||
[plain $ "file " <> filePath <> " stored on remote host " <> show rhId]
|
||||
<> maybe [] ((: []) . plain . cryptoFileArgsStr testView) cfArgs_
|
||||
<> maybe [] ((: []) . cryptoFileArgsStr testView) cfArgs_
|
||||
CRRemoteCtrlList cs -> viewRemoteCtrls cs
|
||||
CRRemoteCtrlFound {remoteCtrl = RemoteCtrlInfo {remoteCtrlId, ctrlDeviceName}, ctrlAppInfo_, appVersion, compatible} ->
|
||||
[ ("remote controller " <> sShow remoteCtrlId <> " found: ")
|
||||
@@ -354,8 +358,8 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe
|
||||
in ("Chat queries" : map viewQuery chatQueries) <> [""] <> ("Agent queries" : map viewQuery agentQueries)
|
||||
CRDebugLocks {chatLockName, chatEntityLocks, agentLocks} ->
|
||||
[ maybe "no chat lock" (("chat lock: " <>) . plain) chatLockName,
|
||||
plain $ "chat entity locks: " <> LB.unpack (J.encode chatEntityLocks),
|
||||
plain $ "agent locks: " <> LB.unpack (J.encode agentLocks)
|
||||
"chat entity locks: " <> viewJSON chatEntityLocks,
|
||||
"agent locks: " <> viewJSON agentLocks
|
||||
]
|
||||
CRAgentStats stats -> map (plain . intercalate ",") stats
|
||||
CRAgentSubs {activeSubs, pendingSubs, removedSubs} ->
|
||||
@@ -370,14 +374,19 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe
|
||||
("active subscriptions:" : map sShow activeSubscriptions)
|
||||
<> ("pending subscriptions: " : map sShow pendingSubscriptions)
|
||||
<> ("removed subscriptions: " : map sShow removedSubscriptions)
|
||||
CRAgentWorkersSummary {agentWorkersSummary} -> ["agent workers summary: " <> plain (LB.unpack $ J.encode agentWorkersSummary)]
|
||||
CRAgentWorkersSummary {agentWorkersSummary} -> ["agent workers summary: " <> viewJSON agentWorkersSummary]
|
||||
CRAgentWorkersDetails {agentWorkersDetails} ->
|
||||
[ "agent workers details:",
|
||||
plain . LB.unpack $ J.encode agentWorkersDetails -- this would be huge, but copypastable when has its own line
|
||||
viewJSON agentWorkersDetails -- this would be huge, but copypastable when has its own line
|
||||
]
|
||||
CRAgentMsgCounts {msgCounts} -> ["received messages (total, duplicates):", viewJSON msgCounts]
|
||||
CRAgentQueuesInfo {agentQueuesInfo} ->
|
||||
[ "agent queues info:",
|
||||
plain . LB.unpack $ J.encode agentQueuesInfo
|
||||
]
|
||||
CRAgentMsgCounts {msgCounts} -> ["received messages (total, duplicates):", plain . LB.unpack $ J.encode msgCounts]
|
||||
CRContactDisabled u c -> ttyUser u ["[" <> ttyContact' c <> "] connection is disabled, to enable: " <> highlight ("/enable " <> viewContactName c) <> ", to delete: " <> highlight ("/d " <> viewContactName c)]
|
||||
CRConnectionDisabled entity -> viewConnectionEntityDisabled entity
|
||||
CRConnectionInactive entity inactive -> viewConnectionEntityInactive entity inactive
|
||||
CRAgentRcvQueueDeleted acId srv aqId err_ ->
|
||||
[ ("completed deleting rcv queue, agent connection id: " <> sShow acId)
|
||||
<> (", server: " <> sShow srv)
|
||||
@@ -388,11 +397,11 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe
|
||||
CRAgentConnDeleted acId -> ["completed deleting connection, agent connection id: " <> sShow acId | logLevel <= CLLInfo]
|
||||
CRAgentUserDeleted auId -> ["completed deleting user" <> if logLevel <= CLLInfo then ", agent user id: " <> sShow auId else ""]
|
||||
CRMessageError u prefix err -> ttyUser u [plain prefix <> ": " <> plain err | prefix == "error" || logLevel <= CLLWarning]
|
||||
CRChatCmdError u e -> ttyUserPrefix' u $ viewChatError logLevel testView e
|
||||
CRChatError u e -> ttyUser' u $ viewChatError logLevel testView e
|
||||
CRChatErrors u errs -> ttyUser' u $ concatMap (viewChatError logLevel testView) errs
|
||||
CRChatCmdError u e -> ttyUserPrefix' u $ viewChatError True logLevel testView e
|
||||
CRChatError u e -> ttyUser' u $ viewChatError False logLevel testView e
|
||||
CRChatErrors u errs -> ttyUser' u $ concatMap (viewChatError False logLevel testView) errs
|
||||
CRArchiveImported archiveErrs -> if null archiveErrs then ["ok"] else ["archive import errors: " <> plain (show archiveErrs)]
|
||||
CRAppSettings as -> ["app settings: " <> plain (LB.unpack $ J.encode as)]
|
||||
CRAppSettings as -> ["app settings: " <> viewJSON as]
|
||||
CRTimedAction _ _ -> []
|
||||
CRCustomChatResponse u r -> ttyUser' u $ [plain r]
|
||||
where
|
||||
@@ -1205,12 +1214,17 @@ viewChatItemTTL = \case
|
||||
deletedAfter ttlStr = ["old messages are set to be deleted after: " <> ttlStr]
|
||||
|
||||
viewNetworkConfig :: NetworkConfig -> [StyledString]
|
||||
viewNetworkConfig NetworkConfig {socksProxy, tcpTimeout} =
|
||||
viewNetworkConfig NetworkConfig {socksProxy, tcpTimeout, smpProxyMode, smpProxyFallback} =
|
||||
[ plain $ maybe "direct network connection" (("using SOCKS5 proxy " <>) . show) socksProxy,
|
||||
"TCP timeout: " <> sShow tcpTimeout,
|
||||
"use " <> highlight' "/network socks=<on/off/[ipv4]:port>[ timeout=<seconds>]" <> " to change settings"
|
||||
plain $ smpProxyModeStr smpProxyMode smpProxyFallback,
|
||||
"use " <> highlight' "/network socks=<on/off/[ipv4]:port>[ timeout=<seconds>][ smp-proxy=always/unknown/unprotected/never][ smp-proxy-fallback=no/protected/yes]" <> " to change settings"
|
||||
]
|
||||
|
||||
smpProxyModeStr :: SMPProxyMode -> SMPProxyFallback -> String
|
||||
smpProxyModeStr SPMNever _ = "private message routing disabled."
|
||||
smpProxyModeStr mode fallback = T.unpack $ safeDecodeUtf8 $ "private message routing mode: " <> strEncode mode <> ", fallback: " <> strEncode fallback
|
||||
|
||||
viewContactInfo :: Contact -> Maybe ConnectionStats -> Maybe Profile -> [StyledString]
|
||||
viewContactInfo ct@Contact {contactId, profile = LocalProfile {localAlias, contactLink}, activeConn, uiThemes, customData} stats incognitoProfile =
|
||||
["contact ID: " <> sShow contactId]
|
||||
@@ -1236,10 +1250,10 @@ viewGroupInfo GroupInfo {groupId, uiThemes, customData} s =
|
||||
<> viewCustomData customData
|
||||
|
||||
viewUITheme :: Maybe UIThemeEntityOverrides -> [StyledString]
|
||||
viewUITheme = maybe [] (\uiThemes -> ["UI themes: " <> plain (LB.toStrict $ J.encode uiThemes)])
|
||||
viewUITheme = maybe [] (\uiThemes -> ["UI themes: " <> viewJSON uiThemes])
|
||||
|
||||
viewCustomData :: Maybe CustomData -> [StyledString]
|
||||
viewCustomData = maybe [] (\(CustomData v) -> ["custom data: " <> plain (LB.toStrict . J.encode $ J.Object v)])
|
||||
viewCustomData = maybe [] (\(CustomData v) -> ["custom data: " <> viewJSON (J.Object v)])
|
||||
|
||||
viewGroupMemberInfo :: GroupInfo -> GroupMember -> Maybe ConnectionStats -> [StyledString]
|
||||
viewGroupMemberInfo GroupInfo {groupId} m@GroupMember {groupMemberId, memberProfile = LocalProfile {localAlias, contactLink}, activeConn} stats =
|
||||
@@ -1681,7 +1695,7 @@ receivingFile_' :: (Maybe RemoteHostId, Maybe User) -> Bool -> String -> AChatIt
|
||||
receivingFile_' hu testView status (AChatItem _ _ chat ChatItem {file = Just CIFile {fileId, fileName, fileSource = Just f@(CryptoFile _ cfArgs_)}, chatDir}) =
|
||||
[plain status <> " receiving " <> fileTransferStr fileId fileName <> fileFrom chat chatDir] <> cfArgsStr cfArgs_ <> getRemoteFileStr
|
||||
where
|
||||
cfArgsStr (Just cfArgs) = [plain (cryptoFileArgsStr testView cfArgs) | status == "completed"]
|
||||
cfArgsStr (Just cfArgs) = [cryptoFileArgsStr testView cfArgs | status == "completed"]
|
||||
cfArgsStr _ = []
|
||||
getRemoteFileStr = case hu of
|
||||
(Just rhId, Just User {userId})
|
||||
@@ -1702,10 +1716,10 @@ viewLocalFile to CIFile {fileId, fileSource} ts tz = case fileSource of
|
||||
Just (CryptoFile fPath _) -> sentWithTime_ ts tz [to <> fileTransferStr fileId fPath]
|
||||
_ -> const []
|
||||
|
||||
cryptoFileArgsStr :: Bool -> CryptoFileArgs -> ByteString
|
||||
cryptoFileArgsStr :: Bool -> CryptoFileArgs -> StyledString
|
||||
cryptoFileArgsStr testView cfArgs@(CFArgs key nonce)
|
||||
| testView = LB.toStrict $ J.encode cfArgs
|
||||
| otherwise = "encryption key: " <> strEncode key <> ", nonce: " <> strEncode nonce
|
||||
| testView = viewJSON cfArgs
|
||||
| otherwise = plain $ "encryption key: " <> strEncode key <> ", nonce: " <> strEncode nonce
|
||||
|
||||
fileFrom :: ChatInfo c -> CIDirection c d -> StyledString
|
||||
fileFrom (DirectChat ct) CIDirectRcv = " from " <> ttyContact' ct
|
||||
@@ -1823,7 +1837,7 @@ viewCallAnswer ct WebRTCSession {rtcSession = answer, rtcIceCandidates = iceCand
|
||||
[ ttyContact' ct <> " continued the WebRTC call",
|
||||
"To connect, please paste the data below in your browser window you opened earlier and click Connect button",
|
||||
"",
|
||||
plain . LB.toStrict . J.encode $ WCCallAnswer {answer, iceCandidates}
|
||||
viewJSON WCCallAnswer {answer, iceCandidates}
|
||||
]
|
||||
|
||||
callMediaStr :: CallType -> StyledString
|
||||
@@ -1894,8 +1908,8 @@ viewRemoteCtrl CtrlAppInfo {deviceName, appVersionRange = AppVersionRange _ (App
|
||||
| otherwise = ""
|
||||
showCompatible = if compatible then "" else ", " <> bold' "not compatible"
|
||||
|
||||
viewChatError :: ChatLogLevel -> Bool -> ChatError -> [StyledString]
|
||||
viewChatError logLevel testView = \case
|
||||
viewChatError :: Bool -> ChatLogLevel -> Bool -> ChatError -> [StyledString]
|
||||
viewChatError isCmd logLevel testView = \case
|
||||
ChatError err -> case err of
|
||||
CENoActiveUser -> ["error: active user is required"]
|
||||
CENoConnectionUser agentConnId -> ["error: message user not found, conn id: " <> sShow agentConnId | logLevel <= CLLError]
|
||||
@@ -1953,7 +1967,6 @@ viewChatError logLevel testView = \case
|
||||
]
|
||||
CEGroupMemberUserRemoved -> ["you are no longer a member of the group"]
|
||||
CEGroupMemberNotFound -> ["group doesn't have this member"]
|
||||
CEGroupMemberIntroNotFound c -> ["group member intro not found for " <> ttyContact c]
|
||||
CEGroupCantResendInvitation g c -> viewCannotResendInvitation g c
|
||||
CEGroupInternal s -> ["chat group bug: " <> plain s]
|
||||
CEFileNotFound f -> ["file not found: " <> plain f]
|
||||
@@ -2028,18 +2041,20 @@ viewChatError logLevel testView = \case
|
||||
DBErrorOpen e -> ["error opening database after encryption: " <> sqliteError' e]
|
||||
e -> ["chat database error: " <> sShow e]
|
||||
ChatErrorAgent err entity_ -> case err of
|
||||
CMD PROHIBITED -> [withConnEntity <> "error: command is prohibited"]
|
||||
CMD PROHIBITED cxt -> [withConnEntity <> plain ("error: command is prohibited, " <> cxt)]
|
||||
SMP _ SMP.AUTH ->
|
||||
[ withConnEntity
|
||||
<> "error: connection authorization failed - this could happen if connection was deleted,\
|
||||
\ secured with different credentials, or due to a bug - please re-create the connection"
|
||||
]
|
||||
AGENT A_DUPLICATE -> [withConnEntity <> "error: AGENT A_DUPLICATE" | logLevel == CLLDebug]
|
||||
AGENT A_PROHIBITED -> [withConnEntity <> "error: AGENT A_PROHIBITED" | logLevel <= CLLWarning]
|
||||
CONN NOT_FOUND -> [withConnEntity <> "error: CONN NOT_FOUND" | logLevel <= CLLWarning]
|
||||
BROKER _ NETWORK | not isCmd -> []
|
||||
BROKER _ TIMEOUT | not isCmd -> []
|
||||
AGENT A_DUPLICATE -> [withConnEntity <> "error: AGENT A_DUPLICATE" | logLevel == CLLDebug || isCmd]
|
||||
AGENT (A_PROHIBITED e) -> [withConnEntity <> "error: AGENT A_PROHIBITED, " <> plain e | logLevel <= CLLWarning || isCmd]
|
||||
CONN NOT_FOUND -> [withConnEntity <> "error: CONN NOT_FOUND" | logLevel <= CLLWarning || isCmd]
|
||||
CRITICAL restart e -> [plain $ "critical error: " <> e] <> ["please restart the app" | restart]
|
||||
INTERNAL e -> [plain $ "internal error: " <> e]
|
||||
e -> [withConnEntity <> "smp agent error: " <> sShow e | logLevel <= CLLWarning]
|
||||
e -> [withConnEntity <> "smp agent error: " <> sShow e | logLevel <= CLLWarning || isCmd]
|
||||
where
|
||||
withConnEntity = case entity_ of
|
||||
Just entity@(RcvDirectMsgConnection conn contact_) -> case contact_ of
|
||||
@@ -2075,6 +2090,14 @@ viewConnectionEntityDisabled entity = case entity of
|
||||
where
|
||||
entityLabel = connEntityLabel entity
|
||||
|
||||
viewConnectionEntityInactive :: ConnectionEntity -> Bool -> [StyledString]
|
||||
viewConnectionEntityInactive entity inactive
|
||||
| inactive = ["[" <> connEntityLabel entity <> "] connection is marked as inactive"]
|
||||
| otherwise = ["[" <> connEntityLabel entity <> "] inactive connection is marked as active"]
|
||||
|
||||
viewJSON :: J.ToJSON a => a -> StyledString
|
||||
viewJSON = plain . LB.toStrict . J.encode
|
||||
|
||||
connEntityLabel :: ConnectionEntity -> StyledString
|
||||
connEntityLabel = \case
|
||||
RcvDirectMsgConnection _ (Just Contact {localDisplayName = c}) -> plain c
|
||||
|
||||
Reference in New Issue
Block a user