From d7f9e17bcb95a13c66e42a5a61aa67acc85272ce Mon Sep 17 00:00:00 2001
From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
Date: Mon, 13 Mar 2023 10:30:32 +0000
Subject: [PATCH 01/17] core: use XFTP to send and receive files (#1993)
* core: use XFTP to send and receive files
* xftp files progress
* xftp reception stubs, migration
* update simplexmq
* xftp sequence diagram
* additional chat events
* send file via XFTP
* send XFTP file description inline when file is uploaded
---
cabal.project | 4 +-
docs/protocol/diagrams/xftp.mmd | 42 +++
scripts/nix/sha256map.nix | 4 +-
src/Simplex/Chat.hs | 301 +++++++++++++-----
src/Simplex/Chat/Archive.hs | 2 +-
src/Simplex/Chat/Controller.hs | 30 ++
src/Simplex/Chat/Messages.hs | 67 +++-
.../Migrations/M20230304_file_description.hs | 24 +-
src/Simplex/Chat/Migrations/chat_schema.sql | 20 +-
src/Simplex/Chat/Store.hs | 146 ++++++---
src/Simplex/Chat/Types.hs | 78 ++++-
src/Simplex/Chat/View.hs | 11 +-
stack.yaml | 4 +-
13 files changed, 580 insertions(+), 153 deletions(-)
create mode 100644 docs/protocol/diagrams/xftp.mmd
diff --git a/cabal.project b/cabal.project
index c5c00106e..b9ddbd34d 100644
--- a/cabal.project
+++ b/cabal.project
@@ -7,7 +7,7 @@ constraints: zip +disable-bzip2 +disable-zstd
source-repository-package
type: git
location: https://github.com/simplex-chat/simplexmq.git
- tag: 8fde8e1344699cdcdc67709595c9285cd06bbef3
+ tag: bd4fecf4a84071079cffccfc0f35a916eac0e086
source-repository-package
type: git
@@ -17,7 +17,7 @@ source-repository-package
source-repository-package
type: git
location: https://github.com/kazu-yamamoto/http2.git
- tag: b3b62ba36900babfde1a073c705cbccc2685f385
+ tag: 78e18f52295a7f89e828539a03fbcb24931461a3
source-repository-package
type: git
diff --git a/docs/protocol/diagrams/xftp.mmd b/docs/protocol/diagrams/xftp.mmd
new file mode 100644
index 000000000..af4595207
--- /dev/null
+++ b/docs/protocol/diagrams/xftp.mmd
@@ -0,0 +1,42 @@
+sequenceDiagram
+ participant A as Alice
+ participant AC as Alice Chat
+ participant AA as Alice Agent
+ participant XFTP as Alice's XFTP relay(s)
+ participant SMP as Bob's SMP relay
+ participant BA as Bob Agent
+ participant BC as Bob Chat
+ participant B as Bob
+
+ A ->> AC: APISendMessage
+ AC ->> AA: sendMessage(x.msg.new) /
CIFSSndStored
+ AA ->> SMP: SEND
+ SMP ->> BA: MSG
+ BA ->> BC: MSG
+ BC ->> B: CRNewChatItem
(file not ready)
+ B ->> BC: ReceiveFile
+ BC ->> B: error: no file description
+ AC ->> AA: sendFile
+ AC ->> A: CRSndFileStart
+ AA ->> XFTP: chunk (FNEW, FPUT)
+ AA ->> AC: SFPROG /
CIFSSndTransfer
+ AC ->> A: CRSndFileProgress (new)
+ AA ->> XFTP: chunks
+ AA ->> AC: SFDONE sd rds
+ AC ->> AA: sendMessage(x.msg.file.descr) /
FSComplete / CIFSSndComplete
+ AC ->> A: CRSndFileComplete (?)
+ AA ->> SMP: SEND
+ SMP ->> BA: MSG
+ BA ->> BC: MSG
+ BC ->> B: CRChatItemUpdated
(file is ready)
+ BC ->> B: CRFileReady (TBC)
+ B ->> BC: ReceiveFile
+ BC ->> BA: getFile
+ BC ->> B: CRRcvFileStart
+ XFTP ->> BA: chunk (FGET / FRFile)
+ BA ->> BC: RFPROG
+ BC ->> B: CRRcvFileProgress (new)
+ XFTP ->> BA: chunks
+ BA ->> BC: RFDONE
+ BC ->> B: CRNewChatItem
(file received)
+ BC ->> B: CRRcvFileComplete
diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix
index c94510158..bb3d71813 100644
--- a/scripts/nix/sha256map.nix
+++ b/scripts/nix/sha256map.nix
@@ -1,7 +1,7 @@
{
- "https://github.com/simplex-chat/simplexmq.git"."8fde8e1344699cdcdc67709595c9285cd06bbef3" = "1nvxmmfq3k1a8l14lksxdsqzxq19kmvg2kpiryqdks3k946x6pzn";
+ "https://github.com/simplex-chat/simplexmq.git"."bd4fecf4a84071079cffccfc0f35a916eac0e086" = "11sp91znlnfflilw0gdd64f4z6y9ni88iv7xjrdkyj6yhjqfa4wr";
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
- "https://github.com/kazu-yamamoto/http2.git"."b3b62ba36900babfde1a073c705cbccc2685f385" = "076gl9mcm9gxcif5662g5ar0pd817657mc46y99ighria3z36cmz";
+ "https://github.com/kazu-yamamoto/http2.git"."78e18f52295a7f89e828539a03fbcb24931461a3" = "05q165anvv0qrcxqbvq1dlvw0l8gmsa9kl6sazk1mfhz2g0yimdk";
"https://github.com/simplex-chat/direct-sqlcipher.git"."34309410eb2069b029b8fc1872deb1e0db123294" = "0kwkmhyfsn2lixdlgl15smgr1h5gjk7fky6abzh8rng2h5ymnffd";
"https://github.com/simplex-chat/sqlcipher-simple.git"."5e154a2aeccc33ead6c243ec07195ab673137221" = "1d1gc5wax4vqg0801ajsmx1sbwvd9y7p7b8mmskvqsmpbwgbh0m0";
"https://github.com/simplex-chat/aeson.git"."3eb66f9a68f103b5f1489382aad89f5712a64db7" = "0kilkx59fl6c3qy3kjczqvm8c3f4n3p0bdk9biyflf51ljnzp4yp";
diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs
index e2f31b94a..f50423069 100644
--- a/src/Simplex/Chat.hs
+++ b/src/Simplex/Chat.hs
@@ -69,7 +69,7 @@ import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Encoding
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (base64P)
-import Simplex.Messaging.Protocol (ErrorType (..), MsgBody, MsgFlags (..), NtfServer, ProtoServerWithAuth, ProtocolType (..), ProtocolTypeI)
+import Simplex.Messaging.Protocol (EntityId, ErrorType (..), MsgBody, MsgFlags (..), NtfServer, ProtoServerWithAuth, ProtocolType (..), ProtocolTypeI)
import qualified Simplex.Messaging.Protocol as SMP
import qualified Simplex.Messaging.TMap as TM
import Simplex.Messaging.Transport.Client (defaultSocksProxy)
@@ -105,7 +105,9 @@ defaultChatConfig =
},
tbqSize = 1024,
fileChunkSize = 15780, -- do not change
+ xftpDescrPartSize = 14000,
inlineFiles = defaultInlineFilesConfig,
+ xftpFileConfig = Nothing,
logLevel = CLLImportant,
subscriptionEvents = False,
hostEvents = False,
@@ -165,7 +167,8 @@ newChatController ChatDatabase {chatStore, agentStore} user cfg@ChatConfig {agen
cleanupManagerAsync <- newTVarIO Nothing
timedItemThreads <- atomically TM.empty
showLiveItems <- newTVarIO False
- pure ChatController {activeTo, firstTime, currentUser, smpAgent, agentAsync, chatStore, chatStoreChanged, idsDrg, inputQ, outputQ, notifyQ, chatLock, sndFiles, rcvFiles, currentCalls, config, sendNotification, incognitoMode, filesFolder, expireCIThreads, expireCIFlags, cleanupManagerAsync, timedItemThreads, showLiveItems, logFilePath = logFile}
+ userXFTPFileConfig <- newTVarIO $ xftpFileConfig cfg
+ pure ChatController {activeTo, firstTime, currentUser, smpAgent, agentAsync, chatStore, chatStoreChanged, idsDrg, inputQ, outputQ, notifyQ, chatLock, sndFiles, rcvFiles, currentCalls, config, sendNotification, incognitoMode, filesFolder, expireCIThreads, expireCIFlags, cleanupManagerAsync, timedItemThreads, showLiveItems, userXFTPFileConfig, logFilePath = logFile}
where
configServers :: DefaultAgentServers
configServers =
@@ -380,9 +383,9 @@ processChatCommand = \case
if isVoice mc && not (featureAllowed SCFVoice forUser ct)
then pure $ chatCmdError (Just user) ("feature not allowed " <> T.unpack (chatFeatureNameText CFVoice))
else do
- (fileInvitation_, ciFile_, ft_) <- unzipMaybe3 <$> setupSndFileTransfer ct
+ (fInv_, ciFile_, ft_) <- unzipMaybe3 <$> setupSndFileTransfer ct
timed_ <- sndContactCITimed live ct
- (msgContainer, quotedItem_) <- prepareMsg fileInvitation_ timed_
+ (msgContainer, quotedItem_) <- prepareMsg fInv_ timed_
(msg@SndMessage {sharedMsgId}, _) <- sendDirectContactMessage ct (XMsgNew msgContainer)
case ft_ of
Just ft@FileTransferMeta {fileInline = Just IFMSent} ->
@@ -396,23 +399,30 @@ processChatCommand = \case
where
setupSndFileTransfer :: Contact -> m (Maybe (FileInvitation, CIFile 'MDSnd, FileTransferMeta))
setupSndFileTransfer ct = forM file_ $ \file -> do
- (fileSize, chSize, fileInline) <- checkSndFile mc file 1
- (agentConnId_, fileConnReq) <-
- if isJust fileInline
- then pure (Nothing, Nothing)
- else bimap Just Just <$> withAgent (\a -> createConnection a (aUserId user) True SCMInvitation Nothing)
- let fileName = takeFileName file
- fileInvitation = FileInvitation {fileName, fileSize, fileDigest = Nothing, fileConnReq, fileInline, fileDescr = Nothing}
- withStore' $ \db -> do
- ft@FileTransferMeta {fileId} <- createSndDirectFileTransfer db userId ct file fileInvitation agentConnId_ chSize
- fileStatus <- case fileInline of
- Just IFMSent -> createSndDirectInlineFT db ct ft $> CIFSSndTransfer
- _ -> pure CIFSSndStored
- let ciFile = CIFile {fileId, fileName, fileSize, filePath = Just file, fileStatus}
- pure (fileInvitation, ciFile, ft)
+ (fileSize, fileMode) <- checkSndFile mc file 1
+ case fileMode of
+ SendFileSMP fileInline -> smpSndFileTransfer file fileSize fileInline
+ SendFileXFTP xftpCfg -> xftpSndFileTransfer user file fileSize xftpCfg 1 $ CGContact ct
+ where
+ smpSndFileTransfer :: FilePath -> Integer -> Maybe InlineFileMode -> m (FileInvitation, CIFile 'MDSnd, FileTransferMeta)
+ smpSndFileTransfer file fileSize fileInline = do
+ (agentConnId_, fileConnReq) <-
+ if isJust fileInline
+ then pure (Nothing, Nothing)
+ else bimap Just Just <$> withAgent (\a -> createConnection a (aUserId user) True SCMInvitation Nothing)
+ let fileName = takeFileName file
+ fileInvitation = FileInvitation {fileName, fileSize, fileDigest = Nothing, fileConnReq, fileInline, fileDescr = Nothing}
+ chSize <- asks $ fileChunkSize . config
+ withStore' $ \db -> do
+ ft@FileTransferMeta {fileId} <- createSndDirectFileTransfer db userId ct file fileInvitation agentConnId_ chSize
+ fileStatus <- case fileInline of
+ Just IFMSent -> createSndDirectInlineFT db ct ft $> CIFSSndTransfer 0 1
+ _ -> pure CIFSSndStored
+ let ciFile = CIFile {fileId, fileName, fileSize, filePath = Just file, fileStatus}
+ pure (fileInvitation, ciFile, ft)
prepareMsg :: Maybe FileInvitation -> Maybe CITimed -> m (MsgContainer, Maybe (CIQuote 'CTDirect))
- prepareMsg fileInvitation_ timed_ = case quotedItemId_ of
- Nothing -> pure (MCSimple (ExtMsgContent mc fileInvitation_ (ttl' <$> timed_) (justTrue live)), Nothing)
+ prepareMsg fInv_ timed_ = case quotedItemId_ of
+ Nothing -> pure (MCSimple (ExtMsgContent mc fInv_ (ttl' <$> timed_) (justTrue live)), Nothing)
Just quotedItemId -> do
CChatItem _ qci@ChatItem {meta = CIMeta {itemTs, itemSharedMsgId}, formattedText, file} <-
withStore $ \db -> getDirectChatItem db user chatId quotedItemId
@@ -420,7 +430,7 @@ processChatCommand = \case
let msgRef = MsgRef {msgId = itemSharedMsgId, sentAt = itemTs, sent, memberId = Nothing}
qmc = quoteContent origQmc file
quotedItem = CIQuote {chatDir = qd, itemId = Just quotedItemId, sharedMsgId = itemSharedMsgId, sentAt = itemTs, content = qmc, formattedText}
- pure (MCQuote QuotedMsg {msgRef, content = qmc} (ExtMsgContent mc fileInvitation_ (ttl' <$> timed_) (justTrue live)), Just quotedItem)
+ pure (MCQuote QuotedMsg {msgRef, content = qmc} (ExtMsgContent mc fInv_ (ttl' <$> timed_) (justTrue live)), Just quotedItem)
where
quoteData :: ChatItem c d -> m (MsgContent, CIQDirection 'CTDirect, Bool)
quoteData ChatItem {meta = CIMeta {itemDeleted = Just _}} = throwChatError CEInvalidQuote
@@ -433,9 +443,9 @@ processChatCommand = \case
if isVoice mc && not (groupFeatureAllowed SGFVoice gInfo)
then pure $ chatCmdError (Just user) ("feature not allowed " <> T.unpack (groupFeatureNameText GFVoice))
else do
- (fileInvitation_, ciFile_, ft_) <- unzipMaybe3 <$> setupSndFileTransfer gInfo (length $ filter memberCurrent ms)
+ (fInv_, ciFile_, ft_) <- unzipMaybe3 <$> setupSndFileTransfer gInfo (length $ filter memberCurrent ms)
timed_ <- sndGroupCITimed live gInfo
- (msgContainer, quotedItem_) <- prepareMsg fileInvitation_ timed_ membership
+ (msgContainer, quotedItem_) <- prepareMsg fInv_ timed_ membership
msg@SndMessage {sharedMsgId} <- sendGroupMessage user gInfo ms (XMsgNew msgContainer)
mapM_ (sendGroupFileInline ms sharedMsgId) ft_
ci <- saveSndChatItem' user (CDGroupSnd gInfo) msg (CISndMsgContent mc) ciFile_ quotedItem_ timed_ live
@@ -446,14 +456,21 @@ processChatCommand = \case
where
setupSndFileTransfer :: GroupInfo -> Int -> m (Maybe (FileInvitation, CIFile 'MDSnd, FileTransferMeta))
setupSndFileTransfer gInfo n = forM file_ $ \file -> do
- (fileSize, chSize, fileInline) <- checkSndFile mc file $ fromIntegral n
- let fileName = takeFileName file
- fileInvitation = FileInvitation {fileName, fileSize, fileDigest = Nothing, fileConnReq = Nothing, fileInline, fileDescr = Nothing}
- fileStatus = if fileInline == Just IFMSent then CIFSSndTransfer else CIFSSndStored
- withStore' $ \db -> do
- ft@FileTransferMeta {fileId} <- createSndGroupFileTransfer db userId gInfo file fileInvitation chSize
- let ciFile = CIFile {fileId, fileName, fileSize, filePath = Just file, fileStatus}
- pure (fileInvitation, ciFile, ft)
+ (fileSize, fileMode) <- checkSndFile mc file $ fromIntegral n
+ case fileMode of
+ SendFileSMP fileInline -> smpSndFileTransfer file fileSize fileInline
+ SendFileXFTP xftpCfg -> xftpSndFileTransfer user file fileSize xftpCfg n $ CGGroup gInfo
+ where
+ smpSndFileTransfer :: FilePath -> Integer -> Maybe InlineFileMode -> m (FileInvitation, CIFile 'MDSnd, FileTransferMeta)
+ smpSndFileTransfer file fileSize fileInline = do
+ let fileName = takeFileName file
+ fileInvitation = FileInvitation {fileName, fileSize, fileDigest = Nothing, fileConnReq = Nothing, fileInline, fileDescr = Nothing}
+ fileStatus = if fileInline == Just IFMSent then CIFSSndTransfer 0 1 else CIFSSndStored
+ chSize <- asks $ fileChunkSize . config
+ withStore' $ \db -> do
+ ft@FileTransferMeta {fileId} <- createSndGroupFileTransfer db userId gInfo file fileInvitation chSize
+ let ciFile = CIFile {fileId, fileName, fileSize, filePath = Just file, fileStatus}
+ pure (fileInvitation, ciFile, ft)
sendGroupFileInline :: [GroupMember] -> SharedMsgId -> FileTransferMeta -> m ()
sendGroupFileInline ms sharedMsgId ft@FileTransferMeta {fileInline} =
when (fileInline == Just IFMSent) . forM_ ms $ \m ->
@@ -465,8 +482,8 @@ processChatCommand = \case
sendMemberFileInline m conn ft sharedMsgId
processMember _ = pure ()
prepareMsg :: Maybe FileInvitation -> Maybe CITimed -> GroupMember -> m (MsgContainer, Maybe (CIQuote 'CTGroup))
- prepareMsg fileInvitation_ timed_ membership = case quotedItemId_ of
- Nothing -> pure (MCSimple (ExtMsgContent mc fileInvitation_ (ttl' <$> timed_) (justTrue live)), Nothing)
+ prepareMsg fInv_ timed_ membership = case quotedItemId_ of
+ Nothing -> pure (MCSimple (ExtMsgContent mc fInv_ (ttl' <$> timed_) (justTrue live)), Nothing)
Just quotedItemId -> do
CChatItem _ qci@ChatItem {meta = CIMeta {itemTs, itemSharedMsgId}, formattedText, file} <-
withStore $ \db -> getGroupChatItem db user chatId quotedItemId
@@ -474,7 +491,7 @@ processChatCommand = \case
let msgRef = MsgRef {msgId = itemSharedMsgId, sentAt = itemTs, sent, memberId = Just memberId}
qmc = quoteContent origQmc file
quotedItem = CIQuote {chatDir = qd, itemId = Just quotedItemId, sharedMsgId = itemSharedMsgId, sentAt = itemTs, content = qmc, formattedText}
- pure (MCQuote QuotedMsg {msgRef, content = qmc} (ExtMsgContent mc fileInvitation_ (ttl' <$> timed_) (justTrue live)), Just quotedItem)
+ pure (MCQuote QuotedMsg {msgRef, content = qmc} (ExtMsgContent mc fInv_ (ttl' <$> timed_) (justTrue live)), Just quotedItem)
where
quoteData :: ChatItem c d -> GroupMember -> m (MsgContent, CIQDirection 'CTGroup, Bool, GroupMember)
quoteData ChatItem {meta = CIMeta {itemDeleted = Just _}} _ = throwChatError CEInvalidQuote
@@ -507,6 +524,14 @@ processChatCommand = \case
qText = msgContentText qmc
qFileName = maybe qText (T.pack . (fileName :: CIFile d -> String)) ciFile_
qTextOrFile = if T.null qText then qFileName else qText
+ xftpSndFileTransfer :: User -> FilePath -> Integer -> XFTPFileConfig -> Int -> ContactOrGroup -> m (FileInvitation, CIFile 'MDSnd, FileTransferMeta)
+ xftpSndFileTransfer user file fileSize XFTPFileConfig {tempDirectory} n contactOrGroup = do
+ let fileName = takeFileName file
+ fInv = xftpFileInvitation fileName fileSize
+ aFileId <- withAgent $ \a -> xftpSendFile a (aUserId user) file n tempDirectory
+ ft@FileTransferMeta {fileId} <- withStore' $ \db -> createSndFileTransferXFTP db user contactOrGroup file fInv $ AgentSndFileId aFileId
+ let ciFile = CIFile {fileId, fileName, fileSize, filePath = Just file, fileStatus = CIFSSndStored}
+ pure (fInv, ciFile, ft)
unzipMaybe3 :: Maybe (a, b, c) -> (Maybe a, Maybe b, Maybe c)
unzipMaybe3 (Just (a, b, c)) = (Just a, Just b, Just c)
unzipMaybe3 _ = (Nothing, Nothing, Nothing)
@@ -1350,7 +1375,7 @@ processChatCommand = \case
updateGroupProfileByName gName $ \p ->
p {groupPreferences = Just . setGroupPreference' SGFTimedMessages pref $ groupPreferences p}
QuitChat -> liftIO exitSuccess
- ShowVersion -> pure $ CRVersionInfo $ coreVersionInfo $(buildTimestampQ) "" -- $(simplexmqCommitQ)
+ ShowVersion -> pure $ CRVersionInfo $ coreVersionInfo $(buildTimestampQ) $(simplexmqCommitQ)
DebugLocks -> do
chatLockName <- atomically . tryReadTMVar =<< asks chatLock
agentLocks <- withAgent debugAgentLocks
@@ -1441,14 +1466,21 @@ processChatCommand = \case
contactMember Contact {contactId} =
find $ \GroupMember {memberContactId = cId, memberStatus = s} ->
cId == Just contactId && s /= GSMemRemoved && s /= GSMemLeft
- checkSndFile :: MsgContent -> FilePath -> Integer -> m (Integer, Integer, Maybe InlineFileMode)
+ checkSndFile :: MsgContent -> FilePath -> Integer -> m (Integer, SendFileMode)
checkSndFile mc f n = do
fsFilePath <- toFSFilePath f
unlessM (doesFileExist fsFilePath) . throwChatError $ CEFileNotFound f
ChatConfig {fileChunkSize, inlineFiles} <- asks config
+ xftpCfg <- readTVarIO =<< asks userXFTPFileConfig
fileSize <- getFileSize fsFilePath
let chunks = - ((- fileSize) `div` fileChunkSize)
- pure (fileSize, fileChunkSize, inlineFileMode mc inlineFiles chunks n)
+ fileInline = inlineFileMode mc inlineFiles chunks n
+ fileMode = case xftpCfg of
+ Just cfg
+ | fileInline == Just IFMSent || fileSize < minFileSize cfg -> SendFileSMP fileInline
+ | otherwise -> SendFileXFTP cfg
+ _ -> SendFileSMP fileInline
+ pure (fileSize, fileMode)
inlineFileMode mc InlineFilesConfig {offerChunks, sendChunks, totalSendChunks} chunks n
| chunks > offerChunks = Nothing
| chunks <= sendChunks && chunks * n <= totalSendChunks && isVoice mc = Just IFMSent
@@ -1711,18 +1743,22 @@ toFSFilePath f =
maybe f (<> "/" <> f) <$> (readTVarIO =<< asks filesFolder)
acceptFileReceive :: forall m. ChatMonad m => User -> RcvFileTransfer -> Maybe Bool -> Maybe FilePath -> m AChatItem
-acceptFileReceive user@User {userId} RcvFileTransfer {fileId, fileInvitation = FileInvitation {fileName = fName, fileConnReq, fileInline, fileSize}, fileStatus, grpMemberId} rcvInline_ filePath_ = do
+acceptFileReceive user@User {userId} RcvFileTransfer {fileId, rcvFileDescription, fileInvitation = FileInvitation {fileName = fName, fileConnReq, fileInline, fileSize}, fileStatus, grpMemberId} rcvInline_ filePath_ = do
unless (fileStatus == RFSNew) $ case fileStatus of
RFSCancelled _ -> throwChatError $ CEFileCancelled fName
_ -> throwChatError $ CEFileAlreadyReceiving fName
- case fileConnReq of
+ case (rcvFileDescription, fileConnReq) of
-- direct file protocol
- Just connReq -> do
+ (Nothing, Just connReq) -> do
connIds <- joinAgentConnectionAsync user True connReq . directMessage $ XFileAcpt fName
filePath <- getRcvFilePath fileId filePath_ fName
withStore $ \db -> acceptRcvFileTransfer db user fileId connIds ConnJoined filePath
-- group & direct file protocol
- Nothing -> do
+ (Just _fd, _) -> do
+ -- check if file description is fully received, error otherwise
+ -- pass file description to the agent and save AgentRcvFileId
+ throwChatError $ CEFileInternal "XFTP file receiption not implemented"
+ _ -> do
chatRef <- withStore $ \db -> getChatRefByFileId db user fileId
case (chatRef, grpMemberId) of
(ChatRef CTDirect contactId, Nothing) -> do
@@ -1837,18 +1873,24 @@ deleteGroupLink_ user gInfo conn = do
deleteAgentConnectionAsync user $ aConnId conn
withStore' $ \db -> deleteGroupLink db user gInfo
-agentSubscriber :: (MonadUnliftIO m, MonadReader ChatController m) => m ()
+agentSubscriber :: forall m. (MonadUnliftIO m, MonadReader ChatController m) => m ()
agentSubscriber = do
q <- asks $ subQ . smpAgent
l <- asks chatLock
- forever $ do
- (corrId, connId, APC _ msg) <- atomically $ readTBQueue q
- let name = "agentSubscriber connId=" <> str connId <> " corrId=" <> str corrId <> " msg=" <> str (aCommandTag msg)
- withLock l name . void . runExceptT $
- processAgentMessage corrId connId msg `catchError` (toView . CRChatError Nothing)
+ forever $ atomically (readTBQueue q) >>= void . process l
where
- str :: StrEncoding a => a -> String
- str = B.unpack . strEncode
+ process :: Lock -> (ACorrId, EntityId, APartyCmd 'Agent) -> m (Either ChatError ())
+ process l (corrId, entId, APC e msg) = run $ case e of
+ SAENone -> processAgentMessageNoConn msg
+ SAEConn -> processAgentMessage corrId entId msg
+ SAERcvFile -> processAgentMsgRcvFile corrId entId msg
+ SAESndFile -> processAgentMsgSndFile corrId entId msg
+ where
+ run action = do
+ let name = "agentSubscriber entity=" <> show e <> " entId=" <> str entId <> " msg=" <> str (aCommandTag msg)
+ withLock l name $ runExceptT $ action `catchError` (toView . CRChatError Nothing)
+ str :: StrEncoding a => a -> String
+ str = B.unpack . strEncode
type AgentBatchSubscribe m = AgentClient -> [ConnId] -> ExceptT AgentErrorType m (Map ConnId (Either AgentErrorType ()))
@@ -2066,9 +2108,7 @@ expireChatItems user@User {userId} ttl sync = do
membersToDelete <- withStore' $ \db -> getGroupMembersForExpiration db user gInfo
forM_ membersToDelete $ \m -> withStore' $ \db -> deleteGroupMember db user m
-processAgentMessage :: forall e m. (AEntityI e, ChatMonad m) => ACorrId -> ConnId -> ACommand 'Agent e -> m ()
-processAgentMessage _ "" msg =
- processAgentMessageNoConn msg `catchError` (toView . CRChatError Nothing)
+processAgentMessage :: forall m. ChatMonad m => ACorrId -> ConnId -> ACommand 'Agent 'AEConn -> m ()
processAgentMessage _ connId (DEL_RCVQ srv qId err_) =
toView $ CRAgentRcvQueueDeleted (AgentConnId connId) srv (AgentQueueId qId) err_
processAgentMessage _ connId DEL_CONN =
@@ -2078,7 +2118,7 @@ processAgentMessage corrId connId msg =
Just user -> processAgentMessageConn user corrId connId msg `catchError` (toView . CRChatError (Just user))
_ -> throwChatError $ CENoConnectionUser (AgentConnId connId)
-processAgentMessageNoConn :: forall e m. ChatMonad m => ACommand 'Agent e -> m ()
+processAgentMessageNoConn :: forall m. ChatMonad m => ACommand 'Agent 'AENone -> m ()
processAgentMessageNoConn = \case
CONNECT p h -> hostEvent $ CRHostConnected p h
DISCONNECT p h -> hostEvent $ CRHostDisconnected p h
@@ -2086,7 +2126,6 @@ processAgentMessageNoConn = \case
UP srv conns -> serverEvent srv conns CRContactsSubscribed "connected"
SUSPENDED -> toView CRChatSuspended
DEL_USER agentUserId -> toView $ CRAgentUserDeleted agentUserId
- _ -> pure ()
where
hostEvent :: ChatResponse -> m ()
hostEvent = whenM (asks $ hostEvents . config) . toView
@@ -2095,7 +2134,92 @@ processAgentMessageNoConn = \case
toView $ event srv cs
showToast ("server " <> str) (safeDecodeUtf8 $ strEncode host)
-processAgentMessageConn :: forall e m. (AEntityI e, ChatMonad m) => User -> ACorrId -> ConnId -> ACommand 'Agent e -> m ()
+processAgentMsgSndFile :: forall m. ChatMonad m => ACorrId -> SndFileId -> ACommand 'Agent 'AESndFile -> m ()
+processAgentMsgSndFile _corrId aFileId msg =
+ withStore' (`getUserByASndFileId` AgentSndFileId aFileId) >>= \case
+ Just user -> process user `catchError` (toView . CRChatError (Just user))
+ _ -> throwChatError $ CENoSndFileUser $ AgentSndFileId aFileId
+ where
+ process :: User -> m ()
+ process user = do
+ ft@FileTransferMeta {fileId} <- withStore $ \db -> getAgentSndFileXFTP db user $ AgentSndFileId aFileId
+ case msg of
+ SFPROG _sent _total -> do
+ -- update chat item status
+ -- send status to view
+ pure ()
+ SFDONE _sndDescr rfds -> do
+ AChatItem _ d cInfo _ci@ChatItem {meta = CIMeta {itemSharedMsgId = msgId_, itemDeleted}} <-
+ withStore $ \db -> getChatItemByFileId db user fileId
+ case (msgId_, itemDeleted) of
+ (Just sharedMsgId, Nothing) -> case (rfds, d, cInfo) of
+ (rfd : _, SMDSnd, DirectChat ct) -> do
+ let rfdText = safeDecodeUtf8 $ strEncode rfd
+ withStore' $ \db -> createSndDirectFTDescrXFTP db user ct ft rfdText
+ -- TODO update chat item status to show 100% progress
+ sendDirectFileDescription ct rfdText ft sharedMsgId
+ (_, SMDSnd, GroupChat _g) -> do
+ -- store file descriptions and files to snd_files
+ -- send messages with descriptions to the recipients
+ -- update chat item file status (CIFileStatus)
+ -- update sent file status
+ -- ??? possibly another event as we need one event per group, not per member
+ -- toView $ CRSndFileComplete user ci ft
+ pure ()
+ _ -> pure () -- TODO error
+ _ -> pure () -- TODO error
+ pure ()
+ where
+ sendDirectFileDescription :: Contact -> Text -> FileTransferMeta -> SharedMsgId -> m ()
+ sendDirectFileDescription ct rfd ft sharedMsgId = do
+ msgDeliveryId <- sendFileDescription_ rfd sharedMsgId $ sendDirectContactMessage ct
+ withStore' $ \db -> updateSndDirectFTDelivery db ct ft msgDeliveryId
+
+ _sendMemberFileDescription :: GroupMember -> Connection -> Text -> FileTransferMeta -> SharedMsgId -> m ()
+ _sendMemberFileDescription m@GroupMember {groupId} conn rfd ft sharedMsgId = do
+ msgDeliveryId <- sendFileDescription_ rfd sharedMsgId $ \msg' -> sendDirectMessage conn msg' $ GroupId groupId
+ withStore' $ \db -> updateSndGroupFTDelivery db m conn ft msgDeliveryId
+
+ sendFileDescription_ :: Text -> SharedMsgId -> (ChatMsgEvent 'Json -> m (SndMessage, Int64)) -> m Int64
+ sendFileDescription_ rfdText msgId sendMsg = do
+ partSize <- asks $ xftpDescrPartSize . config
+ sendParts 1 partSize rfdText
+ where
+ sendParts partNo partSize rfd = do
+ let (part, rest) = T.splitAt partSize rfd
+ complete = T.null rest
+ fileDescr = FileDescr {fileDescrText = part, fileDescrPartNo = partNo, fileDescrComplete = complete}
+ (_, msgDeliveryId) <- sendMsg $ XMsgFileDescr {msgId, fileDescr}
+ if complete
+ then pure msgDeliveryId
+ else sendParts (partNo + 1) partSize rest
+
+processAgentMsgRcvFile :: forall m. ChatMonad m => ACorrId -> RcvFileId -> ACommand 'Agent 'AERcvFile -> m ()
+processAgentMsgRcvFile _corrId aFileId msg =
+ withStore' (`getUserByARcvFileId` AgentRcvFileId aFileId) >>= \case
+ Just user -> process user `catchError` (toView . CRChatError (Just user))
+ _ -> throwChatError $ CENoRcvFileUser $ AgentRcvFileId aFileId
+ where
+ process :: User -> m ()
+ process user = do
+ _rcvFile <- withStore (\db -> getAgentRcvFileXFTP db user $ AgentRcvFileId aFileId)
+ -- >>= updateConnStatus
+ -- load file transfer meta (add chat item status to type and also contact/group)
+ case msg of
+ RFPROG _sent _total -> do
+ -- update chat item status
+ -- send status to view
+ pure ()
+ RFDONE _filePath -> do
+ -- update chat item status
+ -- send status to view
+ pure ()
+ RFERR _e -> do
+ -- update chat item status
+ -- send status to view
+ pure ()
+
+processAgentMessageConn :: forall m. ChatMonad m => User -> ACorrId -> ConnId -> ACommand 'Agent 'AEConn -> m ()
processAgentMessageConn user _ agentConnId END =
withStore (\db -> getConnectionEntity db user $ AgentConnId agentConnId) >>= \case
RcvDirectMsgConnection _ (Just ct@Contact {localDisplayName = c}) -> do
@@ -2186,6 +2310,8 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
updateChatLock "directMessage" event
case event of
XMsgNew mc -> newContentMessage ct mc msg msgMeta
+ XMsgFileDescr sharedMsgId fileDescr -> messageFileDescription ct sharedMsgId fileDescr msgMeta
+ XMsgFileCancel sharedMsgId -> cancelMessageFile ct sharedMsgId msgMeta
XMsgUpdate sharedMsgId mContent ttl live -> messageUpdate ct sharedMsgId mContent msg msgMeta ttl live
XMsgDel sharedMsgId _ -> messageDelete ct sharedMsgId msg msgMeta
-- TODO discontinue XFile
@@ -2398,6 +2524,8 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
updateChatLock "groupMessage" event
case event of
XMsgNew mc -> canSend $ newGroupContentMessage gInfo m mc msg msgMeta
+ XMsgFileDescr sharedMsgId fileDescr -> canSend $ groupMessageFileDescription gInfo m sharedMsgId fileDescr msgMeta
+ XMsgFileCancel sharedMsgId -> cancelGroupMessageFile gInfo m sharedMsgId msgMeta
XMsgUpdate sharedMsgId mContent ttl live -> canSend $ groupMessageUpdate gInfo m sharedMsgId mContent msg msgMeta ttl live
XMsgDel sharedMsgId memberId -> groupMessageDelete gInfo m sharedMsgId memberId msg
-- TODO discontinue XFile
@@ -2459,7 +2587,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
CON -> do
ci <- withStore $ \db -> do
liftIO $ updateSndFileStatus db ft FSConnected
- updateDirectCIFileStatus db user fileId CIFSSndTransfer
+ updateDirectCIFileStatus db user fileId $ CIFSSndTransfer 0 1
toView $ CRSndFileStart user ci ft
sendFileChunk user ft
SENT msgId -> do
@@ -2535,7 +2663,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
startReceivingFile ft@RcvFileTransfer {fileId} = do
ci <- withStore $ \db -> do
liftIO $ updateRcvFileStatus db ft FSConnected
- liftIO $ updateCIFileStatus db user fileId CIFSRcvTransfer
+ liftIO $ updateCIFileStatus db user fileId $ CIFSRcvTransfer 0 1
getChatItemByFileId db user fileId
toView $ CRRcvFileStart user ci
@@ -2637,7 +2765,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
where
s = " " <> name <> "=" <> B.unpack (strEncode $ toCMEventTag event)
- withCompletedCommand :: Connection -> ACommand 'Agent e -> (CommandData -> m ()) -> m ()
+ withCompletedCommand :: forall e. AEntityI e => Connection -> ACommand 'Agent e -> (CommandData -> m ()) -> m ()
withCompletedCommand Connection {connId} agentMsg action = do
let agentMsgTag = APCT (sAEntity @e) $ aCommandTag agentMsg
cmdData_ <- withStore' $ \db -> getCommandDataByCorrId db user corrId
@@ -2729,7 +2857,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
newContentMessage ct@Contact {localDisplayName = c, contactUsed, chatSettings} mc msg@RcvMessage {sharedMsgId_} msgMeta = do
unless contactUsed $ withStore' $ \db -> updateContactUsed db user ct
checkIntegrityCreateItem (CDDirectRcv ct) msgMeta
- let ExtMsgContent content fileInvitation_ _ _ = mcExtMsgContent mc
+ let ExtMsgContent content fInv_ _ _ = mcExtMsgContent mc
if isVoice content && not (featureAllowed SCFVoice forContact ct)
then do
void $ newChatItem (CIRcvChatFeatureRejected CFVoice) Nothing Nothing False
@@ -2738,7 +2866,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
let ExtMsgContent _ _ itemTTL live_ = mcExtMsgContent mc
timed_ = rcvContactCITimed ct itemTTL
live = fromMaybe False live_
- ciFile_ <- processFileInvitation fileInvitation_ content $ \db -> createRcvFileTransfer db userId ct
+ ciFile_ <- processFileInvitation fInv_ content $ \db -> createRcvFileTransfer db userId ct
ChatItem {formattedText} <- newChatItem (CIRcvMsgContent content) ciFile_ timed_ live
when (enableNtfs chatSettings) $ do
showMsgToast (c <> "> ") content formattedText
@@ -2749,11 +2877,36 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
toView $ CRNewChatItem user (AChatItem SCTDirect SMDRcv (DirectChat ct) ci)
pure ci
+ messageFileDescription :: Contact -> SharedMsgId -> FileDescr -> MsgMeta -> m ()
+ messageFileDescription ct _sharedMsgId _fileDescr msgMeta = do
+ checkIntegrityCreateItem (CDDirectRcv ct) msgMeta
+ -- find the original chat item and file
+ -- re-create file item if it does not exist
+ -- check file description part number
+ -- append file description part to the record
+ -- if file description is complete send it to the agent to receive
+ pure ()
+
+ groupMessageFileDescription :: GroupInfo -> GroupMember -> SharedMsgId -> FileDescr -> MsgMeta -> m ()
+ groupMessageFileDescription _gInfo _m _sharedMsgId _fileDescr _msgMeta = do
+ pure ()
+
+ cancelMessageFile :: Contact -> SharedMsgId -> MsgMeta -> m ()
+ cancelMessageFile ct _sharedMsgId msgMeta = do
+ checkIntegrityCreateItem (CDDirectRcv ct) msgMeta
+ -- find the original chat item and file
+ -- mark file as cancelled, remove description if excists
+ pure ()
+
+ cancelGroupMessageFile :: GroupInfo -> GroupMember -> SharedMsgId -> MsgMeta -> m ()
+ cancelGroupMessageFile _gInfo _m _sharedMsgId _msgMeta = do
+ pure ()
+
processFileInvitation :: Maybe FileInvitation -> MsgContent -> (DB.Connection -> FileInvitation -> Maybe InlineFileMode -> Integer -> IO RcvFileTransfer) -> m (Maybe (CIFile 'MDRcv))
processFileInvitation fInv_ mc createRcvFT = forM fInv_ $ \fInv@FileInvitation {fileName, fileSize} -> do
- chSize <- asks $ fileChunkSize . config
- inline <- receiveInlineMode fInv (Just mc) chSize
- ft@RcvFileTransfer {fileId} <- withStore' $ \db -> createRcvFT db fInv inline chSize
+ ChatConfig {fileChunkSize} <- asks config
+ inline <- receiveInlineMode fInv (Just mc) fileChunkSize
+ ft@RcvFileTransfer {fileId} <- withStore' $ \db -> createRcvFT db fInv inline fileChunkSize
(filePath, fileStatus) <- case inline of
Just IFMSent -> do
fPath <- getRcvFilePath fileId Nothing fileName
@@ -2886,9 +3039,9 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
processFileInvitation' :: Contact -> FileInvitation -> RcvMessage -> MsgMeta -> m ()
processFileInvitation' ct@Contact {localDisplayName = c} fInv@FileInvitation {fileName, fileSize} msg@RcvMessage {sharedMsgId_} msgMeta = do
checkIntegrityCreateItem (CDDirectRcv ct) msgMeta
- chSize <- asks $ fileChunkSize . config
- inline <- receiveInlineMode fInv Nothing chSize
- RcvFileTransfer {fileId} <- withStore' $ \db -> createRcvFileTransfer db userId ct fInv inline chSize
+ ChatConfig {fileChunkSize} <- asks config
+ inline <- receiveInlineMode fInv Nothing fileChunkSize
+ RcvFileTransfer {fileId} <- withStore' $ \db -> createRcvFileTransfer db userId ct fInv inline fileChunkSize
let ciFile = Just $ CIFile {fileId, fileName, fileSize, filePath = Nothing, fileStatus = CIFSRcvInvitation}
ci <- saveRcvChatItem' user (CDDirectRcv ct) msg sharedMsgId_ msgMeta (CIRcvMsgContent $ MCFile "") ciFile Nothing False
toView $ CRNewChatItem user (AChatItem SCTDirect SMDRcv (DirectChat ct) ci)
@@ -2898,9 +3051,9 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
-- TODO remove once XFile is discontinued
processGroupFileInvitation' :: GroupInfo -> GroupMember -> FileInvitation -> RcvMessage -> MsgMeta -> m ()
processGroupFileInvitation' gInfo m@GroupMember {localDisplayName = c} fInv@FileInvitation {fileName, fileSize} msg@RcvMessage {sharedMsgId_} msgMeta = do
- chSize <- asks $ fileChunkSize . config
- inline <- receiveInlineMode fInv Nothing chSize
- RcvFileTransfer {fileId} <- withStore' $ \db -> createRcvGroupFileTransfer db userId m fInv inline chSize
+ ChatConfig {fileChunkSize} <- asks config
+ inline <- receiveInlineMode fInv Nothing fileChunkSize
+ RcvFileTransfer {fileId} <- withStore' $ \db -> createRcvGroupFileTransfer db userId m fInv inline fileChunkSize
let ciFile = Just $ CIFile {fileId, fileName, fileSize, filePath = Nothing, fileStatus = CIFSRcvInvitation}
ci <- saveRcvChatItem' user (CDGroupRcv gInfo m) msg sharedMsgId_ msgMeta (CIRcvMsgContent $ MCFile "") ciFile Nothing False
groupMsgToView gInfo m ci msgMeta
@@ -2909,8 +3062,8 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
setActive $ ActiveG g
receiveInlineMode :: FileInvitation -> Maybe MsgContent -> Integer -> m (Maybe InlineFileMode)
- receiveInlineMode FileInvitation {fileSize, fileInline} mc_ chSize = case fileInline of
- Just mode -> do
+ receiveInlineMode FileInvitation {fileSize, fileInline, fileDescr} mc_ chSize = case (fileInline, fileDescr) of
+ (Just mode, Nothing) -> do
InlineFilesConfig {receiveChunks, receiveInstant} <- asks $ inlineFiles . config
pure $ if fileSize <= receiveChunks * chSize then inline' receiveInstant else Nothing
where
@@ -2941,7 +3094,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
-- receiving inline
_ -> do
event <- withStore $ \db -> do
- ci <- updateDirectCIFileStatus db user fileId CIFSSndTransfer
+ ci <- updateDirectCIFileStatus db user fileId $ CIFSSndTransfer 0 1
sft <- liftIO $ createSndDirectInlineFT db ct ft
pure $ CRSndFileStart user ci sft
toView event
@@ -2953,7 +3106,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
checkSndInlineFTComplete :: Connection -> AgentMsgId -> m ()
checkSndInlineFTComplete conn agentMsgId = do
- ft_ <- withStore' $ \db -> getSndInlineFTViaMsgDelivery db user conn agentMsgId
+ ft_ <- withStore' $ \db -> getSndFTViaMsgDelivery db user conn agentMsgId
forM_ ft_ $ \ft@SndFileTransfer {fileId} -> do
ci <- withStore $ \db -> do
liftIO $ updateSndFileStatus db ft FSComplete
@@ -3020,7 +3173,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
(_, Just conn) -> do
-- receiving inline
event <- withStore $ \db -> do
- ci <- updateDirectCIFileStatus db user fileId CIFSSndTransfer
+ ci <- updateDirectCIFileStatus db user fileId $ CIFSSndTransfer 0 1
sft <- liftIO $ createSndGroupInlineFT db m conn ft
pure $ CRSndFileStart user ci sft
toView event
diff --git a/src/Simplex/Chat/Archive.hs b/src/Simplex/Chat/Archive.hs
index f07fba4ca..110c1dbda 100644
--- a/src/Simplex/Chat/Archive.hs
+++ b/src/Simplex/Chat/Archive.hs
@@ -65,7 +65,7 @@ importArchive cfg@ArchiveConfig {archivePath} =
backup f = whenM (doesFileExist f) $ copyFile f $ f <> ".bak"
withTempDir :: ChatMonad m => ArchiveConfig -> (String -> (FilePath -> m ()) -> m ())
-withTempDir cfg = case parentTempDirectory cfg of
+withTempDir cfg = case parentTempDirectory (cfg :: ArchiveConfig) of
Just tmpDir -> withTempDirectory tmpDir
_ -> withSystemTempDirectory
diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs
index e9b499de0..cbf66ef4c 100644
--- a/src/Simplex/Chat/Controller.hs
+++ b/src/Simplex/Chat/Controller.hs
@@ -105,7 +105,9 @@ data ChatConfig = ChatConfig
defaultServers :: DefaultAgentServers,
tbqSize :: Natural,
fileChunkSize :: Integer,
+ xftpDescrPartSize :: Int,
inlineFiles :: InlineFilesConfig,
+ xftpFileConfig :: Maybe XFTPFileConfig, -- Nothing - XFTP is disabled
subscriptionEvents :: Bool,
hostEvents :: Bool,
logLevel :: ChatLogLevel,
@@ -168,6 +170,7 @@ data ChatController = ChatController
cleanupManagerAsync :: TVar (Maybe (Async ())),
timedItemThreads :: TMap (ChatRef, ChatItemId) (TVar (Maybe (Weak ThreadId))),
showLiveItems :: TVar Bool,
+ userXFTPFileConfig :: TVar (Maybe XFTPFileConfig),
logFilePath :: Maybe FilePath
}
@@ -421,9 +424,12 @@ data ChatResponse
| CRContactRequestAlreadyAccepted {user :: User, contact :: Contact}
| CRLeftMemberUser {user :: User, groupInfo :: GroupInfo}
| CRGroupDeletedUser {user :: User, groupInfo :: GroupInfo}
+ | CRRcvFileDescrReady {user :: User, chatItem :: AChatItem}
| CRRcvFileAccepted {user :: User, chatItem :: AChatItem}
| CRRcvFileAcceptedSndCancelled {user :: User, rcvFileTransfer :: RcvFileTransfer}
+ | CRRcvFileDescrNotReady {user :: User, chatItem :: AChatItem}
| CRRcvFileStart {user :: User, chatItem :: AChatItem}
+ | CRRcvFileProgressXFTP {user :: User, chatItem :: AChatItem, receivedChunks :: Int, totalChunks :: Int}
| CRRcvFileComplete {user :: User, chatItem :: AChatItem}
| CRRcvFileCancelled {user :: User, rcvFileTransfer :: RcvFileTransfer}
| CRRcvFileSndCancelled {user :: User, rcvFileTransfer :: RcvFileTransfer}
@@ -432,6 +438,10 @@ data ChatResponse
| CRSndFileCancelled {chatItem :: AChatItem, sndFileTransfer :: SndFileTransfer}
| CRSndFileRcvCancelled {user :: User, chatItem :: AChatItem, sndFileTransfer :: SndFileTransfer}
| CRSndGroupFileCancelled {user :: User, chatItem :: AChatItem, fileTransferMeta :: FileTransferMeta, sndFileTransfers :: [SndFileTransfer]}
+ | CRSndFileStartXFTP {user :: User, chatItem :: AChatItem, fileTransferMeta :: FileTransferMeta}
+ | CRSndFileProgressXFTP {user :: User, chatItem :: AChatItem, fileTransferMeta :: FileTransferMeta, sentChunks :: Int, totalChunks :: Int}
+ | CRSndFileCompleteXFTP {user :: User, chatItem :: AChatItem, fileTransferMeta :: FileTransferMeta}
+ | CRSndFileCancelledXFTP {user :: User, chatItem :: AChatItem, fileTransferMeta :: FileTransferMeta}
| CRUserProfileUpdated {user :: User, fromProfile :: Profile, toProfile :: Profile}
| CRContactAliasUpdated {user :: User, toContact :: Contact}
| CRConnectionAliasUpdated {user :: User, toConnection :: PendingContactConnection}
@@ -608,6 +618,19 @@ instance ToJSON ComposedMessage where
toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True}
toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True}
+data XFTPFileConfig = XFTPFileConfig
+ { minFileSize :: Integer,
+ tempDirectory :: Maybe FilePath
+ }
+ deriving (Show, Generic, FromJSON)
+
+defaultXFTPFileConfig :: XFTPFileConfig
+defaultXFTPFileConfig = XFTPFileConfig {minFileSize = 0, tempDirectory = Nothing}
+
+instance ToJSON XFTPFileConfig where
+ toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True}
+ toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True}
+
data NtfMsgInfo = NtfMsgInfo {msgTs :: UTCTime, msgFlags :: MsgFlags}
deriving (Show, Generic)
@@ -668,6 +691,11 @@ data CoreVersionInfo = CoreVersionInfo
instance ToJSON CoreVersionInfo where toEncoding = J.genericToEncoding J.defaultOptions
+data SendFileMode
+ = SendFileSMP (Maybe InlineFileMode)
+ | SendFileXFTP XFTPFileConfig
+ deriving (Show, Generic)
+
data ChatError
= ChatError {errorType :: ChatErrorType}
| ChatErrorAgent {agentError :: AgentErrorType, connectionEntity_ :: Maybe ConnectionEntity}
@@ -682,6 +710,8 @@ instance ToJSON ChatError where
data ChatErrorType
= CENoActiveUser
| CENoConnectionUser {agentConnId :: AgentConnId}
+ | CENoSndFileUser {agentSndFileId :: AgentSndFileId}
+ | CENoRcvFileUser {agentRcvFileId :: AgentRcvFileId}
| CEActiveUserExists -- TODO delete
| CEUserExists {contactName :: ContactName}
| CEDifferentActiveUser {commandUserId :: UserId, activeUserId :: UserId}
diff --git a/src/Simplex/Chat/Messages.hs b/src/Simplex/Chat/Messages.hs
index d94cff500..e6a45349e 100644
--- a/src/Simplex/Chat/Messages.hs
+++ b/src/Simplex/Chat/Messages.hs
@@ -13,6 +13,7 @@
module Simplex.Chat.Messages where
+import Control.Applicative ((<|>))
import Data.Aeson (FromJSON, ToJSON)
import qualified Data.Aeson as J
import qualified Data.Attoparsec.ByteString.Char8 as A
@@ -418,12 +419,12 @@ instance MsgDirectionI d => ToJSON (CIFile d) where
data CIFileStatus (d :: MsgDirection) where
CIFSSndStored :: CIFileStatus 'MDSnd
- CIFSSndTransfer :: CIFileStatus 'MDSnd
+ CIFSSndTransfer :: {sndProgress :: Int, sndTotal :: Int} -> CIFileStatus 'MDSnd
CIFSSndCancelled :: CIFileStatus 'MDSnd
CIFSSndComplete :: CIFileStatus 'MDSnd
CIFSRcvInvitation :: CIFileStatus 'MDRcv
CIFSRcvAccepted :: CIFileStatus 'MDRcv
- CIFSRcvTransfer :: CIFileStatus 'MDRcv
+ CIFSRcvTransfer :: {rcvProgress :: Int, rcvTotal :: Int} -> CIFileStatus 'MDRcv
CIFSRcvComplete :: CIFileStatus 'MDRcv
CIFSRcvCancelled :: CIFileStatus 'MDRcv
@@ -434,18 +435,18 @@ deriving instance Show (CIFileStatus d)
ciFileEnded :: CIFileStatus d -> Bool
ciFileEnded = \case
CIFSSndStored -> False
- CIFSSndTransfer -> False
+ CIFSSndTransfer {} -> False
CIFSSndCancelled -> True
CIFSSndComplete -> True
CIFSRcvInvitation -> False
CIFSRcvAccepted -> False
- CIFSRcvTransfer -> False
+ CIFSRcvTransfer {} -> False
CIFSRcvCancelled -> True
CIFSRcvComplete -> True
-instance MsgDirectionI d => ToJSON (CIFileStatus d) where
- toJSON = strToJSON
- toEncoding = strToJEncoding
+instance ToJSON (CIFileStatus d) where
+ toJSON = J.toJSON . jsonCIFileStatus
+ toEncoding = J.toEncoding . jsonCIFileStatus
instance MsgDirectionI d => ToField (CIFileStatus d) where toField = toField . decodeLatin1 . strEncode
@@ -458,12 +459,12 @@ deriving instance Show ACIFileStatus
instance MsgDirectionI d => StrEncoding (CIFileStatus d) where
strEncode = \case
CIFSSndStored -> "snd_stored"
- CIFSSndTransfer -> "snd_transfer"
+ CIFSSndTransfer sent total -> strEncode (Str "snd_transfer", sent, total)
CIFSSndCancelled -> "snd_cancelled"
CIFSSndComplete -> "snd_complete"
CIFSRcvInvitation -> "rcv_invitation"
CIFSRcvAccepted -> "rcv_accepted"
- CIFSRcvTransfer -> "rcv_transfer"
+ CIFSRcvTransfer rcvd total -> strEncode (Str "rcv_transfer", rcvd, total)
CIFSRcvComplete -> "rcv_complete"
CIFSRcvCancelled -> "rcv_cancelled"
strP = (\(AFS _ st) -> checkDirection st) <$?> strP
@@ -473,15 +474,59 @@ instance StrEncoding ACIFileStatus where
strP =
A.takeTill (== ' ') >>= \case
"snd_stored" -> pure $ AFS SMDSnd CIFSSndStored
- "snd_transfer" -> pure $ AFS SMDSnd CIFSSndTransfer
+ "snd_transfer" -> AFS SMDSnd <$> progress CIFSSndTransfer
"snd_cancelled" -> pure $ AFS SMDSnd CIFSSndCancelled
"snd_complete" -> pure $ AFS SMDSnd CIFSSndComplete
"rcv_invitation" -> pure $ AFS SMDRcv CIFSRcvInvitation
"rcv_accepted" -> pure $ AFS SMDRcv CIFSRcvAccepted
- "rcv_transfer" -> pure $ AFS SMDRcv CIFSRcvTransfer
+ "rcv_transfer" -> AFS SMDRcv <$> progress CIFSRcvTransfer
"rcv_complete" -> pure $ AFS SMDRcv CIFSRcvComplete
"rcv_cancelled" -> pure $ AFS SMDRcv CIFSRcvCancelled
_ -> fail "bad file status"
+ where
+ progress :: (Int -> Int -> a) -> A.Parser a
+ progress f = f <$> num <*> num <|> pure (f 0 1)
+ num = A.space *> A.decimal
+
+data JSONCIFileStatus
+ = JCIFSSndStored
+ | JCIFSSndTransfer {sndProgress :: Int, sndTotal :: Int}
+ | JCIFSSndCancelled
+ | JCIFSSndComplete
+ | JCIFSRcvInvitation
+ | JCIFSRcvAccepted
+ | JCIFSRcvTransfer {rcvProgress :: Int, rcvTotal :: Int}
+ | JCIFSRcvComplete
+ | JCIFSRcvCancelled
+ deriving (Generic)
+
+instance ToJSON JSONCIFileStatus where
+ toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "JCIFS"
+ toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "JCIFS"
+
+jsonCIFileStatus :: CIFileStatus d -> JSONCIFileStatus
+jsonCIFileStatus = \case
+ CIFSSndStored -> JCIFSSndStored
+ CIFSSndTransfer sent total -> JCIFSSndTransfer sent total
+ CIFSSndCancelled -> JCIFSSndCancelled
+ CIFSSndComplete -> JCIFSSndComplete
+ CIFSRcvInvitation -> JCIFSRcvInvitation
+ CIFSRcvAccepted -> JCIFSRcvAccepted
+ CIFSRcvTransfer rcvd total -> JCIFSRcvTransfer rcvd total
+ CIFSRcvComplete -> JCIFSRcvComplete
+ CIFSRcvCancelled -> JCIFSRcvCancelled
+
+aciFileStatusJSON :: JSONCIFileStatus -> ACIFileStatus
+aciFileStatusJSON = \case
+ JCIFSSndStored -> AFS SMDSnd CIFSSndStored
+ JCIFSSndTransfer sent total -> AFS SMDSnd $ CIFSSndTransfer sent total
+ JCIFSSndCancelled -> AFS SMDSnd CIFSSndCancelled
+ JCIFSSndComplete -> AFS SMDSnd CIFSSndComplete
+ JCIFSRcvInvitation -> AFS SMDRcv CIFSRcvInvitation
+ JCIFSRcvAccepted -> AFS SMDRcv CIFSRcvAccepted
+ JCIFSRcvTransfer rcvd total -> AFS SMDRcv $ CIFSRcvTransfer rcvd total
+ JCIFSRcvComplete -> AFS SMDRcv CIFSRcvComplete
+ JCIFSRcvCancelled -> AFS SMDRcv CIFSRcvCancelled
-- to conveniently read file data from db
data CIFileInfo = CIFileInfo
diff --git a/src/Simplex/Chat/Migrations/M20230304_file_description.hs b/src/Simplex/Chat/Migrations/M20230304_file_description.hs
index 1846de09d..40e7d0f0a 100644
--- a/src/Simplex/Chat/Migrations/M20230304_file_description.hs
+++ b/src/Simplex/Chat/Migrations/M20230304_file_description.hs
@@ -11,19 +11,25 @@ import Database.SQLite.Simple.QQ (sql)
m20230304_file_description :: Query
m20230304_file_description =
[sql|
-CREATE TABLE recipient_file_descriptions (
+CREATE TABLE xftp_file_descriptions (
file_descr_id INTEGER PRIMARY KEY AUTOINCREMENT,
- file_descr_size INTEGER NOT NULL,
- file_descr_status TEXT NOT NULL,
- file_descr_text TEXT NOT NULL
+ user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE,
+ file_descr_text TEXT NOT NULL,
+ file_descr_part_no INTEGER NOT NULL DEFAULT(0),
+ file_descr_complete INTEGER NOT NULL DEFAULT(0),
+ created_at TEXT NOT NULL DEFAULT(datetime('now')),
+ updated_at TEXT NOT NULL DEFAULT(datetime('now'))
);
-ALTER TABLE rcv_files ADD COLUMN file_descr_id INTEGER NULL
- REFERENCES recipient_file_descriptions(file_descr_id) ON DELETE RESTRICT;
+ALTER TABLE files ADD COLUMN agent_snd_file_id BLOB NULL;
+
+ALTER TABLE files ADD COLUMN private_snd_file_descr TEXT NULL;
ALTER TABLE snd_files ADD COLUMN file_descr_id INTEGER NULL
- REFERENCES recipient_file_descriptions(file_descr_id) ON DELETE RESTRICT;
+ REFERENCES xftp_file_descriptions ON DELETE SET NULL;
- -- this is a private file description allowing to delete the file from the server
-ALTER TABLE files ADD COLUMN snd_file_descr_text TEXT NULL;
+ALTER TABLE rcv_files ADD COLUMN file_descr_id INTEGER NULL
+ REFERENCES xftp_file_descriptions ON DELETE SET NULL;
+
+ALTER TABLE rcv_files ADD COLUMN agent_rcv_file_id BLOB NULL;
|]
diff --git a/src/Simplex/Chat/Migrations/chat_schema.sql b/src/Simplex/Chat/Migrations/chat_schema.sql
index 5562bce7d..5c2758508 100644
--- a/src/Simplex/Chat/Migrations/chat_schema.sql
+++ b/src/Simplex/Chat/Migrations/chat_schema.sql
@@ -193,7 +193,9 @@ CREATE TABLE files(
updated_at TEXT CHECK(updated_at NOT NULL),
cancelled INTEGER,
ci_file_status TEXT,
- file_inline TEXT
+ file_inline TEXT,
+ agent_snd_file_id BLOB NULL,
+ private_snd_file_descr TEXT NULL
);
CREATE TABLE snd_files(
file_id INTEGER NOT NULL REFERENCES files ON DELETE CASCADE,
@@ -204,6 +206,8 @@ CREATE TABLE snd_files(
updated_at TEXT CHECK(updated_at NOT NULL),
file_inline TEXT,
last_inline_msg_delivery_id INTEGER,
+ file_descr_id INTEGER NULL
+ REFERENCES xftp_file_descriptions ON DELETE SET NULL,
PRIMARY KEY(file_id, connection_id)
) WITHOUT ROWID;
CREATE TABLE rcv_files(
@@ -215,7 +219,10 @@ CREATE TABLE rcv_files(
created_at TEXT CHECK(created_at NOT NULL),
updated_at TEXT CHECK(updated_at NOT NULL),
rcv_file_inline TEXT,
- file_inline TEXT
+ file_inline TEXT,
+ file_descr_id INTEGER NULL
+ REFERENCES xftp_file_descriptions ON DELETE SET NULL,
+ agent_rcv_file_id BLOB NULL
);
CREATE TABLE snd_file_chunks(
file_id INTEGER NOT NULL,
@@ -551,3 +558,12 @@ CREATE INDEX idx_smp_servers_user_id ON smp_servers(user_id);
CREATE INDEX idx_chat_items_item_deleted_by_group_member_id ON chat_items(
item_deleted_by_group_member_id
);
+CREATE TABLE xftp_file_descriptions(
+ file_descr_id INTEGER PRIMARY KEY AUTOINCREMENT,
+ user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE,
+ file_descr_text TEXT NOT NULL,
+ file_descr_part_no INTEGER NOT NULL DEFAULT(0),
+ file_descr_complete INTEGER NOT NULL DEFAULT(0),
+ created_at TEXT NOT NULL DEFAULT(datetime('now')),
+ updated_at TEXT NOT NULL DEFAULT(datetime('now'))
+);
diff --git a/src/Simplex/Chat/Store.hs b/src/Simplex/Chat/Store.hs
index 604d33db4..d80afae62 100644
--- a/src/Simplex/Chat/Store.hs
+++ b/src/Simplex/Chat/Store.hs
@@ -33,6 +33,8 @@ module Simplex.Chat.Store
getUser,
getUserIdByName,
getUserByAConnId,
+ getUserByASndFileId,
+ getUserByARcvFileId,
getUserByContactId,
getUserByGroupId,
getUserByFileId,
@@ -152,7 +154,11 @@ module Simplex.Chat.Store
createSndGroupInlineFT,
updateSndDirectFTDelivery,
updateSndGroupFTDelivery,
- getSndInlineFTViaMsgDelivery,
+ getSndFTViaMsgDelivery,
+ createSndFileTransferXFTP,
+ createSndDirectFTDescrXFTP,
+ getAgentSndFileXFTP,
+ getAgentRcvFileXFTP,
updateFileCancelled,
updateCIFileStatus,
getSharedMsgIdByFileId,
@@ -345,11 +351,11 @@ import Simplex.Chat.Migrations.M20230118_recreate_smp_servers
import Simplex.Chat.Migrations.M20230129_drop_chat_items_group_idx
import Simplex.Chat.Migrations.M20230206_item_deleted_by_group_member_id
import Simplex.Chat.Migrations.M20230303_group_link_role
--- import Simplex.Chat.Migrations.M20230304_file_description
+import Simplex.Chat.Migrations.M20230304_file_description
import Simplex.Chat.Protocol
import Simplex.Chat.Types
import Simplex.Chat.Util (week)
-import Simplex.Messaging.Agent.Protocol (ACorrId, AgentMsgId, ConnId, InvitationId, MsgMeta (..))
+import Simplex.Messaging.Agent.Protocol (ACorrId, AgentMsgId, ConnId, InvitationId, MsgMeta (..), UserId)
import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore (..), createSQLiteStore, firstRow, firstRow', maybeFirstRow, withTransaction)
import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..))
import qualified Simplex.Messaging.Crypto as C
@@ -412,8 +418,8 @@ schemaMigrations =
("20230118_recreate_smp_servers", m20230118_recreate_smp_servers),
("20230129_drop_chat_items_group_idx", m20230129_drop_chat_items_group_idx),
("20230206_item_deleted_by_group_member_id", m20230206_item_deleted_by_group_member_id),
- ("20230303_group_link_role", m20230303_group_link_role)
- -- ("20230304_file_description", m20230304_file_description)
+ ("20230303_group_link_role", m20230303_group_link_role),
+ ("20230304_file_description", m20230304_file_description)
]
-- | The list of migrations in ascending order by date
@@ -541,6 +547,16 @@ getUserByAConnId db agentConnId =
maybeFirstRow toUser $
DB.query db (userQuery <> " JOIN connections c ON c.user_id = u.user_id WHERE c.agent_conn_id = ?") (Only agentConnId)
+getUserByASndFileId :: DB.Connection -> AgentSndFileId -> IO (Maybe User)
+getUserByASndFileId db aSndFileId =
+ maybeFirstRow toUser $
+ DB.query db (userQuery <> " JOIN files f ON f.user_id = u.user_id WHERE f.agent_snd_file_id = ?") (Only aSndFileId)
+
+getUserByARcvFileId :: DB.Connection -> AgentRcvFileId -> IO (Maybe User)
+getUserByARcvFileId db aRcvFileId =
+ maybeFirstRow toUser $
+ DB.query db (userQuery <> " JOIN rcv_files r USING (file_id) JOIN files f ON f.user_id = u.user_id WHERE r.agent_rcv_file_id = ?") (Only aRcvFileId)
+
getUserByContactId :: DB.Connection -> ContactId -> ExceptT StoreError IO User
getUserByContactId db contactId =
ExceptT . firstRow toUser (SEUserNotFoundByContactId contactId) $
@@ -1394,7 +1410,10 @@ getLiveSndFileTransfers db User {userId} = do
SELECT DISTINCT f.file_id
FROM files f
JOIN snd_files s USING (file_id)
- WHERE f.user_id = ? AND s.file_status IN (?, ?, ?) AND s.file_inline IS NULL
+ WHERE f.user_id = ?
+ AND s.file_status IN (?, ?, ?)
+ AND s.file_descr_id IS NULL
+ AND s.file_inline IS NULL
AND s.created_at > ?
|]
(userId, FSNew, FSAccepted, FSConnected, cutoffTs)
@@ -1721,7 +1740,7 @@ getConnectionEntity db user@User {userId, userContactId} agentConnId = do
DB.query
db
[sql|
- SELECT s.file_status, f.file_name, f.file_size, f.chunk_size, f.file_path, s.file_inline, cs.local_display_name, m.local_display_name
+ SELECT s.file_status, f.file_name, f.file_size, f.chunk_size, f.file_path, s.file_descr_id, s.file_inline, cs.local_display_name, m.local_display_name
FROM snd_files s
JOIN files f USING (file_id)
LEFT JOIN contacts cs USING (contact_id)
@@ -1729,10 +1748,10 @@ getConnectionEntity db user@User {userId, userContactId} agentConnId = do
WHERE f.user_id = ? AND f.file_id = ? AND s.connection_id = ?
|]
(userId, fileId, connId)
- sndFileTransfer_ :: Int64 -> Int64 -> (FileStatus, String, Integer, Integer, FilePath, Maybe InlineFileMode, Maybe ContactName, Maybe ContactName) -> Either StoreError SndFileTransfer
- sndFileTransfer_ fileId connId (fileStatus, fileName, fileSize, chunkSize, filePath, fileInline, contactName_, memberName_) =
+ sndFileTransfer_ :: Int64 -> Int64 -> (FileStatus, String, Integer, Integer, FilePath, Maybe Int64, Maybe InlineFileMode, Maybe ContactName, Maybe ContactName) -> Either StoreError SndFileTransfer
+ sndFileTransfer_ fileId connId (fileStatus, fileName, fileSize, chunkSize, filePath, fileDescrId, fileInline, contactName_, memberName_) =
case contactName_ <|> memberName_ of
- Just recipientDisplayName -> Right SndFileTransfer {fileId, fileStatus, fileName, fileSize, chunkSize, filePath, fileInline, recipientDisplayName, connId, agentConnId}
+ Just recipientDisplayName -> Right SndFileTransfer {fileId, fileStatus, fileName, fileSize, chunkSize, filePath, fileDescrId, fileInline, recipientDisplayName, connId, agentConnId}
Nothing -> Left $ SESndFileInvalid fileId
getUserContact_ :: Int64 -> ExceptT StoreError IO UserContact
getUserContact_ userContactLinkId = ExceptT $ do
@@ -2619,7 +2638,7 @@ createSndDirectFileTransfer db userId Contact {contactId} filePath FileInvitatio
db
"INSERT INTO snd_files (file_id, file_status, file_inline, connection_id, created_at, updated_at) VALUES (?,?,?,?,?,?)"
(fileId, fileStatus, fileInline, connId, currentTs, currentTs)
- pure FileTransferMeta {fileId, fileName, filePath, fileSize, fileInline, chunkSize, cancelled = False}
+ pure FileTransferMeta {fileId, xftpSndFile = Nothing, fileName, filePath, fileSize, fileInline, chunkSize, cancelled = False}
createSndDirectFTConnection :: DB.Connection -> User -> Int64 -> (CommandId, ConnId) -> IO ()
createSndDirectFTConnection db user@User {userId} fileId (cmdId, acId) = do
@@ -2639,7 +2658,7 @@ createSndGroupFileTransfer db userId GroupInfo {groupId} filePath FileInvitation
"INSERT INTO files (user_id, group_id, file_name, file_path, file_size, chunk_size, file_inline, ci_file_status, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?)"
(userId, groupId, fileName, filePath, fileSize, chunkSize, fileInline, CIFSSndStored, currentTs, currentTs)
fileId <- insertedRowId db
- pure FileTransferMeta {fileId, fileName, filePath, fileSize, fileInline, chunkSize, cancelled = False}
+ pure FileTransferMeta {fileId, xftpSndFile = Nothing, fileName, filePath, fileSize, fileInline, chunkSize, cancelled = False}
createSndGroupFileTransferConnection :: DB.Connection -> User -> Int64 -> (CommandId, ConnId) -> GroupMember -> IO ()
createSndGroupFileTransferConnection db user@User {userId} fileId (cmdId, acId) GroupMember {groupMemberId} = do
@@ -2660,7 +2679,7 @@ createSndDirectInlineFT db Contact {localDisplayName = n, activeConn = Connectio
db
"INSERT INTO snd_files (file_id, file_status, file_inline, connection_id, created_at, updated_at) VALUES (?,?,?,?,?,?)"
(fileId, fileStatus, fileInline', connId, currentTs, currentTs)
- pure SndFileTransfer {fileId, fileName, filePath, fileSize, chunkSize, recipientDisplayName = n, connId, agentConnId, fileStatus, fileInline = fileInline'}
+ pure SndFileTransfer {fileId, fileName, filePath, fileSize, chunkSize, recipientDisplayName = n, connId, agentConnId, fileStatus, fileDescrId = Nothing, fileInline = fileInline'}
createSndGroupInlineFT :: DB.Connection -> GroupMember -> Connection -> FileTransferMeta -> IO SndFileTransfer
createSndGroupInlineFT db GroupMember {groupMemberId, localDisplayName = n} Connection {connId, agentConnId} FileTransferMeta {fileId, fileName, filePath, fileSize, chunkSize, fileInline} = do
@@ -2671,7 +2690,7 @@ createSndGroupInlineFT db GroupMember {groupMemberId, localDisplayName = n} Conn
db
"INSERT INTO snd_files (file_id, file_status, file_inline, connection_id, group_member_id, created_at, updated_at) VALUES (?,?,?,?,?,?,?)"
(fileId, fileStatus, fileInline', connId, groupMemberId, currentTs, currentTs)
- pure SndFileTransfer {fileId, fileName, filePath, fileSize, chunkSize, recipientDisplayName = n, connId, agentConnId, fileStatus, fileInline = fileInline'}
+ pure SndFileTransfer {fileId, fileName, filePath, fileSize, chunkSize, recipientDisplayName = n, connId, agentConnId, fileStatus, fileDescrId = Nothing, fileInline = fileInline'}
updateSndDirectFTDelivery :: DB.Connection -> Contact -> FileTransferMeta -> Int64 -> IO ()
updateSndDirectFTDelivery db Contact {activeConn = Connection {connId}} FileTransferMeta {fileId} msgDeliveryId =
@@ -2687,27 +2706,60 @@ updateSndGroupFTDelivery db GroupMember {groupMemberId} Connection {connId} File
"UPDATE snd_files SET last_inline_msg_delivery_id = ? WHERE group_member_id = ? AND connection_id = ? AND file_id = ? AND file_inline IS NOT NULL"
(msgDeliveryId, groupMemberId, connId, fileId)
-getSndInlineFTViaMsgDelivery :: DB.Connection -> User -> Connection -> AgentMsgId -> IO (Maybe SndFileTransfer)
-getSndInlineFTViaMsgDelivery db User {userId} Connection {connId, agentConnId} agentMsgId = do
+getSndFTViaMsgDelivery :: DB.Connection -> User -> Connection -> AgentMsgId -> IO (Maybe SndFileTransfer)
+getSndFTViaMsgDelivery db User {userId} Connection {connId, agentConnId} agentMsgId = do
(sndFileTransfer_ <=< listToMaybe)
<$> DB.query
db
[sql|
- SELECT s.file_id, s.file_status, f.file_name, f.file_size, f.chunk_size, f.file_path, s.file_inline, c.local_display_name, m.local_display_name
+ SELECT s.file_id, s.file_status, f.file_name, f.file_size, f.chunk_size, f.file_path, s.file_descr_id, s.file_inline, c.local_display_name, m.local_display_name
FROM msg_deliveries d
JOIN snd_files s ON s.connection_id = d.connection_id AND s.last_inline_msg_delivery_id = d.msg_delivery_id
JOIN files f ON f.file_id = s.file_id
LEFT JOIN contacts c USING (contact_id)
LEFT JOIN group_members m USING (group_member_id)
- WHERE d.connection_id = ? AND d.agent_msg_id = ? AND f.user_id = ? AND s.file_inline IS NOT NULL
+ WHERE d.connection_id = ? AND d.agent_msg_id = ? AND f.user_id = ?
+ AND (s.file_descr_id IS NOT NULL OR s.file_inline IS NOT NULL)
|]
(connId, agentMsgId, userId)
where
- sndFileTransfer_ :: (Int64, FileStatus, String, Integer, Integer, FilePath, Maybe InlineFileMode, Maybe ContactName, Maybe ContactName) -> Maybe SndFileTransfer
- sndFileTransfer_ (fileId, fileStatus, fileName, fileSize, chunkSize, filePath, fileInline, contactName_, memberName_) =
- (\n -> SndFileTransfer {fileId, fileStatus, fileName, fileSize, chunkSize, filePath, fileInline, recipientDisplayName = n, connId, agentConnId})
+ sndFileTransfer_ :: (Int64, FileStatus, String, Integer, Integer, FilePath, Maybe Int64, Maybe InlineFileMode, Maybe ContactName, Maybe ContactName) -> Maybe SndFileTransfer
+ sndFileTransfer_ (fileId, fileStatus, fileName, fileSize, chunkSize, filePath, fileDescrId, fileInline, contactName_, memberName_) =
+ (\n -> SndFileTransfer {fileId, fileStatus, fileName, fileSize, chunkSize, filePath, fileDescrId, fileInline, recipientDisplayName = n, connId, agentConnId})
<$> (contactName_ <|> memberName_)
+createSndFileTransferXFTP :: DB.Connection -> User -> ContactOrGroup -> FilePath -> FileInvitation -> AgentSndFileId -> IO FileTransferMeta
+createSndFileTransferXFTP db User {userId} contactOrGroup filePath FileInvitation {fileName, fileSize} agentSndFileId = do
+ currentTs <- getCurrentTime
+ let chunkSize = 0
+ xftpSndFile = Just XFTPSndFile {agentSndFileId, privateSndFileDescr = Nothing}
+ DB.execute
+ db
+ "INSERT INTO files (contact_id, group_id, user_id, file_name, file_path, file_size, chunk_size, agent_snd_file_id, ci_file_status, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?)"
+ (contactAndGroupIds contactOrGroup :. (userId, fileName, filePath, fileSize, chunkSize, agentSndFileId, CIFSSndStored, currentTs, currentTs))
+ fileId <- insertedRowId db
+ pure FileTransferMeta {fileId, xftpSndFile, fileName, filePath, fileSize, fileInline = Nothing, chunkSize, cancelled = False}
+
+createSndDirectFTDescrXFTP :: DB.Connection -> User -> Contact -> FileTransferMeta -> Text -> IO ()
+createSndDirectFTDescrXFTP db User {userId} Contact {activeConn = Connection {connId}} FileTransferMeta {fileId} rfdText = do
+ let fileStatus = FSConnected
+ DB.execute db "INSERT INTO xftp_file_descriptions (user_id, file_descr_text, file_descr_complete) VALUES (?,?,?)" (userId, rfdText, True)
+ fileDescrId <- insertedRowId db
+ DB.execute
+ db
+ "INSERT INTO snd_files (file_id, file_status, file_descr_id, connection_id) VALUES (?,?,?,?)"
+ (fileId, fileStatus, fileDescrId, connId)
+
+getAgentSndFileXFTP :: DB.Connection -> User -> AgentSndFileId -> ExceptT StoreError IO FileTransferMeta
+getAgentSndFileXFTP db user aSndFileId = do
+ fileId <-
+ ExceptT . firstRow fromOnly (SESndFileNotFoundXFTP aSndFileId) $
+ DB.query db "SELECT file_id FROM files WHERE agent_snd_file_id = ?" (Only aSndFileId)
+ getFileTransferMeta db user fileId
+
+getAgentRcvFileXFTP :: DB.Connection -> User -> AgentRcvFileId -> ExceptT StoreError IO FileTransferMeta
+getAgentRcvFileXFTP _db _user _aFileId = undefined
+
updateFileCancelled :: MsgDirectionI d => DB.Connection -> User -> Int64 -> CIFileStatus d -> IO ()
updateFileCancelled db User {userId} fileId ciFileStatus = do
currentTs <- getCurrentTime
@@ -2845,32 +2897,46 @@ deleteSndFileChunks db SndFileTransfer {fileId, connId} =
DB.execute db "DELETE FROM snd_file_chunks WHERE file_id = ? AND connection_id = ?" (fileId, connId)
createRcvFileTransfer :: DB.Connection -> UserId -> Contact -> FileInvitation -> Maybe InlineFileMode -> Integer -> IO RcvFileTransfer
-createRcvFileTransfer db userId Contact {contactId, localDisplayName = c} f@FileInvitation {fileName, fileSize, fileConnReq, fileInline} rcvFileInline chunkSize = do
+createRcvFileTransfer db userId Contact {contactId, localDisplayName = c} f@FileInvitation {fileName, fileSize, fileConnReq, fileInline, fileDescr} rcvFileInline chunkSize = do
currentTs <- getCurrentTime
DB.execute
db
"INSERT INTO files (user_id, contact_id, file_name, file_size, chunk_size, file_inline, ci_file_status, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)"
(userId, contactId, fileName, fileSize, chunkSize, fileInline, CIFSRcvInvitation, currentTs, currentTs)
fileId <- insertedRowId db
+ rfd <- mapM (createRcvFD_ db) fileDescr
+ let rfdId = (fileDescrId :: RcvFileDescr -> Int64) <$> rfd
DB.execute
db
- "INSERT INTO rcv_files (file_id, file_status, file_queue_info, file_inline, rcv_file_inline, created_at, updated_at) VALUES (?,?,?,?,?,?,?)"
- (fileId, FSNew, fileConnReq, fileInline, rcvFileInline, currentTs, currentTs)
- pure RcvFileTransfer {fileId, fileInvitation = f, fileStatus = RFSNew, rcvFileInline, rcvFileDescription = Nothing, senderDisplayName = c, chunkSize, cancelled = False, grpMemberId = Nothing}
+ "INSERT INTO rcv_files (file_id, file_status, file_queue_info, file_inline, rcv_file_inline, file_descr_id, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)"
+ (fileId, FSNew, fileConnReq, fileInline, rcvFileInline, rfdId, currentTs, currentTs)
+ pure RcvFileTransfer {fileId, fileInvitation = f, fileStatus = RFSNew, rcvFileInline, rcvFileDescription = rfd, senderDisplayName = c, chunkSize, cancelled = False, grpMemberId = Nothing}
createRcvGroupFileTransfer :: DB.Connection -> UserId -> GroupMember -> FileInvitation -> Maybe InlineFileMode -> Integer -> IO RcvFileTransfer
-createRcvGroupFileTransfer db userId GroupMember {groupId, groupMemberId, localDisplayName = c} f@FileInvitation {fileName, fileSize, fileConnReq, fileInline} rcvFileInline chunkSize = do
+createRcvGroupFileTransfer db userId GroupMember {groupId, groupMemberId, localDisplayName = c} f@FileInvitation {fileName, fileSize, fileConnReq, fileInline, fileDescr} rcvFileInline chunkSize = do
currentTs <- getCurrentTime
DB.execute
db
"INSERT INTO files (user_id, group_id, file_name, file_size, chunk_size, file_inline, ci_file_status, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)"
(userId, groupId, fileName, fileSize, chunkSize, fileInline, CIFSRcvInvitation, currentTs, currentTs)
fileId <- insertedRowId db
+ rfd <- mapM (createRcvFD_ db) fileDescr
+ let rfdId = (fileDescrId :: RcvFileDescr -> Int64) <$> rfd
DB.execute
db
- "INSERT INTO rcv_files (file_id, file_status, file_queue_info, file_inline, rcv_file_inline, group_member_id, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)"
- (fileId, FSNew, fileConnReq, fileInline, rcvFileInline, groupMemberId, currentTs, currentTs)
- pure RcvFileTransfer {fileId, fileInvitation = f, fileStatus = RFSNew, rcvFileInline, rcvFileDescription = Nothing, senderDisplayName = c, chunkSize, cancelled = False, grpMemberId = Just groupMemberId}
+ "INSERT INTO rcv_files (file_id, file_status, file_queue_info, file_inline, rcv_file_inline, group_member_id, file_descr_id, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)"
+ (fileId, FSNew, fileConnReq, fileInline, rcvFileInline, groupMemberId, rfdId, currentTs, currentTs)
+ pure RcvFileTransfer {fileId, fileInvitation = f, fileStatus = RFSNew, rcvFileInline, rcvFileDescription = rfd, senderDisplayName = c, chunkSize, cancelled = False, grpMemberId = Just groupMemberId}
+
+createRcvFD_ :: DB.Connection -> FileDescr -> IO RcvFileDescr
+createRcvFD_ db FileDescr {fileDescrText, fileDescrComplete} = do
+ -- TODO validate that fileDescrPartNo = 0, probably when message is received
+ DB.execute
+ db
+ "INSERT INTO file_descriptions (file_descr_text, file_descr_complete) VALUES (?,?)"
+ (fileDescrText, fileDescrComplete)
+ fileDescrId <- insertedRowId db
+ pure RcvFileDescr {fileDescrId, fileDescrPartNo = 0, fileDescrText, fileDescrComplete}
getRcvFileTransferById :: DB.Connection -> FileTransferId -> ExceptT StoreError IO (User, RcvFileTransfer)
getRcvFileTransferById db fileId = do
@@ -3062,7 +3128,7 @@ getSndFileTransfers_ db userId fileId =
<$> DB.query
db
[sql|
- SELECT s.file_status, f.file_name, f.file_size, f.chunk_size, f.file_path, s.file_inline, s.connection_id, c.agent_conn_id,
+ SELECT s.file_status, f.file_name, f.file_size, f.chunk_size, f.file_path, s.file_descr_id, s.file_inline, s.connection_id, c.agent_conn_id,
cs.local_display_name, m.local_display_name
FROM snd_files s
JOIN files f USING (file_id)
@@ -3073,10 +3139,10 @@ getSndFileTransfers_ db userId fileId =
|]
(userId, fileId)
where
- sndFileTransfer :: (FileStatus, String, Integer, Integer, FilePath, Maybe InlineFileMode, Int64, AgentConnId, Maybe ContactName, Maybe ContactName) -> Either StoreError SndFileTransfer
- sndFileTransfer (fileStatus, fileName, fileSize, chunkSize, filePath, fileInline, connId, agentConnId, contactName_, memberName_) =
+ sndFileTransfer :: (FileStatus, String, Integer, Integer, FilePath) :. (Maybe Int64, Maybe InlineFileMode, Int64, AgentConnId, Maybe ContactName, Maybe ContactName) -> Either StoreError SndFileTransfer
+ sndFileTransfer ((fileStatus, fileName, fileSize, chunkSize, filePath) :. (fileDescrId, fileInline, connId, agentConnId, contactName_, memberName_)) =
case contactName_ <|> memberName_ of
- Just recipientDisplayName -> Right SndFileTransfer {fileId, fileStatus, fileName, fileSize, chunkSize, filePath, fileInline, recipientDisplayName, connId, agentConnId}
+ Just recipientDisplayName -> Right SndFileTransfer {fileId, fileStatus, fileName, fileSize, chunkSize, filePath, fileDescrId, fileInline, recipientDisplayName, connId, agentConnId}
Nothing -> Left $ SESndFileInvalid fileId
getFileTransferMeta :: DB.Connection -> User -> Int64 -> ExceptT StoreError IO FileTransferMeta
@@ -3085,15 +3151,16 @@ getFileTransferMeta db User {userId} fileId =
DB.query
db
[sql|
- SELECT f.file_name, f.file_size, f.chunk_size, f.file_path, f.file_inline, f.cancelled
- FROM files f
- WHERE f.user_id = ? AND f.file_id = ?
+ SELECT file_name, file_size, chunk_size, file_path, file_inline, agent_snd_file_id, private_snd_file_descr, cancelled
+ FROM files
+ WHERE user_id = ? AND file_id = ?
|]
(userId, fileId)
where
- fileTransferMeta :: (String, Integer, Integer, FilePath, Maybe InlineFileMode, Maybe Bool) -> FileTransferMeta
- fileTransferMeta (fileName, fileSize, chunkSize, filePath, fileInline, cancelled_) =
- FileTransferMeta {fileId, fileName, fileSize, chunkSize, filePath, fileInline, cancelled = fromMaybe False cancelled_}
+ fileTransferMeta :: (String, Integer, Integer, FilePath, Maybe InlineFileMode, Maybe AgentSndFileId, Maybe Text, Maybe Bool) -> FileTransferMeta
+ fileTransferMeta (fileName, fileSize, chunkSize, filePath, fileInline, aSndFileId_, privateSndFileDescr, cancelled_) =
+ let xftpSndFile = (\fId -> XFTPSndFile {agentSndFileId = fId, privateSndFileDescr}) <$> aSndFileId_
+ in FileTransferMeta {fileId, xftpSndFile, fileName, fileSize, chunkSize, filePath, fileInline, cancelled = fromMaybe False cancelled_}
getContactFileInfo :: DB.Connection -> User -> Contact -> IO [CIFileInfo]
getContactFileInfo db User {userId} Contact {contactId} =
@@ -4979,6 +5046,7 @@ data StoreError
| SERcvFileInvalid {fileId :: FileTransferId}
| SESharedMsgIdNotFoundByFileId {fileId :: FileTransferId}
| SEFileIdNotFoundBySharedMsgId {sharedMsgId :: SharedMsgId}
+ | SESndFileNotFoundXFTP {agentFileId :: AgentSndFileId}
| SEConnectionNotFound {agentConnId :: AgentConnId}
| SEConnectionNotFoundById {connId :: Int64}
| SEPendingConnectionNotFound {connId :: Int64}
diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs
index 17b3c92f9..937ea8117 100644
--- a/src/Simplex/Chat/Types.hs
+++ b/src/Simplex/Chat/Types.hs
@@ -49,7 +49,7 @@ import Database.SQLite.Simple.ToField (ToField (..))
import GHC.Generics (Generic)
import GHC.Records.Compat
import Simplex.FileTransfer.Description (FileDigest)
-import Simplex.Messaging.Agent.Protocol (ACommandTag (..), ACorrId, AParty (..), APartyCmdTag (..), ConnId, ConnectionMode (..), ConnectionRequestUri, InvitationId, SAEntity (..))
+import Simplex.Messaging.Agent.Protocol (ACommandTag (..), ACorrId, AParty (..), APartyCmdTag (..), ConnId, ConnectionMode (..), ConnectionRequestUri, InvitationId, SAEntity (..), UserId)
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (dropPrefix, enumJSON, fromTextField_, sumTypeJSON, taggedObjectJSON)
import Simplex.Messaging.Protocol (AProtoServerWithAuth, ProtoServerWithAuth, ProtocolTypeI)
@@ -126,8 +126,6 @@ instance ToJSON UserInfo where
toJSON = J.genericToJSON J.defaultOptions
toEncoding = J.genericToEncoding J.defaultOptions
-type UserId = Int64
-
type ContactId = Int64
type ProfileId = Int64
@@ -289,6 +287,13 @@ instance ToJSON GroupInfo where toEncoding = J.genericToEncoding J.defaultOption
groupName' :: GroupInfo -> GroupName
groupName' GroupInfo {localDisplayName = g} = g
+data ContactOrGroup = CGContact Contact | CGGroup GroupInfo
+
+contactAndGroupIds :: ContactOrGroup -> (Maybe ContactId, Maybe GroupId)
+contactAndGroupIds = \case
+ CGContact Contact {contactId} -> (Just contactId, Nothing)
+ CGGroup GroupInfo {groupId} -> (Nothing, Just groupId)
+
-- TODO when more settings are added we should create another type to allow partial setting updates (with all Maybe properties)
data ChatSettings = ChatSettings
{ enableNtfs :: Bool
@@ -1457,6 +1462,7 @@ data SndFileTransfer = SndFileTransfer
connId :: Int64,
agentConnId :: AgentConnId,
fileStatus :: FileStatus,
+ fileDescrId :: Maybe Int64,
fileInline :: Maybe InlineFileMode
}
deriving (Eq, Show, Generic)
@@ -1485,19 +1491,27 @@ instance ToJSON FileInvitation where
instance FromJSON FileInvitation where
parseJSON = J.genericParseJSON J.defaultOptions {J.omitNothingFields = True}
-data FileDescr
- = FDText {fileDescrText :: Text}
- | FDInline {fileDescrSize :: Integer, fileDescrInline :: InlineFileMode}
- | FDPending
+data FileDescr = FileDescr {fileDescrText :: Text, fileDescrPartNo :: Int, fileDescrComplete :: Bool}
deriving (Eq, Show, Generic)
instance ToJSON FileDescr where
- toEncoding = J.genericToEncoding . taggedObjectJSON $ dropPrefix "FD"
- toJSON = J.genericToJSON . taggedObjectJSON $ dropPrefix "FD"
+ toEncoding = J.genericToEncoding J.defaultOptions
+ toJSON = J.genericToJSON J.defaultOptions
instance FromJSON FileDescr where
parseJSON = J.genericParseJSON . taggedObjectJSON $ dropPrefix "FD"
+xftpFileInvitation :: FilePath -> Integer -> FileInvitation
+xftpFileInvitation fileName fileSize =
+ FileInvitation
+ { fileName,
+ fileSize,
+ fileDigest = Nothing,
+ fileConnReq = Nothing,
+ fileInline = Nothing,
+ fileDescr = Just FileDescr {fileDescrText = "", fileDescrPartNo = 0, fileDescrComplete = False}
+ }
+
data InlineFileMode
= IFMOffer -- file will be sent inline once accepted
| IFMSent -- file is sent inline without acceptance
@@ -1540,9 +1554,9 @@ instance ToJSON RcvFileTransfer where toEncoding = J.genericToEncoding J.default
data RcvFileDescr = RcvFileDescr
{ fileDescrId :: Int64,
- fileDescrStatus :: RcvFileStatus,
fileDescrText :: Text,
- chunkSize :: Integer
+ fileDescrPartNo :: Int,
+ fileDescrComplete :: Bool
}
deriving (Eq, Show, Generic)
@@ -1594,6 +1608,38 @@ instance FromField AgentConnId where fromField f = AgentConnId <$> fromField f
instance ToField AgentConnId where toField (AgentConnId m) = toField m
+newtype AgentSndFileId = AgentSndFileId ConnId
+ deriving (Eq, Show)
+
+instance StrEncoding AgentSndFileId where
+ strEncode (AgentSndFileId connId) = strEncode connId
+ strDecode s = AgentSndFileId <$> strDecode s
+ strP = AgentSndFileId <$> strP
+
+instance ToJSON AgentSndFileId where
+ toJSON = strToJSON
+ toEncoding = strToJEncoding
+
+instance FromField AgentSndFileId where fromField f = AgentSndFileId <$> fromField f
+
+instance ToField AgentSndFileId where toField (AgentSndFileId m) = toField m
+
+newtype AgentRcvFileId = AgentRcvFileId ConnId
+ deriving (Eq, Show)
+
+instance StrEncoding AgentRcvFileId where
+ strEncode (AgentRcvFileId connId) = strEncode connId
+ strDecode s = AgentRcvFileId <$> strDecode s
+ strP = AgentRcvFileId <$> strP
+
+instance ToJSON AgentRcvFileId where
+ toJSON = strToJSON
+ toEncoding = strToJEncoding
+
+instance FromField AgentRcvFileId where fromField f = AgentRcvFileId <$> fromField f
+
+instance ToField AgentRcvFileId where toField (AgentRcvFileId m) = toField m
+
newtype AgentInvId = AgentInvId InvitationId
deriving (Eq, Show)
@@ -1624,6 +1670,7 @@ instance ToJSON FileTransfer where
data FileTransferMeta = FileTransferMeta
{ fileId :: FileTransferId,
+ xftpSndFile :: Maybe XFTPSndFile,
fileName :: String,
filePath :: String,
fileSize :: Integer,
@@ -1635,10 +1682,19 @@ data FileTransferMeta = FileTransferMeta
instance ToJSON FileTransferMeta where toEncoding = J.genericToEncoding J.defaultOptions
+data XFTPSndFile = XFTPSndFile
+ { agentSndFileId :: AgentSndFileId,
+ privateSndFileDescr :: Maybe Text
+ }
+ deriving (Eq, Show, Generic)
+
+instance ToJSON XFTPSndFile where toEncoding = J.genericToEncoding J.defaultOptions
+
fileTransferCancelled :: FileTransfer -> Bool
fileTransferCancelled (FTSnd FileTransferMeta {cancelled} _) = cancelled
fileTransferCancelled (FTRcv RcvFileTransfer {cancelled}) = cancelled
+-- For XFTP file transfers FSConnected means "uploaded to XFTP relays"
data FileStatus = FSNew | FSAccepted | FSConnected | FSComplete | FSCancelled deriving (Eq, Ord, Show)
instance FromField FileStatus where fromField = fromTextField_ textDecode
diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs
index 0535e5a46..7f73be9dd 100644
--- a/src/Simplex/Chat/View.hs
+++ b/src/Simplex/Chat/View.hs
@@ -130,6 +130,9 @@ responseToView user_ ChatConfig {logLevel, testView} liveItems ts = \case
CRUserDeletedMember u g m -> ttyUser u [ttyGroup' g <> ": you removed " <> ttyMember m <> " from the group"]
CRLeftMemberUser u g -> ttyUser u $ [ttyGroup' g <> ": you left the group"] <> groupPreserved g
CRGroupDeletedUser u g -> ttyUser u [ttyGroup' g <> ": you deleted the group"]
+ CRRcvFileDescrReady _ _ -> []
+ CRRcvFileDescrNotReady _ _ -> []
+ CRRcvFileProgressXFTP _ _ _ _ -> []
CRRcvFileAccepted u ci -> ttyUser u $ savingFile' ci
CRRcvFileAcceptedSndCancelled u ft -> ttyUser u $ viewRcvFileSndCancelled ft
CRSndGroupFileCancelled u _ ftm fts -> ttyUser u $ viewSndGroupFileCancelled ftm fts
@@ -147,6 +150,10 @@ responseToView user_ ChatConfig {logLevel, testView} liveItems ts = \case
CRSndFileStart u _ ft -> ttyUser u $ sendingFile_ "started" ft
CRSndFileComplete u _ ft -> ttyUser u $ sendingFile_ "completed" ft
CRSndFileCancelled _ ft -> sendingFile_ "cancelled" ft
+ CRSndFileStartXFTP _ _ _ -> []
+ CRSndFileProgressXFTP _ _ _ _ _ -> []
+ CRSndFileCompleteXFTP _ _ _ -> []
+ CRSndFileCancelledXFTP _ _ _ -> []
CRSndFileRcvCancelled u _ ft@SndFileTransfer {recipientDisplayName = c} ->
ttyUser u [ttyContact c <> " cancelled receiving " <> sndFile ft]
CRContactConnecting u _ -> ttyUser u []
@@ -1007,7 +1014,7 @@ viewSentFileInvitation to CIFile {fileId, filePath, fileStatus} ts = case filePa
where
ttySentFile fPath = ["/f " <> to <> ttyFilePath fPath] <> cancelSending
cancelSending = case fileStatus of
- CIFSSndTransfer -> []
+ CIFSSndTransfer _ _ -> []
_ -> ["use " <> highlight ("/fc " <> show fileId) <> " to cancel sending"]
sentWithTime_ :: CurrentTime -> [StyledString] -> CIMeta c d -> [StyledString]
@@ -1207,6 +1214,8 @@ viewChatError logLevel = \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]
+ CENoSndFileUser aFileId -> ["error: snd file user not found, file id: " <> sShow aFileId | logLevel <= CLLError]
+ CENoRcvFileUser aFileId -> ["error: rcv file user not found, file id: " <> sShow aFileId | logLevel <= CLLError]
CEActiveUserExists -> ["error: active user already exists"]
CEUserExists name -> ["user with the name " <> ttyContact name <> " already exists"]
CEDifferentActiveUser commandUserId activeUserId -> ["error: different active user, command user id: " <> sShow commandUserId <> ", active user id: " <> sShow activeUserId]
diff --git a/stack.yaml b/stack.yaml
index 208d3ca94..f3e19e6c5 100644
--- a/stack.yaml
+++ b/stack.yaml
@@ -49,7 +49,9 @@ extra-deps:
# - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561
# - ../simplexmq
- github: simplex-chat/simplexmq
- commit: 8fde8e1344699cdcdc67709595c9285cd06bbef3
+ commit: bd4fecf4a84071079cffccfc0f35a916eac0e086
+ - github: kazu-yamamoto/http2
+ commit: 78e18f52295a7f89e828539a03fbcb24931461a3
# - ../direct-sqlcipher
- github: simplex-chat/direct-sqlcipher
commit: 34309410eb2069b029b8fc1872deb1e0db123294
From bfc178faf370335fe3b532d77b46c1da0f1bc754 Mon Sep 17 00:00:00 2001
From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
Date: Tue, 14 Mar 2023 11:42:44 +0400
Subject: [PATCH 02/17] core: process rcv file description (#1997)
* core: process rcv file description
* refactor, groups
* view
* refactor
* update simplexmq
* refactor
---------
Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
---
cabal.project | 2 +-
scripts/nix/sha256map.nix | 2 +-
src/Simplex/Chat.hs | 62 ++++++++++------
src/Simplex/Chat/Controller.hs | 9 ++-
src/Simplex/Chat/Store.hs | 131 ++++++++++++++++++++++++---------
src/Simplex/Chat/View.hs | 1 +
stack.yaml | 2 +-
7 files changed, 147 insertions(+), 62 deletions(-)
diff --git a/cabal.project b/cabal.project
index b9ddbd34d..5da331e77 100644
--- a/cabal.project
+++ b/cabal.project
@@ -7,7 +7,7 @@ constraints: zip +disable-bzip2 +disable-zstd
source-repository-package
type: git
location: https://github.com/simplex-chat/simplexmq.git
- tag: bd4fecf4a84071079cffccfc0f35a916eac0e086
+ tag: ddc2da8fe44f95928213522ec43a40154fd3c050
source-repository-package
type: git
diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix
index bb3d71813..6e1fc47a8 100644
--- a/scripts/nix/sha256map.nix
+++ b/scripts/nix/sha256map.nix
@@ -1,5 +1,5 @@
{
- "https://github.com/simplex-chat/simplexmq.git"."bd4fecf4a84071079cffccfc0f35a916eac0e086" = "11sp91znlnfflilw0gdd64f4z6y9ni88iv7xjrdkyj6yhjqfa4wr";
+ "https://github.com/simplex-chat/simplexmq.git"."ddc2da8fe44f95928213522ec43a40154fd3c050" = "1c6bdl6vhy1h459hwsxdiw27xkckcw53c5g1g8fy2bp8gn9q5k4s";
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
"https://github.com/kazu-yamamoto/http2.git"."78e18f52295a7f89e828539a03fbcb24931461a3" = "05q165anvv0qrcxqbvq1dlvw0l8gmsa9kl6sazk1mfhz2g0yimdk";
"https://github.com/simplex-chat/direct-sqlcipher.git"."34309410eb2069b029b8fc1872deb1e0db123294" = "0kwkmhyfsn2lixdlgl15smgr1h5gjk7fky6abzh8rng2h5ymnffd";
diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs
index f50423069..4c94150fb 100644
--- a/src/Simplex/Chat.hs
+++ b/src/Simplex/Chat.hs
@@ -41,6 +41,7 @@ import qualified Data.Map.Strict as M
import Data.Maybe (catMaybes, fromMaybe, isJust, isNothing, listToMaybe, mapMaybe, maybeToList)
import Data.Text (Text)
import qualified Data.Text as T
+import Data.Text.Encoding (encodeUtf8)
import Data.Time (NominalDiffTime, addUTCTime)
import Data.Time.Clock (UTCTime, diffUTCTime, getCurrentTime, nominalDiffTimeToSeconds)
import Data.Time.Clock.System (SystemTime, systemToUTCTime)
@@ -58,6 +59,8 @@ import Simplex.Chat.Store
import Simplex.Chat.Types
import Simplex.Chat.Util (diffInMicros, diffInSeconds)
import Simplex.FileTransfer.Client.Presets (defaultXFTPServers)
+import Simplex.FileTransfer.Description (ValidFileDescription)
+import Simplex.FileTransfer.Protocol (FileParty (..))
import Simplex.Messaging.Agent as Agent
import Simplex.Messaging.Agent.Client (AgentStatsKey (..))
import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), AgentDatabase (..), InitialAgentServers (..), createAgentStore, defaultAgentConfig)
@@ -168,7 +171,8 @@ newChatController ChatDatabase {chatStore, agentStore} user cfg@ChatConfig {agen
timedItemThreads <- atomically TM.empty
showLiveItems <- newTVarIO False
userXFTPFileConfig <- newTVarIO $ xftpFileConfig cfg
- pure ChatController {activeTo, firstTime, currentUser, smpAgent, agentAsync, chatStore, chatStoreChanged, idsDrg, inputQ, outputQ, notifyQ, chatLock, sndFiles, rcvFiles, currentCalls, config, sendNotification, incognitoMode, filesFolder, expireCIThreads, expireCIFlags, cleanupManagerAsync, timedItemThreads, showLiveItems, userXFTPFileConfig, logFilePath = logFile}
+ tempDirectory <- newTVarIO Nothing
+ pure ChatController {activeTo, firstTime, currentUser, smpAgent, agentAsync, chatStore, chatStoreChanged, idsDrg, inputQ, outputQ, notifyQ, chatLock, sndFiles, rcvFiles, currentCalls, config, sendNotification, incognitoMode, filesFolder, expireCIThreads, expireCIFlags, cleanupManagerAsync, timedItemThreads, showLiveItems, userXFTPFileConfig, tempDirectory, logFilePath = logFile}
where
configServers :: DefaultAgentServers
configServers =
@@ -402,7 +406,7 @@ processChatCommand = \case
(fileSize, fileMode) <- checkSndFile mc file 1
case fileMode of
SendFileSMP fileInline -> smpSndFileTransfer file fileSize fileInline
- SendFileXFTP xftpCfg -> xftpSndFileTransfer user file fileSize xftpCfg 1 $ CGContact ct
+ SendFileXFTP -> xftpSndFileTransfer user file fileSize 1 $ CGContact ct
where
smpSndFileTransfer :: FilePath -> Integer -> Maybe InlineFileMode -> m (FileInvitation, CIFile 'MDSnd, FileTransferMeta)
smpSndFileTransfer file fileSize fileInline = do
@@ -459,7 +463,7 @@ processChatCommand = \case
(fileSize, fileMode) <- checkSndFile mc file $ fromIntegral n
case fileMode of
SendFileSMP fileInline -> smpSndFileTransfer file fileSize fileInline
- SendFileXFTP xftpCfg -> xftpSndFileTransfer user file fileSize xftpCfg n $ CGGroup gInfo
+ SendFileXFTP -> xftpSndFileTransfer user file fileSize n $ CGGroup gInfo
where
smpSndFileTransfer :: FilePath -> Integer -> Maybe InlineFileMode -> m (FileInvitation, CIFile 'MDSnd, FileTransferMeta)
smpSndFileTransfer file fileSize fileInline = do
@@ -524,11 +528,12 @@ processChatCommand = \case
qText = msgContentText qmc
qFileName = maybe qText (T.pack . (fileName :: CIFile d -> String)) ciFile_
qTextOrFile = if T.null qText then qFileName else qText
- xftpSndFileTransfer :: User -> FilePath -> Integer -> XFTPFileConfig -> Int -> ContactOrGroup -> m (FileInvitation, CIFile 'MDSnd, FileTransferMeta)
- xftpSndFileTransfer user file fileSize XFTPFileConfig {tempDirectory} n contactOrGroup = do
+ xftpSndFileTransfer :: User -> FilePath -> Integer -> Int -> ContactOrGroup -> m (FileInvitation, CIFile 'MDSnd, FileTransferMeta)
+ xftpSndFileTransfer user file fileSize n contactOrGroup = do
let fileName = takeFileName file
fInv = xftpFileInvitation fileName fileSize
- aFileId <- withAgent $ \a -> xftpSendFile a (aUserId user) file n tempDirectory
+ tmp <- readTVarIO =<< asks tempDirectory
+ aFileId <- withAgent $ \a -> xftpSendFile a (aUserId user) file n tmp
ft@FileTransferMeta {fileId} <- withStore' $ \db -> createSndFileTransferXFTP db user contactOrGroup file fInv $ AgentSndFileId aFileId
let ciFile = CIFile {fileId, fileName, fileSize, filePath = Just file, fileStatus = CIFSSndStored}
pure (fInv, ciFile, ft)
@@ -1478,7 +1483,7 @@ processChatCommand = \case
fileMode = case xftpCfg of
Just cfg
| fileInline == Just IFMSent || fileSize < minFileSize cfg -> SendFileSMP fileInline
- | otherwise -> SendFileXFTP cfg
+ | otherwise -> SendFileXFTP
_ -> SendFileSMP fileInline
pure (fileSize, fileMode)
inlineFileMode mc InlineFilesConfig {offerChunks, sendChunks, totalSendChunks} chunks n
@@ -2878,35 +2883,46 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
pure ci
messageFileDescription :: Contact -> SharedMsgId -> FileDescr -> MsgMeta -> m ()
- messageFileDescription ct _sharedMsgId _fileDescr msgMeta = do
+ messageFileDescription ct@Contact {contactId} sharedMsgId fileDescr msgMeta = do
checkIntegrityCreateItem (CDDirectRcv ct) msgMeta
- -- find the original chat item and file
- -- re-create file item if it does not exist
- -- check file description part number
- -- append file description part to the record
- -- if file description is complete send it to the agent to receive
- pure ()
+ fileId <- withStore $ \db -> getFileIdBySharedMsgId db userId contactId sharedMsgId
+ processFDMessage fileId fileDescr
groupMessageFileDescription :: GroupInfo -> GroupMember -> SharedMsgId -> FileDescr -> MsgMeta -> m ()
- groupMessageFileDescription _gInfo _m _sharedMsgId _fileDescr _msgMeta = do
- pure ()
+ groupMessageFileDescription GroupInfo {groupId} _m sharedMsgId fileDescr _msgMeta = do
+ fileId <- withStore $ \db -> getGroupFileIdBySharedMsgId db userId groupId sharedMsgId
+ processFDMessage fileId fileDescr
+
+ processFDMessage :: FileTransferId -> FileDescr -> m ()
+ processFDMessage fileId fileDescr = do
+ (rfd, _aci) <- withStore $ \db -> do
+ rfd <- appendRcvFD db userId fileId fileDescr
+ aci <- getChatItemByFileId db user fileId
+ -- ? re-create file item if it does not exist
+ pure (rfd, aci)
+ let RcvFileDescr {fileDescrText, fileDescrComplete} = rfd
+ when fileDescrComplete $ do
+ rd <- parseRcvFileDescription fileDescrText
+ tmp <- readTVarIO =<< asks tempDirectory
+ aFileId <- withAgent $ \a -> xftpReceiveFile a (aUserId user) rd tmp
+ withStore' $ \db -> updateRcvFileAgentId db fileId aFileId
cancelMessageFile :: Contact -> SharedMsgId -> MsgMeta -> m ()
cancelMessageFile ct _sharedMsgId msgMeta = do
checkIntegrityCreateItem (CDDirectRcv ct) msgMeta
-- find the original chat item and file
- -- mark file as cancelled, remove description if excists
+ -- mark file as cancelled, remove description if exists
pure ()
cancelGroupMessageFile :: GroupInfo -> GroupMember -> SharedMsgId -> MsgMeta -> m ()
cancelGroupMessageFile _gInfo _m _sharedMsgId _msgMeta = do
pure ()
- processFileInvitation :: Maybe FileInvitation -> MsgContent -> (DB.Connection -> FileInvitation -> Maybe InlineFileMode -> Integer -> IO RcvFileTransfer) -> m (Maybe (CIFile 'MDRcv))
+ processFileInvitation :: Maybe FileInvitation -> MsgContent -> (DB.Connection -> FileInvitation -> Maybe InlineFileMode -> Integer -> ExceptT StoreError IO RcvFileTransfer) -> m (Maybe (CIFile 'MDRcv))
processFileInvitation fInv_ mc createRcvFT = forM fInv_ $ \fInv@FileInvitation {fileName, fileSize} -> do
ChatConfig {fileChunkSize} <- asks config
inline <- receiveInlineMode fInv (Just mc) fileChunkSize
- ft@RcvFileTransfer {fileId} <- withStore' $ \db -> createRcvFT db fInv inline fileChunkSize
+ ft@RcvFileTransfer {fileId} <- withStore $ \db -> createRcvFT db fInv inline fileChunkSize
(filePath, fileStatus) <- case inline of
Just IFMSent -> do
fPath <- getRcvFilePath fileId Nothing fileName
@@ -3041,7 +3057,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
checkIntegrityCreateItem (CDDirectRcv ct) msgMeta
ChatConfig {fileChunkSize} <- asks config
inline <- receiveInlineMode fInv Nothing fileChunkSize
- RcvFileTransfer {fileId} <- withStore' $ \db -> createRcvFileTransfer db userId ct fInv inline fileChunkSize
+ RcvFileTransfer {fileId} <- withStore $ \db -> createRcvFileTransfer db userId ct fInv inline fileChunkSize
let ciFile = Just $ CIFile {fileId, fileName, fileSize, filePath = Nothing, fileStatus = CIFSRcvInvitation}
ci <- saveRcvChatItem' user (CDDirectRcv ct) msg sharedMsgId_ msgMeta (CIRcvMsgContent $ MCFile "") ciFile Nothing False
toView $ CRNewChatItem user (AChatItem SCTDirect SMDRcv (DirectChat ct) ci)
@@ -3053,7 +3069,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
processGroupFileInvitation' gInfo m@GroupMember {localDisplayName = c} fInv@FileInvitation {fileName, fileSize} msg@RcvMessage {sharedMsgId_} msgMeta = do
ChatConfig {fileChunkSize} <- asks config
inline <- receiveInlineMode fInv Nothing fileChunkSize
- RcvFileTransfer {fileId} <- withStore' $ \db -> createRcvGroupFileTransfer db userId m fInv inline fileChunkSize
+ RcvFileTransfer {fileId} <- withStore $ \db -> createRcvGroupFileTransfer db userId m fInv inline fileChunkSize
let ciFile = Just $ CIFile {fileId, fileName, fileSize, filePath = Nothing, fileStatus = CIFSRcvInvitation}
ci <- saveRcvChatItem' user (CDGroupRcv gInfo m) msg sharedMsgId_ msgMeta (CIRcvMsgContent $ MCFile "") ciFile Nothing False
groupMsgToView gInfo m ci msgMeta
@@ -3565,6 +3581,10 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
groupMsgToView g' m ci msgMeta
createGroupFeatureChangedItems user cd CIRcvGroupFeature g g'
+parseRcvFileDescription :: ChatMonad m => Text -> m (ValidFileDescription 'FRecipient)
+parseRcvFileDescription =
+ liftEither . first (ChatError . CEInvalidFileDescription) . (strDecode . encodeUtf8)
+
sendDirectFileInline :: ChatMonad m => Contact -> FileTransferMeta -> SharedMsgId -> m ()
sendDirectFileInline ct ft sharedMsgId = do
msgDeliveryId <- sendFileInline_ ft sharedMsgId $ sendDirectContactMessage ct
diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs
index cbf66ef4c..bf381218f 100644
--- a/src/Simplex/Chat/Controller.hs
+++ b/src/Simplex/Chat/Controller.hs
@@ -171,6 +171,7 @@ data ChatController = ChatController
timedItemThreads :: TMap (ChatRef, ChatItemId) (TVar (Maybe (Weak ThreadId))),
showLiveItems :: TVar Bool,
userXFTPFileConfig :: TVar (Maybe XFTPFileConfig),
+ tempDirectory :: TVar (Maybe FilePath),
logFilePath :: Maybe FilePath
}
@@ -619,13 +620,12 @@ instance ToJSON ComposedMessage where
toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True}
data XFTPFileConfig = XFTPFileConfig
- { minFileSize :: Integer,
- tempDirectory :: Maybe FilePath
+ { minFileSize :: Integer
}
deriving (Show, Generic, FromJSON)
defaultXFTPFileConfig :: XFTPFileConfig
-defaultXFTPFileConfig = XFTPFileConfig {minFileSize = 0, tempDirectory = Nothing}
+defaultXFTPFileConfig = XFTPFileConfig {minFileSize = 0}
instance ToJSON XFTPFileConfig where
toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True}
@@ -693,7 +693,7 @@ instance ToJSON CoreVersionInfo where toEncoding = J.genericToEncoding J.default
data SendFileMode
= SendFileSMP (Maybe InlineFileMode)
- | SendFileXFTP XFTPFileConfig
+ | SendFileXFTP
deriving (Show, Generic)
data ChatError
@@ -764,6 +764,7 @@ data ChatErrorType
| CEAgentNoSubResult {agentConnId :: AgentConnId}
| CECommandError {message :: String}
| CEAgentCommandError {message :: String}
+ | CEInvalidFileDescription {message :: String}
| CEInternalError {message :: String}
deriving (Show, Exception, Generic)
diff --git a/src/Simplex/Chat/Store.hs b/src/Simplex/Chat/Store.hs
index d80afae62..1e60ead21 100644
--- a/src/Simplex/Chat/Store.hs
+++ b/src/Simplex/Chat/Store.hs
@@ -173,6 +173,8 @@ module Simplex.Chat.Store
deleteSndFileChunks,
createRcvFileTransfer,
createRcvGroupFileTransfer,
+ appendRcvFD,
+ updateRcvFileAgentId,
getRcvFileTransferById,
getRcvFileTransfer,
acceptRcvFileTransfer,
@@ -355,7 +357,7 @@ import Simplex.Chat.Migrations.M20230304_file_description
import Simplex.Chat.Protocol
import Simplex.Chat.Types
import Simplex.Chat.Util (week)
-import Simplex.Messaging.Agent.Protocol (ACorrId, AgentMsgId, ConnId, InvitationId, MsgMeta (..), UserId)
+import Simplex.Messaging.Agent.Protocol (ACorrId, AgentMsgId, ConnId, InvitationId, MsgMeta (..), RcvFileId, UserId)
import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore (..), createSQLiteStore, firstRow, firstRow', maybeFirstRow, withTransaction)
import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..))
import qualified Simplex.Messaging.Crypto as C
@@ -2896,47 +2898,107 @@ deleteSndFileChunks :: DB.Connection -> SndFileTransfer -> IO ()
deleteSndFileChunks db SndFileTransfer {fileId, connId} =
DB.execute db "DELETE FROM snd_file_chunks WHERE file_id = ? AND connection_id = ?" (fileId, connId)
-createRcvFileTransfer :: DB.Connection -> UserId -> Contact -> FileInvitation -> Maybe InlineFileMode -> Integer -> IO RcvFileTransfer
+createRcvFileTransfer :: DB.Connection -> UserId -> Contact -> FileInvitation -> Maybe InlineFileMode -> Integer -> ExceptT StoreError IO RcvFileTransfer
createRcvFileTransfer db userId Contact {contactId, localDisplayName = c} f@FileInvitation {fileName, fileSize, fileConnReq, fileInline, fileDescr} rcvFileInline chunkSize = do
- currentTs <- getCurrentTime
- DB.execute
- db
- "INSERT INTO files (user_id, contact_id, file_name, file_size, chunk_size, file_inline, ci_file_status, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)"
- (userId, contactId, fileName, fileSize, chunkSize, fileInline, CIFSRcvInvitation, currentTs, currentTs)
- fileId <- insertedRowId db
- rfd <- mapM (createRcvFD_ db) fileDescr
+ currentTs <- liftIO getCurrentTime
+ fileId <- liftIO $ do
+ DB.execute
+ db
+ "INSERT INTO files (user_id, contact_id, file_name, file_size, chunk_size, file_inline, ci_file_status, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)"
+ (userId, contactId, fileName, fileSize, chunkSize, fileInline, CIFSRcvInvitation, currentTs, currentTs)
+ insertedRowId db
+ rfd <- mapM (createRcvFD_ db userId) fileDescr
let rfdId = (fileDescrId :: RcvFileDescr -> Int64) <$> rfd
- DB.execute
- db
- "INSERT INTO rcv_files (file_id, file_status, file_queue_info, file_inline, rcv_file_inline, file_descr_id, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)"
- (fileId, FSNew, fileConnReq, fileInline, rcvFileInline, rfdId, currentTs, currentTs)
+ liftIO $
+ DB.execute
+ db
+ "INSERT INTO rcv_files (file_id, file_status, file_queue_info, file_inline, rcv_file_inline, file_descr_id, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)"
+ (fileId, FSNew, fileConnReq, fileInline, rcvFileInline, rfdId, currentTs, currentTs)
pure RcvFileTransfer {fileId, fileInvitation = f, fileStatus = RFSNew, rcvFileInline, rcvFileDescription = rfd, senderDisplayName = c, chunkSize, cancelled = False, grpMemberId = Nothing}
-createRcvGroupFileTransfer :: DB.Connection -> UserId -> GroupMember -> FileInvitation -> Maybe InlineFileMode -> Integer -> IO RcvFileTransfer
+createRcvGroupFileTransfer :: DB.Connection -> UserId -> GroupMember -> FileInvitation -> Maybe InlineFileMode -> Integer -> ExceptT StoreError IO RcvFileTransfer
createRcvGroupFileTransfer db userId GroupMember {groupId, groupMemberId, localDisplayName = c} f@FileInvitation {fileName, fileSize, fileConnReq, fileInline, fileDescr} rcvFileInline chunkSize = do
- currentTs <- getCurrentTime
- DB.execute
- db
- "INSERT INTO files (user_id, group_id, file_name, file_size, chunk_size, file_inline, ci_file_status, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)"
- (userId, groupId, fileName, fileSize, chunkSize, fileInline, CIFSRcvInvitation, currentTs, currentTs)
- fileId <- insertedRowId db
- rfd <- mapM (createRcvFD_ db) fileDescr
+ currentTs <- liftIO getCurrentTime
+ fileId <- liftIO $ do
+ DB.execute
+ db
+ "INSERT INTO files (user_id, group_id, file_name, file_size, chunk_size, file_inline, ci_file_status, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)"
+ (userId, groupId, fileName, fileSize, chunkSize, fileInline, CIFSRcvInvitation, currentTs, currentTs)
+ insertedRowId db
+ rfd <- mapM (createRcvFD_ db userId) fileDescr
let rfdId = (fileDescrId :: RcvFileDescr -> Int64) <$> rfd
- DB.execute
- db
- "INSERT INTO rcv_files (file_id, file_status, file_queue_info, file_inline, rcv_file_inline, group_member_id, file_descr_id, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)"
- (fileId, FSNew, fileConnReq, fileInline, rcvFileInline, groupMemberId, rfdId, currentTs, currentTs)
+ liftIO $
+ DB.execute
+ db
+ "INSERT INTO rcv_files (file_id, file_status, file_queue_info, file_inline, rcv_file_inline, group_member_id, file_descr_id, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)"
+ (fileId, FSNew, fileConnReq, fileInline, rcvFileInline, groupMemberId, rfdId, currentTs, currentTs)
pure RcvFileTransfer {fileId, fileInvitation = f, fileStatus = RFSNew, rcvFileInline, rcvFileDescription = rfd, senderDisplayName = c, chunkSize, cancelled = False, grpMemberId = Just groupMemberId}
-createRcvFD_ :: DB.Connection -> FileDescr -> IO RcvFileDescr
-createRcvFD_ db FileDescr {fileDescrText, fileDescrComplete} = do
- -- TODO validate that fileDescrPartNo = 0, probably when message is received
- DB.execute
- db
- "INSERT INTO file_descriptions (file_descr_text, file_descr_complete) VALUES (?,?)"
- (fileDescrText, fileDescrComplete)
- fileDescrId <- insertedRowId db
- pure RcvFileDescr {fileDescrId, fileDescrPartNo = 0, fileDescrText, fileDescrComplete}
+createRcvFD_ :: DB.Connection -> UserId -> FileDescr -> ExceptT StoreError IO RcvFileDescr
+createRcvFD_ db userId FileDescr {fileDescrText, fileDescrPartNo, fileDescrComplete} = do
+ when (fileDescrPartNo /= 0) $ throwError SERcvFileInvalidDescrPart
+ fileDescrId <- liftIO $ do
+ DB.execute
+ db
+ "INSERT INTO xftp_file_descriptions (user_id, file_descr_text, file_descr_part_no, file_descr_complete) VALUES (?,?,?,?)"
+ (userId, fileDescrText, fileDescrPartNo, fileDescrComplete)
+ insertedRowId db
+ pure RcvFileDescr {fileDescrId, fileDescrPartNo, fileDescrText, fileDescrComplete}
+
+appendRcvFD :: DB.Connection -> UserId -> FileTransferId -> FileDescr -> ExceptT StoreError IO RcvFileDescr
+appendRcvFD db userId fileId fd@FileDescr {fileDescrText, fileDescrPartNo, fileDescrComplete} = do
+ currentTs <- liftIO getCurrentTime
+ liftIO (getRcvFileDescrByFileId_ db fileId) >>= \case
+ Nothing -> do
+ rfd@RcvFileDescr {fileDescrId} <- createRcvFD_ db userId fd
+ liftIO $
+ DB.execute
+ db
+ "UPDATE rcv_files SET file_descr_id = ?, updated_at = ? WHERE file_id = ?"
+ (fileDescrId, currentTs, fileId)
+ pure rfd
+ Just
+ RcvFileDescr
+ { fileDescrId,
+ fileDescrText = rfdText,
+ fileDescrPartNo = rfdPNo,
+ fileDescrComplete = rfdComplete
+ } -> do
+ when (fileDescrPartNo /= rfdPNo + 1 || rfdComplete) $ throwError SERcvFileInvalidDescrPart
+ let fileDescrText' = rfdText <> fileDescrText
+ liftIO $
+ DB.execute
+ db
+ [sql|
+ UPDATE xftp_file_descriptions
+ SET file_descr_text = ?, file_descr_part_no = ?, file_descr_complete = ?
+ WHERE file_descr_id = ?
+ |]
+ (fileDescrText', fileDescrPartNo, fileDescrComplete, fileDescrId)
+ pure RcvFileDescr {fileDescrId, fileDescrText = fileDescrText', fileDescrPartNo, fileDescrComplete}
+
+getRcvFileDescrByFileId_ :: DB.Connection -> FileTransferId -> IO (Maybe RcvFileDescr)
+getRcvFileDescrByFileId_ db fileId =
+ maybeFirstRow toRcvFileDescr $
+ DB.query
+ db
+ [sql|
+ SELECT d.file_descr_id, d.file_descr_text, d.file_descr_part_no, d.file_descr_complete
+ FROM xftp_file_descriptions d
+ JOIN rcv_files f ON f.file_descr_id = d.file_descr_id
+ WHERE f.file_id = ?
+ LIMIT 1
+ |]
+ (Only fileId)
+ where
+ toRcvFileDescr :: (Int64, Text, Int, Bool) -> RcvFileDescr
+ toRcvFileDescr (fileDescrId, fileDescrText, fileDescrPartNo, fileDescrComplete) =
+ RcvFileDescr {fileDescrId, fileDescrText, fileDescrPartNo, fileDescrComplete}
+
+updateRcvFileAgentId :: DB.Connection -> FileTransferId -> RcvFileId -> IO ()
+updateRcvFileAgentId db fileId aFileId = do
+ currentTs <- getCurrentTime
+ DB.execute db "UPDATE rcv_files SET agent_rcv_file_id = ?, updated_at = ? WHERE file_id = ?" (aFileId, currentTs, fileId)
getRcvFileTransferById :: DB.Connection -> FileTransferId -> ExceptT StoreError IO (User, RcvFileTransfer)
getRcvFileTransferById db fileId = do
@@ -5044,6 +5106,7 @@ data StoreError
| SERcvFileNotFound {fileId :: FileTransferId}
| SEFileNotFound {fileId :: FileTransferId}
| SERcvFileInvalid {fileId :: FileTransferId}
+ | SERcvFileInvalidDescrPart
| SESharedMsgIdNotFoundByFileId {fileId :: FileTransferId}
| SEFileIdNotFoundBySharedMsgId {sharedMsgId :: SharedMsgId}
| SESndFileNotFoundXFTP {agentFileId :: AgentSndFileId}
diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs
index 7f73be9dd..8cfbe66cd 100644
--- a/src/Simplex/Chat/View.hs
+++ b/src/Simplex/Chat/View.hs
@@ -1271,6 +1271,7 @@ viewChatError logLevel = \case
CEAgentNoSubResult connId -> ["no subscription result for connection: " <> sShow connId]
CECommandError e -> ["bad chat command: " <> plain e]
CEAgentCommandError e -> ["agent command error: " <> plain e]
+ CEInvalidFileDescription e -> ["invalid file description: " <> plain e]
CEInternalError e -> ["internal chat error: " <> plain e]
-- e -> ["chat error: " <> sShow e]
ChatErrorStore err -> case err of
diff --git a/stack.yaml b/stack.yaml
index f3e19e6c5..a6063f8e0 100644
--- a/stack.yaml
+++ b/stack.yaml
@@ -49,7 +49,7 @@ extra-deps:
# - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561
# - ../simplexmq
- github: simplex-chat/simplexmq
- commit: bd4fecf4a84071079cffccfc0f35a916eac0e086
+ commit: ddc2da8fe44f95928213522ec43a40154fd3c050
- github: kazu-yamamoto/http2
commit: 78e18f52295a7f89e828539a03fbcb24931461a3
# - ../direct-sqlcipher
From e21b4d42368a64842eb5d92eb1f917a10710e59c Mon Sep 17 00:00:00 2001
From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
Date: Tue, 14 Mar 2023 09:28:54 +0000
Subject: [PATCH 03/17] xftp: send file descriptions when ready (#1999)
* xftp: send file descriptions when ready
* remove comments, update progress on completion
* update simplexmq
* fix error condition
Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
* fix conflict
* saveMemberFD
* more efficient list merging
---------
Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
---
src/Simplex/Chat.hs | 92 ++++++++++++++++++++++-----------------
src/Simplex/Chat/Store.hs | 87 +++++++++++++++++++++++-------------
src/Simplex/Chat/Types.hs | 11 ++---
3 files changed, 115 insertions(+), 75 deletions(-)
diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs
index 4c94150fb..96aaf9a8a 100644
--- a/src/Simplex/Chat.hs
+++ b/src/Simplex/Chat.hs
@@ -442,12 +442,12 @@ processChatCommand = \case
quoteData ChatItem {content = CIRcvMsgContent qmc} = pure (qmc, CIQDirectRcv, False)
quoteData _ = throwChatError CEInvalidQuote
CTGroup -> do
- Group gInfo@GroupInfo {groupId, membership, localDisplayName = gName} ms <- withStore $ \db -> getGroup db user chatId
+ g@(Group gInfo@GroupInfo {groupId, membership, localDisplayName = gName} ms) <- withStore $ \db -> getGroup db user chatId
assertUserGroupRole gInfo GRAuthor
if isVoice mc && not (groupFeatureAllowed SGFVoice gInfo)
then pure $ chatCmdError (Just user) ("feature not allowed " <> T.unpack (groupFeatureNameText GFVoice))
else do
- (fInv_, ciFile_, ft_) <- unzipMaybe3 <$> setupSndFileTransfer gInfo (length $ filter memberCurrent ms)
+ (fInv_, ciFile_, ft_) <- unzipMaybe3 <$> setupSndFileTransfer g (length $ filter memberCurrent ms)
timed_ <- sndGroupCITimed live gInfo
(msgContainer, quotedItem_) <- prepareMsg fInv_ timed_ membership
msg@SndMessage {sharedMsgId} <- sendGroupMessage user gInfo ms (XMsgNew msgContainer)
@@ -458,12 +458,12 @@ processChatCommand = \case
setActive $ ActiveG gName
pure $ CRNewChatItem user (AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci)
where
- setupSndFileTransfer :: GroupInfo -> Int -> m (Maybe (FileInvitation, CIFile 'MDSnd, FileTransferMeta))
- setupSndFileTransfer gInfo n = forM file_ $ \file -> do
+ setupSndFileTransfer :: Group -> Int -> m (Maybe (FileInvitation, CIFile 'MDSnd, FileTransferMeta))
+ setupSndFileTransfer g@(Group gInfo _) n = forM file_ $ \file -> do
(fileSize, fileMode) <- checkSndFile mc file $ fromIntegral n
case fileMode of
SendFileSMP fileInline -> smpSndFileTransfer file fileSize fileInline
- SendFileXFTP -> xftpSndFileTransfer user file fileSize n $ CGGroup gInfo
+ SendFileXFTP -> xftpSndFileTransfer user file fileSize n $ CGGroup g
where
smpSndFileTransfer :: FilePath -> Integer -> Maybe InlineFileMode -> m (FileInvitation, CIFile 'MDSnd, FileTransferMeta)
smpSndFileTransfer file fileSize fileInline = do
@@ -531,11 +531,21 @@ processChatCommand = \case
xftpSndFileTransfer :: User -> FilePath -> Integer -> Int -> ContactOrGroup -> m (FileInvitation, CIFile 'MDSnd, FileTransferMeta)
xftpSndFileTransfer user file fileSize n contactOrGroup = do
let fileName = takeFileName file
- fInv = xftpFileInvitation fileName fileSize
+ fileDescr = FileDescr {fileDescrText = "", fileDescrPartNo = 0, fileDescrComplete = False}
+ fInv = xftpFileInvitation fileName fileSize fileDescr
tmp <- readTVarIO =<< asks tempDirectory
aFileId <- withAgent $ \a -> xftpSendFile a (aUserId user) file n tmp
ft@FileTransferMeta {fileId} <- withStore' $ \db -> createSndFileTransferXFTP db user contactOrGroup file fInv $ AgentSndFileId aFileId
let ciFile = CIFile {fileId, fileName, fileSize, filePath = Just file, fileStatus = CIFSSndStored}
+ case contactOrGroup of
+ CGContact Contact {activeConn} -> withStore' $ \db -> createSndFTDescrXFTP db user Nothing activeConn ft fileDescr
+ CGGroup (Group _ ms) -> forM_ ms $ \m -> saveMemberFD m `catchError` (toView . CRChatError (Just user))
+ where
+ -- we are not sending files to pending members, same as with inline files
+ saveMemberFD m@GroupMember {activeConn = Just conn@Connection {connStatus}} =
+ when ((connStatus == ConnReady || connStatus == ConnSndReady) && not (connDisabled conn)) $
+ withStore' $ \db -> createSndFTDescrXFTP db user (Just m) conn ft fileDescr
+ saveMemberFD _ = pure ()
pure (fInv, ciFile, ft)
unzipMaybe3 :: Maybe (a, b, c) -> (Maybe a, Maybe b, Maybe c)
unzipMaybe3 (Just (a, b, c)) = (Just a, Just b, Just c)
@@ -2147,51 +2157,53 @@ processAgentMsgSndFile _corrId aFileId msg =
where
process :: User -> m ()
process user = do
- ft@FileTransferMeta {fileId} <- withStore $ \db -> getAgentSndFileXFTP db user $ AgentSndFileId aFileId
+ fileId <- withStore $ \db -> getAgentSndFileIdXFTP db user $ AgentSndFileId aFileId
case msg of
SFPROG _sent _total -> do
-- update chat item status
-- send status to view
pure ()
SFDONE _sndDescr rfds -> do
- AChatItem _ d cInfo _ci@ChatItem {meta = CIMeta {itemSharedMsgId = msgId_, itemDeleted}} <-
+ ci@(AChatItem _ d cInfo _ci@ChatItem {meta = CIMeta {itemSharedMsgId = msgId_, itemDeleted}}) <-
withStore $ \db -> getChatItemByFileId db user fileId
case (msgId_, itemDeleted) of
- (Just sharedMsgId, Nothing) -> case (rfds, d, cInfo) of
- (rfd : _, SMDSnd, DirectChat ct) -> do
- let rfdText = safeDecodeUtf8 $ strEncode rfd
- withStore' $ \db -> createSndDirectFTDescrXFTP db user ct ft rfdText
- -- TODO update chat item status to show 100% progress
- sendDirectFileDescription ct rfdText ft sharedMsgId
- (_, SMDSnd, GroupChat _g) -> do
- -- store file descriptions and files to snd_files
- -- send messages with descriptions to the recipients
- -- update chat item file status (CIFileStatus)
- -- update sent file status
- -- ??? possibly another event as we need one event per group, not per member
- -- toView $ CRSndFileComplete user ci ft
- pure ()
- _ -> pure () -- TODO error
- _ -> pure () -- TODO error
- pure ()
+ (Just sharedMsgId, Nothing) -> do
+ (ft, sfts) <- withStore $ \db -> getSndFileTransfer db user fileId
+ when (length rfds < length sfts) $ throwChatError $ CEInternalError "not enough XFTP file descriptions to send"
+ toView $ CRSndFileProgressXFTP user ci ft 1 1
+ case (rfds, sfts, d, cInfo) of
+ (rfd : _, sft : _, SMDSnd, DirectChat ct) ->
+ sendFileDescription sft rfd sharedMsgId $ sendDirectContactMessage ct
+ (_, _, SMDSnd, GroupChat g@GroupInfo {groupId}) -> do
+ ms <- withStore' $ \db -> getGroupMembers db user g
+ forM_ (zip rfds $ memberFTs ms) $ \mt -> sendToMember mt `catchError` (toView . CRChatError (Just user))
+ where
+ memberFTs :: [GroupMember] -> [(Connection, SndFileTransfer)]
+ memberFTs ms = M.elems $ M.intersectionWith (,) (M.fromList mConns') (M.fromList sfts')
+ where
+ mConns' = mapMaybe useMember ms
+ sfts' = mapMaybe (\sft@SndFileTransfer {groupMemberId} -> (,sft) <$> groupMemberId) sfts
+ useMember GroupMember {groupMemberId, activeConn = Just conn@Connection {connStatus}}
+ | (connStatus == ConnReady || connStatus == ConnSndReady) && not (connDisabled conn) = Just (groupMemberId, conn)
+ | otherwise = Nothing
+ useMember _ = Nothing
+ sendToMember :: (ValidFileDescription 'FRecipient, (Connection, SndFileTransfer)) -> m ()
+ sendToMember (rfd, (conn, sft)) =
+ sendFileDescription sft rfd sharedMsgId $ \msg' -> sendDirectMessage conn msg' $ GroupId groupId
+ _ -> pure ()
+ _ -> pure () -- TODO error?
where
- sendDirectFileDescription :: Contact -> Text -> FileTransferMeta -> SharedMsgId -> m ()
- sendDirectFileDescription ct rfd ft sharedMsgId = do
- msgDeliveryId <- sendFileDescription_ rfd sharedMsgId $ sendDirectContactMessage ct
- withStore' $ \db -> updateSndDirectFTDelivery db ct ft msgDeliveryId
-
- _sendMemberFileDescription :: GroupMember -> Connection -> Text -> FileTransferMeta -> SharedMsgId -> m ()
- _sendMemberFileDescription m@GroupMember {groupId} conn rfd ft sharedMsgId = do
- msgDeliveryId <- sendFileDescription_ rfd sharedMsgId $ \msg' -> sendDirectMessage conn msg' $ GroupId groupId
- withStore' $ \db -> updateSndGroupFTDelivery db m conn ft msgDeliveryId
-
- sendFileDescription_ :: Text -> SharedMsgId -> (ChatMsgEvent 'Json -> m (SndMessage, Int64)) -> m Int64
- sendFileDescription_ rfdText msgId sendMsg = do
+ sendFileDescription :: SndFileTransfer -> ValidFileDescription 'FRecipient -> SharedMsgId -> (ChatMsgEvent 'Json -> m (SndMessage, Int64)) -> m ()
+ sendFileDescription sft rfd msgId sendMsg = do
+ let rfdText = safeDecodeUtf8 $ strEncode rfd
+ withStore' $ \db -> updateSndFTDescrXFTP db user sft rfdText
partSize <- asks $ xftpDescrPartSize . config
- sendParts 1 partSize rfdText
+ msgDeliveryId <- sendParts 1 partSize rfdText
+ -- msgDeliveryId <- sendFileDescription_ rfd sharedMsgId sendMsg
+ withStore' $ \db -> updateSndFTDeliveryXFTP db sft msgDeliveryId
where
- sendParts partNo partSize rfd = do
- let (part, rest) = T.splitAt partSize rfd
+ sendParts partNo partSize rfdText = do
+ let (part, rest) = T.splitAt partSize rfdText
complete = T.null rest
fileDescr = FileDescr {fileDescrText = part, fileDescrPartNo = partNo, fileDescrComplete = complete}
(_, msgDeliveryId) <- sendMsg $ XMsgFileDescr {msgId, fileDescr}
diff --git a/src/Simplex/Chat/Store.hs b/src/Simplex/Chat/Store.hs
index 1e60ead21..1eea4df69 100644
--- a/src/Simplex/Chat/Store.hs
+++ b/src/Simplex/Chat/Store.hs
@@ -156,8 +156,10 @@ module Simplex.Chat.Store
updateSndGroupFTDelivery,
getSndFTViaMsgDelivery,
createSndFileTransferXFTP,
- createSndDirectFTDescrXFTP,
- getAgentSndFileXFTP,
+ createSndFTDescrXFTP,
+ updateSndFTDescrXFTP,
+ updateSndFTDeliveryXFTP,
+ getAgentSndFileIdXFTP,
getAgentRcvFileXFTP,
updateFileCancelled,
updateCIFileStatus,
@@ -190,6 +192,7 @@ module Simplex.Chat.Store
getFileTransferProgress,
getFileTransferMeta,
getSndFileTransfer,
+ getSndFileTransfers,
getContactFileInfo,
deleteContactCIs,
getGroupFileInfo,
@@ -1742,7 +1745,7 @@ getConnectionEntity db user@User {userId, userContactId} agentConnId = do
DB.query
db
[sql|
- SELECT s.file_status, f.file_name, f.file_size, f.chunk_size, f.file_path, s.file_descr_id, s.file_inline, cs.local_display_name, m.local_display_name
+ SELECT s.file_status, f.file_name, f.file_size, f.chunk_size, f.file_path, s.file_descr_id, s.file_inline, s.group_member_id, cs.local_display_name, m.local_display_name
FROM snd_files s
JOIN files f USING (file_id)
LEFT JOIN contacts cs USING (contact_id)
@@ -1750,10 +1753,10 @@ getConnectionEntity db user@User {userId, userContactId} agentConnId = do
WHERE f.user_id = ? AND f.file_id = ? AND s.connection_id = ?
|]
(userId, fileId, connId)
- sndFileTransfer_ :: Int64 -> Int64 -> (FileStatus, String, Integer, Integer, FilePath, Maybe Int64, Maybe InlineFileMode, Maybe ContactName, Maybe ContactName) -> Either StoreError SndFileTransfer
- sndFileTransfer_ fileId connId (fileStatus, fileName, fileSize, chunkSize, filePath, fileDescrId, fileInline, contactName_, memberName_) =
+ sndFileTransfer_ :: Int64 -> Int64 -> (FileStatus, String, Integer, Integer, FilePath, Maybe Int64, Maybe InlineFileMode, Maybe Int64, Maybe ContactName, Maybe ContactName) -> Either StoreError SndFileTransfer
+ sndFileTransfer_ fileId connId (fileStatus, fileName, fileSize, chunkSize, filePath, fileDescrId, fileInline, groupMemberId, contactName_, memberName_) =
case contactName_ <|> memberName_ of
- Just recipientDisplayName -> Right SndFileTransfer {fileId, fileStatus, fileName, fileSize, chunkSize, filePath, fileDescrId, fileInline, recipientDisplayName, connId, agentConnId}
+ Just recipientDisplayName -> Right SndFileTransfer {fileId, fileStatus, fileName, fileSize, chunkSize, filePath, fileDescrId, fileInline, recipientDisplayName, connId, agentConnId, groupMemberId}
Nothing -> Left $ SESndFileInvalid fileId
getUserContact_ :: Int64 -> ExceptT StoreError IO UserContact
getUserContact_ userContactLinkId = ExceptT $ do
@@ -2681,7 +2684,7 @@ createSndDirectInlineFT db Contact {localDisplayName = n, activeConn = Connectio
db
"INSERT INTO snd_files (file_id, file_status, file_inline, connection_id, created_at, updated_at) VALUES (?,?,?,?,?,?)"
(fileId, fileStatus, fileInline', connId, currentTs, currentTs)
- pure SndFileTransfer {fileId, fileName, filePath, fileSize, chunkSize, recipientDisplayName = n, connId, agentConnId, fileStatus, fileDescrId = Nothing, fileInline = fileInline'}
+ pure SndFileTransfer {fileId, fileName, filePath, fileSize, chunkSize, recipientDisplayName = n, connId, agentConnId, groupMemberId = Nothing, fileStatus, fileDescrId = Nothing, fileInline = fileInline'}
createSndGroupInlineFT :: DB.Connection -> GroupMember -> Connection -> FileTransferMeta -> IO SndFileTransfer
createSndGroupInlineFT db GroupMember {groupMemberId, localDisplayName = n} Connection {connId, agentConnId} FileTransferMeta {fileId, fileName, filePath, fileSize, chunkSize, fileInline} = do
@@ -2692,7 +2695,7 @@ createSndGroupInlineFT db GroupMember {groupMemberId, localDisplayName = n} Conn
db
"INSERT INTO snd_files (file_id, file_status, file_inline, connection_id, group_member_id, created_at, updated_at) VALUES (?,?,?,?,?,?,?)"
(fileId, fileStatus, fileInline', connId, groupMemberId, currentTs, currentTs)
- pure SndFileTransfer {fileId, fileName, filePath, fileSize, chunkSize, recipientDisplayName = n, connId, agentConnId, fileStatus, fileDescrId = Nothing, fileInline = fileInline'}
+ pure SndFileTransfer {fileId, fileName, filePath, fileSize, chunkSize, recipientDisplayName = n, connId, agentConnId, groupMemberId = Just groupMemberId, fileStatus, fileDescrId = Nothing, fileInline = fileInline'}
updateSndDirectFTDelivery :: DB.Connection -> Contact -> FileTransferMeta -> Int64 -> IO ()
updateSndDirectFTDelivery db Contact {activeConn = Connection {connId}} FileTransferMeta {fileId} msgDeliveryId =
@@ -2714,7 +2717,7 @@ getSndFTViaMsgDelivery db User {userId} Connection {connId, agentConnId} agentMs
<$> DB.query
db
[sql|
- SELECT s.file_id, s.file_status, f.file_name, f.file_size, f.chunk_size, f.file_path, s.file_descr_id, s.file_inline, c.local_display_name, m.local_display_name
+ SELECT s.file_id, s.file_status, f.file_name, f.file_size, f.chunk_size, f.file_path, s.file_descr_id, s.file_inline, s.group_member_id, c.local_display_name, m.local_display_name
FROM msg_deliveries d
JOIN snd_files s ON s.connection_id = d.connection_id AND s.last_inline_msg_delivery_id = d.msg_delivery_id
JOIN files f ON f.file_id = s.file_id
@@ -2725,9 +2728,9 @@ getSndFTViaMsgDelivery db User {userId} Connection {connId, agentConnId} agentMs
|]
(connId, agentMsgId, userId)
where
- sndFileTransfer_ :: (Int64, FileStatus, String, Integer, Integer, FilePath, Maybe Int64, Maybe InlineFileMode, Maybe ContactName, Maybe ContactName) -> Maybe SndFileTransfer
- sndFileTransfer_ (fileId, fileStatus, fileName, fileSize, chunkSize, filePath, fileDescrId, fileInline, contactName_, memberName_) =
- (\n -> SndFileTransfer {fileId, fileStatus, fileName, fileSize, chunkSize, filePath, fileDescrId, fileInline, recipientDisplayName = n, connId, agentConnId})
+ sndFileTransfer_ :: (Int64, FileStatus, String, Integer, Integer, FilePath, Maybe Int64, Maybe InlineFileMode, Maybe Int64, Maybe ContactName, Maybe ContactName) -> Maybe SndFileTransfer
+ sndFileTransfer_ (fileId, fileStatus, fileName, fileSize, chunkSize, filePath, fileDescrId, fileInline, groupMemberId, contactName_, memberName_) =
+ (\n -> SndFileTransfer {fileId, fileStatus, fileName, fileSize, chunkSize, filePath, fileDescrId, fileInline, groupMemberId, recipientDisplayName = n, connId, agentConnId})
<$> (contactName_ <|> memberName_)
createSndFileTransferXFTP :: DB.Connection -> User -> ContactOrGroup -> FilePath -> FileInvitation -> AgentSndFileId -> IO FileTransferMeta
@@ -2742,22 +2745,43 @@ createSndFileTransferXFTP db User {userId} contactOrGroup filePath FileInvitatio
fileId <- insertedRowId db
pure FileTransferMeta {fileId, xftpSndFile, fileName, filePath, fileSize, fileInline = Nothing, chunkSize, cancelled = False}
-createSndDirectFTDescrXFTP :: DB.Connection -> User -> Contact -> FileTransferMeta -> Text -> IO ()
-createSndDirectFTDescrXFTP db User {userId} Contact {activeConn = Connection {connId}} FileTransferMeta {fileId} rfdText = do
- let fileStatus = FSConnected
- DB.execute db "INSERT INTO xftp_file_descriptions (user_id, file_descr_text, file_descr_complete) VALUES (?,?,?)" (userId, rfdText, True)
+createSndFTDescrXFTP :: DB.Connection -> User -> Maybe GroupMember -> Connection -> FileTransferMeta -> FileDescr -> IO ()
+createSndFTDescrXFTP db User {userId} m Connection {connId} FileTransferMeta {fileId} FileDescr {fileDescrText, fileDescrPartNo, fileDescrComplete} = do
+ let fileStatus = FSNew
+ DB.execute
+ db
+ "INSERT INTO xftp_file_descriptions (user_id, file_descr_text, file_descr_part_no, file_descr_complete) VALUES (?,?,?,?)"
+ (userId, fileDescrText, fileDescrPartNo, fileDescrComplete)
fileDescrId <- insertedRowId db
DB.execute
db
- "INSERT INTO snd_files (file_id, file_status, file_descr_id, connection_id) VALUES (?,?,?,?)"
- (fileId, fileStatus, fileDescrId, connId)
+ "INSERT INTO snd_files (file_id, file_status, file_descr_id, group_member_id, connection_id) VALUES (?,?,?,?,?)"
+ (fileId, fileStatus, fileDescrId, groupMemberId' <$> m, connId)
-getAgentSndFileXFTP :: DB.Connection -> User -> AgentSndFileId -> ExceptT StoreError IO FileTransferMeta
-getAgentSndFileXFTP db user aSndFileId = do
- fileId <-
- ExceptT . firstRow fromOnly (SESndFileNotFoundXFTP aSndFileId) $
- DB.query db "SELECT file_id FROM files WHERE agent_snd_file_id = ?" (Only aSndFileId)
- getFileTransferMeta db user fileId
+updateSndFTDescrXFTP :: DB.Connection -> User -> SndFileTransfer -> Text -> IO ()
+updateSndFTDescrXFTP db user@User {userId} sft@SndFileTransfer {fileId, fileDescrId} rfdText = do
+ DB.execute
+ db
+ [sql|
+ UPDATE xftp_file_descriptions
+ SET file_descr_text = ?, file_descr_part_no = ?, file_descr_complete = ?
+ WHERE user_id = ? AND file_descr_id = ?
+ |]
+ (rfdText, 1 :: Int, True, userId, fileDescrId)
+ updateCIFileStatus db user fileId $ CIFSSndTransfer 1 1
+ updateSndFileStatus db sft FSConnected
+
+updateSndFTDeliveryXFTP :: DB.Connection -> SndFileTransfer -> Int64 -> IO ()
+updateSndFTDeliveryXFTP db SndFileTransfer {connId, fileId, fileDescrId} msgDeliveryId =
+ DB.execute
+ db
+ "UPDATE snd_files SET last_inline_msg_delivery_id = ? WHERE connection_id = ? AND file_id = ? AND file_descr_id = ?"
+ (msgDeliveryId, connId, fileId, fileDescrId)
+
+getAgentSndFileIdXFTP :: DB.Connection -> User -> AgentSndFileId -> ExceptT StoreError IO Int64
+getAgentSndFileIdXFTP db User {userId} aSndFileId =
+ ExceptT . firstRow fromOnly (SESndFileNotFoundXFTP aSndFileId) $
+ DB.query db "SELECT file_id FROM files WHERE user_id = ? AND agent_snd_file_id = ?" (userId, aSndFileId)
getAgentRcvFileXFTP :: DB.Connection -> User -> AgentRcvFileId -> ExceptT StoreError IO FileTransferMeta
getAgentRcvFileXFTP _db _user _aFileId = undefined
@@ -3179,18 +3203,21 @@ getFileTransfer db user@User {userId} fileId =
(userId, fileId)
getSndFileTransfer :: DB.Connection -> User -> Int64 -> ExceptT StoreError IO (FileTransferMeta, [SndFileTransfer])
-getSndFileTransfer db user@User {userId} fileId = do
+getSndFileTransfer db user fileId = do
fileTransferMeta <- getFileTransferMeta db user fileId
- sndFileTransfers <- ExceptT $ getSndFileTransfers_ db userId fileId
+ sndFileTransfers <- getSndFileTransfers db user fileId
pure (fileTransferMeta, sndFileTransfers)
+getSndFileTransfers :: DB.Connection -> User -> Int64 -> ExceptT StoreError IO [SndFileTransfer]
+getSndFileTransfers db User {userId} fileId = ExceptT $ getSndFileTransfers_ db userId fileId
+
getSndFileTransfers_ :: DB.Connection -> UserId -> Int64 -> IO (Either StoreError [SndFileTransfer])
getSndFileTransfers_ db userId fileId =
mapM sndFileTransfer
<$> DB.query
db
[sql|
- SELECT s.file_status, f.file_name, f.file_size, f.chunk_size, f.file_path, s.file_descr_id, s.file_inline, s.connection_id, c.agent_conn_id,
+ SELECT s.file_status, f.file_name, f.file_size, f.chunk_size, f.file_path, s.file_descr_id, s.file_inline, s.connection_id, c.agent_conn_id, s.group_member_id,
cs.local_display_name, m.local_display_name
FROM snd_files s
JOIN files f USING (file_id)
@@ -3201,10 +3228,10 @@ getSndFileTransfers_ db userId fileId =
|]
(userId, fileId)
where
- sndFileTransfer :: (FileStatus, String, Integer, Integer, FilePath) :. (Maybe Int64, Maybe InlineFileMode, Int64, AgentConnId, Maybe ContactName, Maybe ContactName) -> Either StoreError SndFileTransfer
- sndFileTransfer ((fileStatus, fileName, fileSize, chunkSize, filePath) :. (fileDescrId, fileInline, connId, agentConnId, contactName_, memberName_)) =
+ sndFileTransfer :: (FileStatus, String, Integer, Integer, FilePath) :. (Maybe Int64, Maybe InlineFileMode, Int64, AgentConnId, Maybe Int64, Maybe ContactName, Maybe ContactName) -> Either StoreError SndFileTransfer
+ sndFileTransfer ((fileStatus, fileName, fileSize, chunkSize, filePath) :. (fileDescrId, fileInline, connId, agentConnId, groupMemberId, contactName_, memberName_)) =
case contactName_ <|> memberName_ of
- Just recipientDisplayName -> Right SndFileTransfer {fileId, fileStatus, fileName, fileSize, chunkSize, filePath, fileDescrId, fileInline, recipientDisplayName, connId, agentConnId}
+ Just recipientDisplayName -> Right SndFileTransfer {fileId, fileStatus, fileName, fileSize, chunkSize, filePath, fileDescrId, fileInline, recipientDisplayName, connId, agentConnId, groupMemberId}
Nothing -> Left $ SESndFileInvalid fileId
getFileTransferMeta :: DB.Connection -> User -> Int64 -> ExceptT StoreError IO FileTransferMeta
diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs
index 937ea8117..10f3851c3 100644
--- a/src/Simplex/Chat/Types.hs
+++ b/src/Simplex/Chat/Types.hs
@@ -287,12 +287,12 @@ instance ToJSON GroupInfo where toEncoding = J.genericToEncoding J.defaultOption
groupName' :: GroupInfo -> GroupName
groupName' GroupInfo {localDisplayName = g} = g
-data ContactOrGroup = CGContact Contact | CGGroup GroupInfo
+data ContactOrGroup = CGContact Contact | CGGroup Group
contactAndGroupIds :: ContactOrGroup -> (Maybe ContactId, Maybe GroupId)
contactAndGroupIds = \case
CGContact Contact {contactId} -> (Just contactId, Nothing)
- CGGroup GroupInfo {groupId} -> (Nothing, Just groupId)
+ CGGroup (Group GroupInfo {groupId} _) -> (Nothing, Just groupId)
-- TODO when more settings are added we should create another type to allow partial setting updates (with all Maybe properties)
data ChatSettings = ChatSettings
@@ -1461,6 +1461,7 @@ data SndFileTransfer = SndFileTransfer
recipientDisplayName :: ContactName,
connId :: Int64,
agentConnId :: AgentConnId,
+ groupMemberId :: Maybe Int64,
fileStatus :: FileStatus,
fileDescrId :: Maybe Int64,
fileInline :: Maybe InlineFileMode
@@ -1501,15 +1502,15 @@ instance ToJSON FileDescr where
instance FromJSON FileDescr where
parseJSON = J.genericParseJSON . taggedObjectJSON $ dropPrefix "FD"
-xftpFileInvitation :: FilePath -> Integer -> FileInvitation
-xftpFileInvitation fileName fileSize =
+xftpFileInvitation :: FilePath -> Integer -> FileDescr -> FileInvitation
+xftpFileInvitation fileName fileSize fileDescr =
FileInvitation
{ fileName,
fileSize,
fileDigest = Nothing,
fileConnReq = Nothing,
fileInline = Nothing,
- fileDescr = Just FileDescr {fileDescrText = "", fileDescrPartNo = 0, fileDescrComplete = False}
+ fileDescr = Just fileDescr
}
data InlineFileMode
From 9b7fbfd5130b774bcf0d9a86cd6087705b677ae9 Mon Sep 17 00:00:00 2001
From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
Date: Tue, 14 Mar 2023 15:26:40 +0400
Subject: [PATCH 04/17] core: rcv file events (#2002)
---
src/Simplex/Chat.hs | 22 +++++++++----------
.../Migrations/M20230304_file_description.hs | 4 ++++
src/Simplex/Chat/Migrations/chat_schema.sql | 2 ++
src/Simplex/Chat/Store.hs | 21 ++++++++++++------
4 files changed, 30 insertions(+), 19 deletions(-)
diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs
index 96aaf9a8a..ca0ab31b9 100644
--- a/src/Simplex/Chat.hs
+++ b/src/Simplex/Chat.hs
@@ -2219,18 +2219,20 @@ processAgentMsgRcvFile _corrId aFileId msg =
where
process :: User -> m ()
process user = do
- _rcvFile <- withStore (\db -> getAgentRcvFileXFTP db user $ AgentRcvFileId aFileId)
- -- >>= updateConnStatus
- -- load file transfer meta (add chat item status to type and also contact/group)
+ fileId <- withStore (`getAgentRcvFileIdXFTP` AgentRcvFileId aFileId)
case msg of
RFPROG _sent _total -> do
-- update chat item status
-- send status to view
pure ()
RFDONE _filePath -> do
- -- update chat item status
- -- send status to view
- pure ()
+ ci <- withStore $ \db -> do
+ liftIO $ do
+ updateRcvFileStatus' db fileId FSComplete
+ updateCIFileStatus db user fileId CIFSRcvComplete
+ getChatItemByFileId db user fileId
+ -- ack to agent
+ toView $ CRRcvFileComplete user ci
RFERR _e -> do
-- update chat item status
-- send status to view
@@ -2907,17 +2909,13 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
processFDMessage :: FileTransferId -> FileDescr -> m ()
processFDMessage fileId fileDescr = do
- (rfd, _aci) <- withStore $ \db -> do
- rfd <- appendRcvFD db userId fileId fileDescr
- aci <- getChatItemByFileId db user fileId
- -- ? re-create file item if it does not exist
- pure (rfd, aci)
+ rfd <- withStore $ \db -> appendRcvFD db userId fileId fileDescr
let RcvFileDescr {fileDescrText, fileDescrComplete} = rfd
when fileDescrComplete $ do
rd <- parseRcvFileDescription fileDescrText
tmp <- readTVarIO =<< asks tempDirectory
aFileId <- withAgent $ \a -> xftpReceiveFile a (aUserId user) rd tmp
- withStore' $ \db -> updateRcvFileAgentId db fileId aFileId
+ withStore' $ \db -> updateRcvFileAgentId db fileId (AgentRcvFileId aFileId)
cancelMessageFile :: Contact -> SharedMsgId -> MsgMeta -> m ()
cancelMessageFile ct _sharedMsgId msgMeta = do
diff --git a/src/Simplex/Chat/Migrations/M20230304_file_description.hs b/src/Simplex/Chat/Migrations/M20230304_file_description.hs
index 40e7d0f0a..54bf3b8ff 100644
--- a/src/Simplex/Chat/Migrations/M20230304_file_description.hs
+++ b/src/Simplex/Chat/Migrations/M20230304_file_description.hs
@@ -28,8 +28,12 @@ ALTER TABLE files ADD COLUMN private_snd_file_descr TEXT NULL;
ALTER TABLE snd_files ADD COLUMN file_descr_id INTEGER NULL
REFERENCES xftp_file_descriptions ON DELETE SET NULL;
+CREATE INDEX idx_snd_files_file_descr_id ON snd_files(file_descr_id);
+
ALTER TABLE rcv_files ADD COLUMN file_descr_id INTEGER NULL
REFERENCES xftp_file_descriptions ON DELETE SET NULL;
+CREATE INDEX idx_rcv_files_file_descr_id ON rcv_files(file_descr_id);
+
ALTER TABLE rcv_files ADD COLUMN agent_rcv_file_id BLOB NULL;
|]
diff --git a/src/Simplex/Chat/Migrations/chat_schema.sql b/src/Simplex/Chat/Migrations/chat_schema.sql
index 5c2758508..1e36360b4 100644
--- a/src/Simplex/Chat/Migrations/chat_schema.sql
+++ b/src/Simplex/Chat/Migrations/chat_schema.sql
@@ -567,3 +567,5 @@ CREATE TABLE xftp_file_descriptions(
created_at TEXT NOT NULL DEFAULT(datetime('now')),
updated_at TEXT NOT NULL DEFAULT(datetime('now'))
);
+CREATE INDEX idx_snd_files_file_descr_id ON snd_files(file_descr_id);
+CREATE INDEX idx_rcv_files_file_descr_id ON rcv_files(file_descr_id);
diff --git a/src/Simplex/Chat/Store.hs b/src/Simplex/Chat/Store.hs
index 1eea4df69..44be7f2f7 100644
--- a/src/Simplex/Chat/Store.hs
+++ b/src/Simplex/Chat/Store.hs
@@ -160,7 +160,7 @@ module Simplex.Chat.Store
updateSndFTDescrXFTP,
updateSndFTDeliveryXFTP,
getAgentSndFileIdXFTP,
- getAgentRcvFileXFTP,
+ getAgentRcvFileIdXFTP,
updateFileCancelled,
updateCIFileStatus,
getSharedMsgIdByFileId,
@@ -184,6 +184,7 @@ module Simplex.Chat.Store
acceptRcvInlineFT,
startRcvInlineFT,
updateRcvFileStatus,
+ updateRcvFileStatus',
createRcvFileChunk,
updatedRcvFileChunkStored,
deleteRcvFileChunks,
@@ -360,7 +361,7 @@ import Simplex.Chat.Migrations.M20230304_file_description
import Simplex.Chat.Protocol
import Simplex.Chat.Types
import Simplex.Chat.Util (week)
-import Simplex.Messaging.Agent.Protocol (ACorrId, AgentMsgId, ConnId, InvitationId, MsgMeta (..), RcvFileId, UserId)
+import Simplex.Messaging.Agent.Protocol (ACorrId, AgentMsgId, ConnId, InvitationId, MsgMeta (..), UserId)
import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore (..), createSQLiteStore, firstRow, firstRow', maybeFirstRow, withTransaction)
import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..))
import qualified Simplex.Messaging.Crypto as C
@@ -2783,8 +2784,10 @@ getAgentSndFileIdXFTP db User {userId} aSndFileId =
ExceptT . firstRow fromOnly (SESndFileNotFoundXFTP aSndFileId) $
DB.query db "SELECT file_id FROM files WHERE user_id = ? AND agent_snd_file_id = ?" (userId, aSndFileId)
-getAgentRcvFileXFTP :: DB.Connection -> User -> AgentRcvFileId -> ExceptT StoreError IO FileTransferMeta
-getAgentRcvFileXFTP _db _user _aFileId = undefined
+getAgentRcvFileIdXFTP :: DB.Connection -> AgentRcvFileId -> ExceptT StoreError IO FileTransferId
+getAgentRcvFileIdXFTP db aRcvFileId =
+ ExceptT . firstRow fromOnly (SERcvFileNotFoundXFTP aRcvFileId) $
+ DB.query db "SELECT file_id FROM rcv_files WHERE agent_rcv_file_id = ?" (Only aRcvFileId)
updateFileCancelled :: MsgDirectionI d => DB.Connection -> User -> Int64 -> CIFileStatus d -> IO ()
updateFileCancelled db User {userId} fileId ciFileStatus = do
@@ -3019,7 +3022,7 @@ getRcvFileDescrByFileId_ db fileId =
toRcvFileDescr (fileDescrId, fileDescrText, fileDescrPartNo, fileDescrComplete) =
RcvFileDescr {fileDescrId, fileDescrText, fileDescrPartNo, fileDescrComplete}
-updateRcvFileAgentId :: DB.Connection -> FileTransferId -> RcvFileId -> IO ()
+updateRcvFileAgentId :: DB.Connection -> FileTransferId -> AgentRcvFileId -> IO ()
updateRcvFileAgentId db fileId aFileId = do
currentTs <- getCurrentTime
DB.execute db "UPDATE rcv_files SET agent_rcv_file_id = ?, updated_at = ? WHERE file_id = ?" (aFileId, currentTs, fileId)
@@ -3115,7 +3118,10 @@ acceptRcvFT_ db User {userId} fileId filePath rcvFileInline currentTs = do
(rcvFileInline, FSAccepted, currentTs, fileId)
updateRcvFileStatus :: DB.Connection -> RcvFileTransfer -> FileStatus -> IO ()
-updateRcvFileStatus db RcvFileTransfer {fileId} status = do
+updateRcvFileStatus db RcvFileTransfer {fileId} = updateRcvFileStatus' db fileId
+
+updateRcvFileStatus' :: DB.Connection -> FileTransferId -> FileStatus -> IO ()
+updateRcvFileStatus' db fileId status = do
currentTs <- getCurrentTime
DB.execute db "UPDATE rcv_files SET file_status = ?, updated_at = ? WHERE file_id = ?" (status, currentTs, fileId)
@@ -5136,7 +5142,8 @@ data StoreError
| SERcvFileInvalidDescrPart
| SESharedMsgIdNotFoundByFileId {fileId :: FileTransferId}
| SEFileIdNotFoundBySharedMsgId {sharedMsgId :: SharedMsgId}
- | SESndFileNotFoundXFTP {agentFileId :: AgentSndFileId}
+ | SESndFileNotFoundXFTP {agentSndFileId :: AgentSndFileId}
+ | SERcvFileNotFoundXFTP {agentRcvFileId :: AgentRcvFileId}
| SEConnectionNotFound {agentConnId :: AgentConnId}
| SEConnectionNotFoundById {connId :: Int64}
| SEPendingConnectionNotFound {connId :: Int64}
From fda41817e994670166b4474337a9ef26b820c0e2 Mon Sep 17 00:00:00 2001
From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
Date: Tue, 14 Mar 2023 21:51:35 +0400
Subject: [PATCH 05/17] core: XFTP accept; provide save path to agent (#2005)
---
cabal.project | 2 +-
scripts/nix/sha256map.nix | 2 +-
src/Simplex/Chat.hs | 34 ++++++++++++++++++++++------------
src/Simplex/Chat/Store.hs | 22 ++++++++++++++++------
stack.yaml | 2 +-
5 files changed, 41 insertions(+), 21 deletions(-)
diff --git a/cabal.project b/cabal.project
index 5da331e77..67dce5538 100644
--- a/cabal.project
+++ b/cabal.project
@@ -7,7 +7,7 @@ constraints: zip +disable-bzip2 +disable-zstd
source-repository-package
type: git
location: https://github.com/simplex-chat/simplexmq.git
- tag: ddc2da8fe44f95928213522ec43a40154fd3c050
+ tag: db120b6d2eee04836a132f0bfbca9491cacf3dc8
source-repository-package
type: git
diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix
index 6e1fc47a8..1f4922a61 100644
--- a/scripts/nix/sha256map.nix
+++ b/scripts/nix/sha256map.nix
@@ -1,5 +1,5 @@
{
- "https://github.com/simplex-chat/simplexmq.git"."ddc2da8fe44f95928213522ec43a40154fd3c050" = "1c6bdl6vhy1h459hwsxdiw27xkckcw53c5g1g8fy2bp8gn9q5k4s";
+ "https://github.com/simplex-chat/simplexmq.git"."db120b6d2eee04836a132f0bfbca9491cacf3dc8" = "0md0i4vl84mdmkgwjrmlkipqm9k1rqbld0ld1xxss3z1xdb7fdrj";
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
"https://github.com/kazu-yamamoto/http2.git"."78e18f52295a7f89e828539a03fbcb24931461a3" = "05q165anvv0qrcxqbvq1dlvw0l8gmsa9kl6sazk1mfhz2g0yimdk";
"https://github.com/simplex-chat/direct-sqlcipher.git"."34309410eb2069b029b8fc1872deb1e0db123294" = "0kwkmhyfsn2lixdlgl15smgr1h5gjk7fky6abzh8rng2h5ymnffd";
diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs
index ca0ab31b9..44fff46ab 100644
--- a/src/Simplex/Chat.hs
+++ b/src/Simplex/Chat.hs
@@ -1768,11 +1768,13 @@ acceptFileReceive user@User {userId} RcvFileTransfer {fileId, rcvFileDescription
connIds <- joinAgentConnectionAsync user True connReq . directMessage $ XFileAcpt fName
filePath <- getRcvFilePath fileId filePath_ fName
withStore $ \db -> acceptRcvFileTransfer db user fileId connIds ConnJoined filePath
+ -- XFTP
+ (Just rfd, _) -> do
+ filePath <- getRcvFilePath fileId filePath_ fName
+ ci <- withStore $ \db -> xftpAcceptRcvFT db user fileId filePath
+ receiveViaCompleteFD user fileId filePath rfd
+ pure ci
-- group & direct file protocol
- (Just _fd, _) -> do
- -- check if file description is fully received, error otherwise
- -- pass file description to the agent and save AgentRcvFileId
- throwChatError $ CEFileInternal "XFTP file receiption not implemented"
_ -> do
chatRef <- withStore $ \db -> getChatRefByFileId db user fileId
case (chatRef, grpMemberId) of
@@ -1813,6 +1815,14 @@ acceptFileReceive user@User {userId} RcvFileTransfer {fileId, rcvFileDescription
|| (rcvInline_ == Just True && fileSize <= fileChunkSize * offerChunks)
)
+receiveViaCompleteFD :: ChatMonad m => User -> FileTransferId -> FilePath -> RcvFileDescr -> m ()
+receiveViaCompleteFD user fileId filePath RcvFileDescr {fileDescrText, fileDescrComplete} =
+ when fileDescrComplete $ do
+ rd <- parseRcvFileDescription fileDescrText
+ tmp <- readTVarIO =<< asks tempDirectory
+ aFileId <- withAgent $ \a -> xftpReceiveFile a (aUserId user) rd tmp filePath
+ withStore' $ \db -> updateRcvFileAgentId db fileId (AgentRcvFileId aFileId)
+
getRcvFilePath :: forall m. ChatMonad m => FileTransferId -> Maybe FilePath -> String -> m FilePath
getRcvFilePath fileId fPath_ fn = case fPath_ of
Nothing ->
@@ -2225,7 +2235,7 @@ processAgentMsgRcvFile _corrId aFileId msg =
-- update chat item status
-- send status to view
pure ()
- RFDONE _filePath -> do
+ RFDONE -> do
ci <- withStore $ \db -> do
liftIO $ do
updateRcvFileStatus' db fileId FSComplete
@@ -2909,13 +2919,13 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
processFDMessage :: FileTransferId -> FileDescr -> m ()
processFDMessage fileId fileDescr = do
- rfd <- withStore $ \db -> appendRcvFD db userId fileId fileDescr
- let RcvFileDescr {fileDescrText, fileDescrComplete} = rfd
- when fileDescrComplete $ do
- rd <- parseRcvFileDescription fileDescrText
- tmp <- readTVarIO =<< asks tempDirectory
- aFileId <- withAgent $ \a -> xftpReceiveFile a (aUserId user) rd tmp
- withStore' $ \db -> updateRcvFileAgentId db fileId (AgentRcvFileId aFileId)
+ (rfd, RcvFileTransfer {fileStatus}) <- withStore $ \db -> do
+ rfd <- appendRcvFD db userId fileId fileDescr
+ ft <- getRcvFileTransfer db user fileId
+ pure (rfd, ft)
+ case fileStatus of
+ RFSAccepted RcvFileInfo {filePath} -> receiveViaCompleteFD user fileId filePath rfd
+ _ -> pure ()
cancelMessageFile :: Contact -> SharedMsgId -> MsgMeta -> m ()
cancelMessageFile ct _sharedMsgId msgMeta = do
diff --git a/src/Simplex/Chat/Store.hs b/src/Simplex/Chat/Store.hs
index 44be7f2f7..ea53ef562 100644
--- a/src/Simplex/Chat/Store.hs
+++ b/src/Simplex/Chat/Store.hs
@@ -183,6 +183,7 @@ module Simplex.Chat.Store
getContactByFileId,
acceptRcvInlineFT,
startRcvInlineFT,
+ xftpAcceptRcvFT,
updateRcvFileStatus,
updateRcvFileStatus',
createRcvFileChunk,
@@ -1439,7 +1440,9 @@ getLiveRcvFileTransfers db user@User {userId} = do
SELECT f.file_id
FROM files f
JOIN rcv_files r USING (file_id)
- WHERE f.user_id = ? AND r.file_status IN (?, ?) AND r.rcv_file_inline IS NULL
+ WHERE f.user_id = ? AND r.file_status IN (?, ?)
+ AND r.rcv_file_inline IS NULL
+ AND r.file_descr_id IS NULL
AND r.created_at > ?
|]
(userId, FSAccepted, FSConnected, cutoffTs)
@@ -3050,12 +3053,14 @@ getRcvFileTransfer db User {userId} fileId = do
WHERE f.user_id = ? AND f.file_id = ?
|]
(userId, fileId)
- rcvFileTransfer rftRow
+ rfd <- liftIO $ getRcvFileDescrByFileId_ db fileId
+ rcvFileTransfer rfd rftRow
where
rcvFileTransfer ::
+ Maybe RcvFileDescr ->
(FileStatus, Maybe ConnReqInvitation, Maybe Int64, String, Integer, Integer, Maybe Bool) :. (Maybe ContactName, Maybe ContactName, Maybe FilePath, Maybe InlineFileMode, Maybe InlineFileMode) :. (Maybe Int64, Maybe AgentConnId) ->
ExceptT StoreError IO RcvFileTransfer
- rcvFileTransfer ((fileStatus', fileConnReq, grpMemberId, fileName, fileSize, chunkSize, cancelled_) :. (contactName_, memberName_, filePath_, fileInline, rcvFileInline) :. (connId_, agentConnId_)) = do
+ rcvFileTransfer rcvFileDescription ((fileStatus', fileConnReq, grpMemberId, fileName, fileSize, chunkSize, cancelled_) :. (contactName_, memberName_, filePath_, fileInline, rcvFileInline) :. (connId_, agentConnId_)) = do
let fileInv = FileInvitation {fileName, fileSize, fileDigest = Nothing, fileConnReq, fileInline, fileDescr = Nothing}
fileInfo = (filePath_, connId_, agentConnId_)
case contactName_ <|> memberName_ of
@@ -3069,7 +3074,7 @@ getRcvFileTransfer db User {userId} fileId = do
FSCancelled -> ft name fileInv . RFSCancelled <$> rfi_ fileInfo
where
ft senderDisplayName fileInvitation fileStatus =
- RcvFileTransfer {fileId, fileInvitation, fileStatus, rcvFileInline, rcvFileDescription = Nothing, senderDisplayName, chunkSize, cancelled, grpMemberId}
+ RcvFileTransfer {fileId, fileInvitation, fileStatus, rcvFileInline, rcvFileDescription, senderDisplayName, chunkSize, cancelled, grpMemberId}
rfi fileInfo = maybe (throwError $ SERcvFileInvalid fileId) pure =<< rfi_ fileInfo
rfi_ = \case
(Just filePath, connId, agentConnId) -> pure $ Just RcvFileInfo {filePath, connId, agentConnId}
@@ -3097,7 +3102,7 @@ getContactByFileId db user@User {userId} fileId = do
ExceptT . firstRow fromOnly (SEContactNotFoundByFileId fileId) $
DB.query db "SELECT contact_id FROM files WHERE user_id = ? AND file_id = ?" (userId, fileId)
-acceptRcvInlineFT :: DB.Connection -> User -> Int64 -> FilePath -> ExceptT StoreError IO AChatItem
+acceptRcvInlineFT :: DB.Connection -> User -> FileTransferId -> FilePath -> ExceptT StoreError IO AChatItem
acceptRcvInlineFT db user fileId filePath = do
liftIO $ acceptRcvFT_ db user fileId filePath (Just IFMOffer) =<< getCurrentTime
getChatItemByFileId db user fileId
@@ -3106,7 +3111,12 @@ startRcvInlineFT :: DB.Connection -> User -> RcvFileTransfer -> FilePath -> Mayb
startRcvInlineFT db user RcvFileTransfer {fileId} filePath rcvFileInline =
acceptRcvFT_ db user fileId filePath rcvFileInline =<< getCurrentTime
-acceptRcvFT_ :: DB.Connection -> User -> Int64 -> FilePath -> Maybe InlineFileMode -> UTCTime -> IO ()
+xftpAcceptRcvFT :: DB.Connection -> User -> FileTransferId -> FilePath -> ExceptT StoreError IO AChatItem
+xftpAcceptRcvFT db user fileId filePath = do
+ liftIO $ acceptRcvFT_ db user fileId filePath Nothing =<< getCurrentTime
+ getChatItemByFileId db user fileId
+
+acceptRcvFT_ :: DB.Connection -> User -> FileTransferId -> FilePath -> Maybe InlineFileMode -> UTCTime -> IO ()
acceptRcvFT_ db User {userId} fileId filePath rcvFileInline currentTs = do
DB.execute
db
diff --git a/stack.yaml b/stack.yaml
index a6063f8e0..610e54e77 100644
--- a/stack.yaml
+++ b/stack.yaml
@@ -49,7 +49,7 @@ extra-deps:
# - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561
# - ../simplexmq
- github: simplex-chat/simplexmq
- commit: ddc2da8fe44f95928213522ec43a40154fd3c050
+ commit: db120b6d2eee04836a132f0bfbca9491cacf3dc8
- github: kazu-yamamoto/http2
commit: 78e18f52295a7f89e828539a03fbcb24931461a3
# - ../direct-sqlcipher
From 12200a74ffb2d5494efc8ee8dbd9251e7b90038d Mon Sep 17 00:00:00 2001
From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
Date: Thu, 16 Mar 2023 10:49:57 +0400
Subject: [PATCH 06/17] core: XFTP file transfer test (#2009)
---
cabal.project | 2 +-
scripts/nix/sha256map.nix | 2 +-
src/Simplex/Chat.hs | 57 ++++++++++++++++++++--------------
src/Simplex/Chat/Controller.hs | 1 +
src/Simplex/Chat/Store.hs | 34 +++++++++-----------
src/Simplex/Chat/Types.hs | 2 +-
stack.yaml | 2 +-
tests/ChatTests/Files.hs | 26 +++++++++++++++-
8 files changed, 78 insertions(+), 48 deletions(-)
diff --git a/cabal.project b/cabal.project
index 67dce5538..e66052985 100644
--- a/cabal.project
+++ b/cabal.project
@@ -7,7 +7,7 @@ constraints: zip +disable-bzip2 +disable-zstd
source-repository-package
type: git
location: https://github.com/simplex-chat/simplexmq.git
- tag: db120b6d2eee04836a132f0bfbca9491cacf3dc8
+ tag: a0eb53b891b1f4f765f440020654fbae45bf8b00
source-repository-package
type: git
diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix
index 1f4922a61..a057c9a51 100644
--- a/scripts/nix/sha256map.nix
+++ b/scripts/nix/sha256map.nix
@@ -1,5 +1,5 @@
{
- "https://github.com/simplex-chat/simplexmq.git"."db120b6d2eee04836a132f0bfbca9491cacf3dc8" = "0md0i4vl84mdmkgwjrmlkipqm9k1rqbld0ld1xxss3z1xdb7fdrj";
+ "https://github.com/simplex-chat/simplexmq.git"."a0eb53b891b1f4f765f440020654fbae45bf8b00" = "0nbqj26yzdw3h5p4zdw4l65ybi60f571gpl3244fmmv7ll8v8ys8";
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
"https://github.com/kazu-yamamoto/http2.git"."78e18f52295a7f89e828539a03fbcb24931461a3" = "05q165anvv0qrcxqbvq1dlvw0l8gmsa9kl6sazk1mfhz2g0yimdk";
"https://github.com/simplex-chat/direct-sqlcipher.git"."34309410eb2069b029b8fc1872deb1e0db123294" = "0kwkmhyfsn2lixdlgl15smgr1h5gjk7fky6abzh8rng2h5ymnffd";
diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs
index 44fff46ab..ff05f4fed 100644
--- a/src/Simplex/Chat.hs
+++ b/src/Simplex/Chat.hs
@@ -15,7 +15,7 @@
module Simplex.Chat where
import Control.Applicative (optional, (<|>))
-import Control.Concurrent.STM (retry, stateTVar)
+import Control.Concurrent.STM (retry)
import Control.Logger.Simple
import Control.Monad.Except
import Control.Monad.IO.Unlift
@@ -111,6 +111,7 @@ defaultChatConfig =
xftpDescrPartSize = 14000,
inlineFiles = defaultInlineFilesConfig,
xftpFileConfig = Nothing,
+ tempDir = Nothing,
logLevel = CLLImportant,
subscriptionEvents = False,
hostEvents = False,
@@ -145,7 +146,7 @@ createChatDatabase filePrefix key yesToMigrations = do
pure ChatDatabase {chatStore, agentStore}
newChatController :: ChatDatabase -> Maybe User -> ChatConfig -> ChatOpts -> Maybe (Notification -> IO ()) -> IO ChatController
-newChatController ChatDatabase {chatStore, agentStore} user cfg@ChatConfig {agentConfig = aCfg, defaultServers, inlineFiles} ChatOpts {coreOptions = CoreChatOpts {smpServers, networkConfig, logLevel, logConnections, logServerHosts, logFile, tbqSize}, optFilesFolder, allowInstantFiles} sendToast = do
+newChatController ChatDatabase {chatStore, agentStore} user cfg@ChatConfig {agentConfig = aCfg, defaultServers, inlineFiles, tempDir} ChatOpts {coreOptions = CoreChatOpts {smpServers, networkConfig, logLevel, logConnections, logServerHosts, logFile, tbqSize}, optFilesFolder, allowInstantFiles} sendToast = do
let inlineFiles' = if allowInstantFiles then inlineFiles else inlineFiles {sendChunks = 0, receiveInstant = False}
config = cfg {logLevel, tbqSize, subscriptionEvents = logConnections, hostEvents = logServerHosts, defaultServers = configServers, inlineFiles = inlineFiles'}
sendNotification = fromMaybe (const $ pure ()) sendToast
@@ -171,7 +172,7 @@ newChatController ChatDatabase {chatStore, agentStore} user cfg@ChatConfig {agen
timedItemThreads <- atomically TM.empty
showLiveItems <- newTVarIO False
userXFTPFileConfig <- newTVarIO $ xftpFileConfig cfg
- tempDirectory <- newTVarIO Nothing
+ tempDirectory <- newTVarIO tempDir
pure ChatController {activeTo, firstTime, currentUser, smpAgent, agentAsync, chatStore, chatStoreChanged, idsDrg, inputQ, outputQ, notifyQ, chatLock, sndFiles, rcvFiles, currentCalls, config, sendNotification, incognitoMode, filesFolder, expireCIThreads, expireCIFlags, cleanupManagerAsync, timedItemThreads, showLiveItems, userXFTPFileConfig, tempDirectory, logFilePath = logFile}
where
configServers :: DefaultAgentServers
@@ -535,6 +536,7 @@ processChatCommand = \case
fInv = xftpFileInvitation fileName fileSize fileDescr
tmp <- readTVarIO =<< asks tempDirectory
aFileId <- withAgent $ \a -> xftpSendFile a (aUserId user) file n tmp
+ -- TODO CRSndFileStart event for XFTP
ft@FileTransferMeta {fileId} <- withStore' $ \db -> createSndFileTransferXFTP db user contactOrGroup file fInv $ AgentSndFileId aFileId
let ciFile = CIFile {fileId, fileName, fileSize, filePath = Just file, fileStatus = CIFSSndStored}
case contactOrGroup of
@@ -1766,11 +1768,11 @@ acceptFileReceive user@User {userId} RcvFileTransfer {fileId, rcvFileDescription
-- direct file protocol
(Nothing, Just connReq) -> do
connIds <- joinAgentConnectionAsync user True connReq . directMessage $ XFileAcpt fName
- filePath <- getRcvFilePath fileId filePath_ fName
+ filePath <- getRcvFilePath fileId filePath_ fName True
withStore $ \db -> acceptRcvFileTransfer db user fileId connIds ConnJoined filePath
-- XFTP
(Just rfd, _) -> do
- filePath <- getRcvFilePath fileId filePath_ fName
+ filePath <- getRcvFilePath fileId filePath_ fName False
ci <- withStore $ \db -> xftpAcceptRcvFT db user fileId filePath
receiveViaCompleteFD user fileId filePath rfd
pure ci
@@ -1791,7 +1793,7 @@ acceptFileReceive user@User {userId} RcvFileTransfer {fileId, rcvFileDescription
where
acceptFile :: CommandFunction -> (ChatMsgEvent 'Json -> m ()) -> m AChatItem
acceptFile cmdFunction send = do
- filePath <- getRcvFilePath fileId filePath_ fName
+ filePath <- getRcvFilePath fileId filePath_ fName True
inline <- receiveInline
if
| inline -> do
@@ -1821,10 +1823,19 @@ receiveViaCompleteFD user fileId filePath RcvFileDescr {fileDescrText, fileDescr
rd <- parseRcvFileDescription fileDescrText
tmp <- readTVarIO =<< asks tempDirectory
aFileId <- withAgent $ \a -> xftpReceiveFile a (aUserId user) rd tmp filePath
+ startReceivingFile user fileId
withStore' $ \db -> updateRcvFileAgentId db fileId (AgentRcvFileId aFileId)
-getRcvFilePath :: forall m. ChatMonad m => FileTransferId -> Maybe FilePath -> String -> m FilePath
-getRcvFilePath fileId fPath_ fn = case fPath_ of
+startReceivingFile :: ChatMonad m => User -> FileTransferId -> m ()
+startReceivingFile user fileId = do
+ ci <- withStore $ \db -> do
+ liftIO $ updateRcvFileStatus db fileId FSConnected
+ liftIO $ updateCIFileStatus db user fileId $ CIFSRcvTransfer 0 1
+ getChatItemByFileId db user fileId
+ toView $ CRRcvFileStart user ci
+
+getRcvFilePath :: forall m. ChatMonad m => FileTransferId -> Maybe FilePath -> String -> Bool -> m FilePath
+getRcvFilePath fileId fPath_ fn keepHandle = case fPath_ of
Nothing ->
asks filesFolder >>= readTVarIO >>= \case
Nothing -> do
@@ -1849,9 +1860,15 @@ getRcvFilePath fileId fPath_ fn = case fPath_ of
createEmptyFile fPath = emptyFile fPath `E.catch` (throwChatError . CEFileWrite fPath . (show :: E.SomeException -> String))
emptyFile :: FilePath -> m FilePath
emptyFile fPath = do
- h <- getFileHandle fileId fPath rcvFiles AppendMode
+ h <-
+ if keepHandle
+ then getFileHandle fileId fPath rcvFiles AppendMode
+ else getTmpHandle fPath
liftIO $ B.hPut h "" >> hFlush h
pure fPath
+ getTmpHandle :: FilePath -> m Handle
+ getTmpHandle fPath =
+ liftIO (openFile fPath AppendMode) `E.catch` (throwChatError . CEFileInternal . (show :: E.SomeException -> String))
uniqueCombine :: FilePath -> String -> m FilePath
uniqueCombine filePath fileName = tryCombine (0 :: Int)
where
@@ -2238,7 +2255,7 @@ processAgentMsgRcvFile _corrId aFileId msg =
RFDONE -> do
ci <- withStore $ \db -> do
liftIO $ do
- updateRcvFileStatus' db fileId FSComplete
+ updateRcvFileStatus db fileId FSComplete
updateCIFileStatus db user fileId CIFSRcvComplete
getChatItemByFileId db user fileId
-- ack to agent
@@ -2673,7 +2690,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
case chatMsgEvent of
XOk -> allowAgentConnectionAsync user conn confId XOk -- [async agent commands] no continuation needed, but command should be asynchronous for stability
_ -> pure ()
- CON -> startReceivingFile ft
+ CON -> startReceivingFile user fileId
MSG meta _ msgBody -> do
parseFileChunk msgBody >>= receiveFileChunk ft (Just conn) meta
OK ->
@@ -2688,14 +2705,6 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
-- TODO add debugging output
_ -> pure ()
- startReceivingFile :: RcvFileTransfer -> m ()
- startReceivingFile ft@RcvFileTransfer {fileId} = do
- ci <- withStore $ \db -> do
- liftIO $ updateRcvFileStatus db ft FSConnected
- liftIO $ updateCIFileStatus db user fileId $ CIFSRcvTransfer 0 1
- getChatItemByFileId db user fileId
- toView $ CRRcvFileStart user ci
-
receiveFileChunk :: RcvFileTransfer -> Maybe Connection -> MsgMeta -> FileChunk -> m ()
receiveFileChunk ft@RcvFileTransfer {fileId, chunkSize, cancelled} conn_ meta@MsgMeta {recipient = (msgId, _), integrity} = \case
FileChunkCancel ->
@@ -2720,7 +2729,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
appendFileChunk ft chunkNo chunk
ci <- withStore $ \db -> do
liftIO $ do
- updateRcvFileStatus db ft FSComplete
+ updateRcvFileStatus db fileId FSComplete
updateCIFileStatus db user fileId CIFSRcvComplete
deleteRcvFileChunks db ft
getChatItemByFileId db user fileId
@@ -2945,7 +2954,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
ft@RcvFileTransfer {fileId} <- withStore $ \db -> createRcvFT db fInv inline fileChunkSize
(filePath, fileStatus) <- case inline of
Just IFMSent -> do
- fPath <- getRcvFilePath fileId Nothing fileName
+ fPath <- getRcvFilePath fileId Nothing fileName True
withStore' $ \db -> startRcvInlineFT db user ft fPath inline
pure (Just fPath, CIFSRcvAccepted)
_ -> pure (Nothing, CIFSRcvInvitation)
@@ -3171,9 +3180,9 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
receiveInlineChunk RcvFileTransfer {fileId, fileStatus = RFSNew} FileChunk {chunkNo} _
| chunkNo == 1 = throwChatError $ CEInlineFileProhibited fileId
| otherwise = pure ()
- receiveInlineChunk ft chunk meta = do
+ receiveInlineChunk ft@RcvFileTransfer {fileId} chunk meta = do
case chunk of
- FileChunk {chunkNo} -> when (chunkNo == 1) $ startReceivingFile ft
+ FileChunk {chunkNo} -> when (chunkNo == 1) $ startReceivingFile user fileId
_ -> pure ()
receiveFileChunk ft Nothing meta chunk
@@ -3714,7 +3723,7 @@ cancelRcvFileTransfer user ft@RcvFileTransfer {fileId, rcvFileInline} =
closeFileHandle fileId rcvFiles
withStore' $ \db -> do
updateFileCancelled db user fileId CIFSRcvCancelled
- updateRcvFileStatus db ft FSCancelled
+ updateRcvFileStatus db fileId FSCancelled
deleteRcvFileChunks db ft
pure fileConnId
fileConnId = if isNothing rcvFileInline then liveRcvFileTransferConnId ft else Nothing
diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs
index bf381218f..0cb8f8441 100644
--- a/src/Simplex/Chat/Controller.hs
+++ b/src/Simplex/Chat/Controller.hs
@@ -108,6 +108,7 @@ data ChatConfig = ChatConfig
xftpDescrPartSize :: Int,
inlineFiles :: InlineFilesConfig,
xftpFileConfig :: Maybe XFTPFileConfig, -- Nothing - XFTP is disabled
+ tempDir :: Maybe FilePath,
subscriptionEvents :: Bool,
hostEvents :: Bool,
logLevel :: ChatLogLevel,
diff --git a/src/Simplex/Chat/Store.hs b/src/Simplex/Chat/Store.hs
index ea53ef562..7669816dc 100644
--- a/src/Simplex/Chat/Store.hs
+++ b/src/Simplex/Chat/Store.hs
@@ -185,7 +185,6 @@ module Simplex.Chat.Store
startRcvInlineFT,
xftpAcceptRcvFT,
updateRcvFileStatus,
- updateRcvFileStatus',
createRcvFileChunk,
updatedRcvFileChunkStored,
deleteRcvFileChunks,
@@ -274,7 +273,6 @@ module Simplex.Chat.Store
where
import Control.Applicative ((<|>))
-import Control.Concurrent.STM (stateTVar)
import Control.Exception (Exception)
import qualified Control.Exception as E
import Control.Monad.Except
@@ -562,7 +560,7 @@ getUserByASndFileId db aSndFileId =
getUserByARcvFileId :: DB.Connection -> AgentRcvFileId -> IO (Maybe User)
getUserByARcvFileId db aRcvFileId =
maybeFirstRow toUser $
- DB.query db (userQuery <> " JOIN rcv_files r USING (file_id) JOIN files f ON f.user_id = u.user_id WHERE r.agent_rcv_file_id = ?") (Only aRcvFileId)
+ DB.query db (userQuery <> " JOIN files f ON f.user_id = u.user_id JOIN rcv_files r ON r.file_id = f.file_id WHERE r.agent_rcv_file_id = ?") (Only aRcvFileId)
getUserByContactId :: DB.Connection -> ContactId -> ExceptT StoreError IO User
getUserByContactId db contactId =
@@ -2751,16 +2749,17 @@ createSndFileTransferXFTP db User {userId} contactOrGroup filePath FileInvitatio
createSndFTDescrXFTP :: DB.Connection -> User -> Maybe GroupMember -> Connection -> FileTransferMeta -> FileDescr -> IO ()
createSndFTDescrXFTP db User {userId} m Connection {connId} FileTransferMeta {fileId} FileDescr {fileDescrText, fileDescrPartNo, fileDescrComplete} = do
+ currentTs <- getCurrentTime
let fileStatus = FSNew
DB.execute
db
- "INSERT INTO xftp_file_descriptions (user_id, file_descr_text, file_descr_part_no, file_descr_complete) VALUES (?,?,?,?)"
- (userId, fileDescrText, fileDescrPartNo, fileDescrComplete)
+ "INSERT INTO xftp_file_descriptions (user_id, file_descr_text, file_descr_part_no, file_descr_complete, created_at, updated_at) VALUES (?,?,?,?,?,?)"
+ (userId, fileDescrText, fileDescrPartNo, fileDescrComplete, currentTs, currentTs)
fileDescrId <- insertedRowId db
DB.execute
db
- "INSERT INTO snd_files (file_id, file_status, file_descr_id, group_member_id, connection_id) VALUES (?,?,?,?,?)"
- (fileId, fileStatus, fileDescrId, groupMemberId' <$> m, connId)
+ "INSERT INTO snd_files (file_id, file_status, file_descr_id, group_member_id, connection_id, created_at, updated_at) VALUES (?,?,?,?,?,?,?)"
+ (fileId, fileStatus, fileDescrId, groupMemberId' <$> m, connId, currentTs, currentTs)
updateSndFTDescrXFTP :: DB.Connection -> User -> SndFileTransfer -> Text -> IO ()
updateSndFTDescrXFTP db user@User {userId} sft@SndFileTransfer {fileId, fileDescrId} rfdText = do
@@ -2937,7 +2936,7 @@ createRcvFileTransfer db userId Contact {contactId, localDisplayName = c} f@File
"INSERT INTO files (user_id, contact_id, file_name, file_size, chunk_size, file_inline, ci_file_status, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)"
(userId, contactId, fileName, fileSize, chunkSize, fileInline, CIFSRcvInvitation, currentTs, currentTs)
insertedRowId db
- rfd <- mapM (createRcvFD_ db userId) fileDescr
+ rfd <- mapM (createRcvFD_ db userId currentTs) fileDescr
let rfdId = (fileDescrId :: RcvFileDescr -> Int64) <$> rfd
liftIO $
DB.execute
@@ -2955,7 +2954,7 @@ createRcvGroupFileTransfer db userId GroupMember {groupId, groupMemberId, localD
"INSERT INTO files (user_id, group_id, file_name, file_size, chunk_size, file_inline, ci_file_status, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)"
(userId, groupId, fileName, fileSize, chunkSize, fileInline, CIFSRcvInvitation, currentTs, currentTs)
insertedRowId db
- rfd <- mapM (createRcvFD_ db userId) fileDescr
+ rfd <- mapM (createRcvFD_ db userId currentTs) fileDescr
let rfdId = (fileDescrId :: RcvFileDescr -> Int64) <$> rfd
liftIO $
DB.execute
@@ -2964,14 +2963,14 @@ createRcvGroupFileTransfer db userId GroupMember {groupId, groupMemberId, localD
(fileId, FSNew, fileConnReq, fileInline, rcvFileInline, groupMemberId, rfdId, currentTs, currentTs)
pure RcvFileTransfer {fileId, fileInvitation = f, fileStatus = RFSNew, rcvFileInline, rcvFileDescription = rfd, senderDisplayName = c, chunkSize, cancelled = False, grpMemberId = Just groupMemberId}
-createRcvFD_ :: DB.Connection -> UserId -> FileDescr -> ExceptT StoreError IO RcvFileDescr
-createRcvFD_ db userId FileDescr {fileDescrText, fileDescrPartNo, fileDescrComplete} = do
+createRcvFD_ :: DB.Connection -> UserId -> UTCTime -> FileDescr -> ExceptT StoreError IO RcvFileDescr
+createRcvFD_ db userId currentTs FileDescr {fileDescrText, fileDescrPartNo, fileDescrComplete} = do
when (fileDescrPartNo /= 0) $ throwError SERcvFileInvalidDescrPart
fileDescrId <- liftIO $ do
DB.execute
db
- "INSERT INTO xftp_file_descriptions (user_id, file_descr_text, file_descr_part_no, file_descr_complete) VALUES (?,?,?,?)"
- (userId, fileDescrText, fileDescrPartNo, fileDescrComplete)
+ "INSERT INTO xftp_file_descriptions (user_id, file_descr_text, file_descr_part_no, file_descr_complete, created_at, updated_at) VALUES (?,?,?,?,?,?)"
+ (userId, fileDescrText, fileDescrPartNo, fileDescrComplete, currentTs, currentTs)
insertedRowId db
pure RcvFileDescr {fileDescrId, fileDescrPartNo, fileDescrText, fileDescrComplete}
@@ -2980,7 +2979,7 @@ appendRcvFD db userId fileId fd@FileDescr {fileDescrText, fileDescrPartNo, fileD
currentTs <- liftIO getCurrentTime
liftIO (getRcvFileDescrByFileId_ db fileId) >>= \case
Nothing -> do
- rfd@RcvFileDescr {fileDescrId} <- createRcvFD_ db userId fd
+ rfd@RcvFileDescr {fileDescrId} <- createRcvFD_ db userId currentTs fd
liftIO $
DB.execute
db
@@ -3127,11 +3126,8 @@ acceptRcvFT_ db User {userId} fileId filePath rcvFileInline currentTs = do
"UPDATE rcv_files SET rcv_file_inline = ?, file_status = ?, updated_at = ? WHERE file_id = ?"
(rcvFileInline, FSAccepted, currentTs, fileId)
-updateRcvFileStatus :: DB.Connection -> RcvFileTransfer -> FileStatus -> IO ()
-updateRcvFileStatus db RcvFileTransfer {fileId} = updateRcvFileStatus' db fileId
-
-updateRcvFileStatus' :: DB.Connection -> FileTransferId -> FileStatus -> IO ()
-updateRcvFileStatus' db fileId status = do
+updateRcvFileStatus :: DB.Connection -> FileTransferId -> FileStatus -> IO ()
+updateRcvFileStatus db fileId status = do
currentTs <- getCurrentTime
DB.execute db "UPDATE rcv_files SET file_status = ?, updated_at = ? WHERE file_id = ?" (status, currentTs, fileId)
diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs
index 10f3851c3..6c475b24f 100644
--- a/src/Simplex/Chat/Types.hs
+++ b/src/Simplex/Chat/Types.hs
@@ -1500,7 +1500,7 @@ instance ToJSON FileDescr where
toJSON = J.genericToJSON J.defaultOptions
instance FromJSON FileDescr where
- parseJSON = J.genericParseJSON . taggedObjectJSON $ dropPrefix "FD"
+ parseJSON = J.genericParseJSON J.defaultOptions
xftpFileInvitation :: FilePath -> Integer -> FileDescr -> FileInvitation
xftpFileInvitation fileName fileSize fileDescr =
diff --git a/stack.yaml b/stack.yaml
index 610e54e77..aa8d3d969 100644
--- a/stack.yaml
+++ b/stack.yaml
@@ -49,7 +49,7 @@ extra-deps:
# - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561
# - ../simplexmq
- github: simplex-chat/simplexmq
- commit: db120b6d2eee04836a132f0bfbca9491cacf3dc8
+ commit: a0eb53b891b1f4f765f440020654fbae45bf8b00
- github: kazu-yamamoto/http2
commit: 78e18f52295a7f89e828539a03fbcb24931461a3
# - ../direct-sqlcipher
diff --git a/tests/ChatTests/Files.hs b/tests/ChatTests/Files.hs
index b28f6c0d0..16335d628 100644
--- a/tests/ChatTests/Files.hs
+++ b/tests/ChatTests/Files.hs
@@ -8,7 +8,7 @@ import ChatTests.Utils
import Control.Concurrent (threadDelay)
import Control.Concurrent.Async (concurrently_)
import qualified Data.ByteString.Char8 as B
-import Simplex.Chat.Controller (ChatConfig (..), InlineFilesConfig (..), defaultInlineFilesConfig)
+import Simplex.Chat.Controller (ChatConfig (..), InlineFilesConfig (..), XFTPFileConfig (..), defaultInlineFilesConfig)
import Simplex.Chat.Options (ChatOpts (..))
import Simplex.Messaging.Util (unlessM)
import System.Directory (copyFile, doesFileExist)
@@ -48,6 +48,8 @@ chatFileTests = do
it "v2" testAsyncFileTransfer
it "v1" testAsyncFileTransferV1
xit "send and receive file to group, fully asynchronous" testAsyncGroupFileTransfer
+ describe "file transfer over XFTP" $ do
+ it "send and receive file" testXFTPFileTransfer
runTestFileTransfer :: HasCallStack => TestCC -> TestCC -> IO ()
runTestFileTransfer alice bob = do
@@ -915,6 +917,28 @@ testAsyncGroupFileTransfer tmp = do
dest2 <- B.readFile "./tests/tmp/test_1.jpg"
dest2 `shouldBe` src
+testXFTPFileTransfer :: HasCallStack => FilePath -> IO ()
+testXFTPFileTransfer =
+ testChatCfg2 cfg aliceProfile bobProfile $ \alice bob -> do
+ connectUsers alice bob
+
+ alice #> "/f @bob ./tests/fixtures/test.pdf"
+ alice <## "use /fc 1 to cancel sending"
+ bob <# "alice> sends file test.pdf (266.0 KiB / 272376 bytes)"
+ bob <## "use /fr 1 [/ | ] to receive it"
+ bob ##> "/fr 1 ./tests/tmp"
+ bob <## "saving file 1 from alice to ./tests/tmp/test.pdf"
+ -- alice <## "started sending file 1 (test.pdf) to bob" -- TODO "started uploading" ?
+ alice <## "completed sending file 1 (test.pdf) to bob"
+ bob <## "started receiving file 1 (test.pdf) from alice"
+ bob <## "completed receiving file 1 (test.pdf) from alice"
+
+ src <- B.readFile "./tests/fixtures/test.pdf"
+ dest <- B.readFile "./tests/tmp/test.pdf"
+ dest `shouldBe` src
+ where
+ cfg = testCfg {xftpFileConfig = Just $ XFTPFileConfig {minFileSize = 0}, tempDir = Just "./tests/tmp"}
+
startFileTransfer :: HasCallStack => TestCC -> TestCC -> IO ()
startFileTransfer alice bob =
startFileTransfer' alice bob "test.jpg" "136.5 KiB / 139737 bytes"
From 34a3387830e5c7569013e47fc315e89cd9ed30f4 Mon Sep 17 00:00:00 2001
From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
Date: Thu, 16 Mar 2023 14:12:19 +0400
Subject: [PATCH 07/17] core: xftp servers option; use local xftp server in
tests (#2015)
---
src/Simplex/Chat.hs | 5 +++--
src/Simplex/Chat/Mobile.hs | 1 +
src/Simplex/Chat/Options.hs | 18 ++++++++++++++++-
tests/ChatClient.hs | 40 +++++++++++++++++++++++++++++++++++++
tests/ChatTests/Files.hs | 29 ++++++++++++++-------------
5 files changed, 76 insertions(+), 17 deletions(-)
diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs
index ff05f4fed..51b0c5278 100644
--- a/src/Simplex/Chat.hs
+++ b/src/Simplex/Chat.hs
@@ -146,7 +146,7 @@ createChatDatabase filePrefix key yesToMigrations = do
pure ChatDatabase {chatStore, agentStore}
newChatController :: ChatDatabase -> Maybe User -> ChatConfig -> ChatOpts -> Maybe (Notification -> IO ()) -> IO ChatController
-newChatController ChatDatabase {chatStore, agentStore} user cfg@ChatConfig {agentConfig = aCfg, defaultServers, inlineFiles, tempDir} ChatOpts {coreOptions = CoreChatOpts {smpServers, networkConfig, logLevel, logConnections, logServerHosts, logFile, tbqSize}, optFilesFolder, allowInstantFiles} sendToast = do
+newChatController ChatDatabase {chatStore, agentStore} user cfg@ChatConfig {agentConfig = aCfg, defaultServers, inlineFiles, tempDir} ChatOpts {coreOptions = CoreChatOpts {smpServers, xftpServers, networkConfig, logLevel, logConnections, logServerHosts, logFile, tbqSize}, optFilesFolder, allowInstantFiles} sendToast = do
let inlineFiles' = if allowInstantFiles then inlineFiles else inlineFiles {sendChunks = 0, receiveInstant = False}
config = cfg {logLevel, tbqSize, subscriptionEvents = logConnections, hostEvents = logServerHosts, defaultServers = configServers, inlineFiles = inlineFiles'}
sendNotification = fromMaybe (const $ pure ()) sendToast
@@ -178,7 +178,8 @@ newChatController ChatDatabase {chatStore, agentStore} user cfg@ChatConfig {agen
configServers :: DefaultAgentServers
configServers =
let smp' = fromMaybe (smp (defaultServers :: DefaultAgentServers)) (nonEmpty smpServers)
- in defaultServers {smp = smp', netCfg = networkConfig}
+ xftp' = fromMaybe (xftp (defaultServers :: DefaultAgentServers)) (nonEmpty xftpServers)
+ in defaultServers {smp = smp', xftp = xftp', netCfg = networkConfig}
agentServers :: ChatConfig -> IO InitialAgentServers
agentServers config@ChatConfig {defaultServers = defServers@DefaultAgentServers {ntf, netCfg}} = do
users <- withTransaction chatStore getUsers
diff --git a/src/Simplex/Chat/Mobile.hs b/src/Simplex/Chat/Mobile.hs
index 172a0c179..d40df7cfe 100644
--- a/src/Simplex/Chat/Mobile.hs
+++ b/src/Simplex/Chat/Mobile.hs
@@ -130,6 +130,7 @@ mobileChatOpts dbFilePrefix dbKey =
{ dbFilePrefix,
dbKey,
smpServers = [],
+ xftpServers = [],
networkConfig = defaultNetworkConfig,
logLevel = CLLImportant,
logConnections = False,
diff --git a/src/Simplex/Chat/Options.hs b/src/Simplex/Chat/Options.hs
index 8631e7a27..20053a806 100644
--- a/src/Simplex/Chat/Options.hs
+++ b/src/Simplex/Chat/Options.hs
@@ -25,7 +25,7 @@ import Simplex.Chat.Controller (ChatLogLevel (..), updateStr, versionNumber, ver
import Simplex.Messaging.Client (NetworkConfig (..), defaultNetworkConfig)
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (parseAll)
-import Simplex.Messaging.Protocol (SMPServerWithAuth)
+import Simplex.Messaging.Protocol (SMPServerWithAuth, XFTPServerWithAuth)
import Simplex.Messaging.Transport.Client (SocksProxy, defaultSocksProxy)
import System.FilePath (combine)
@@ -43,6 +43,7 @@ data CoreChatOpts = CoreChatOpts
{ dbFilePrefix :: String,
dbKey :: String,
smpServers :: [SMPServerWithAuth],
+ xftpServers :: [XFTPServerWithAuth],
networkConfig :: NetworkConfig,
logLevel :: ChatLogLevel,
logConnections :: Bool,
@@ -88,6 +89,14 @@ coreChatOptsP appDir defaultDbFileName = do
<> help "Semicolon-separated list of SMP server(s) to use (each server can have more than one hostname)"
<> value []
)
+ xftpServers <-
+ option
+ parseXFTPServers
+ ( long "xftp-server"
+ <> metavar "SERVER"
+ <> help "Semicolon-separated list of XFTP server(s) to use (each server can have more than one hostname)"
+ <> value []
+ )
socksProxy <-
flag' (Just defaultSocksProxy) (short 'x' <> help "Use local SOCKS5 proxy at :9050")
<|> option
@@ -156,6 +165,7 @@ coreChatOptsP appDir defaultDbFileName = do
{ dbFilePrefix,
dbKey,
smpServers,
+ xftpServers,
networkConfig = fullNetworkConfig socksProxy (useTcpTimeout socksProxy t) (logTLSErrors || logLevel == CLLDebug),
logLevel,
logConnections = logConnections || logLevel <= CLLInfo,
@@ -236,6 +246,9 @@ fullNetworkConfig socksProxy tcpTimeout logTLSErrors =
parseSMPServers :: ReadM [SMPServerWithAuth]
parseSMPServers = eitherReader $ parseAll smpServersP . B.pack
+parseXFTPServers :: ReadM [XFTPServerWithAuth]
+parseXFTPServers = eitherReader $ parseAll xftpServersP . B.pack
+
parseSocksProxy :: ReadM (Maybe SocksProxy)
parseSocksProxy = eitherReader $ parseAll strP . B.pack
@@ -248,6 +261,9 @@ serverPortP = Just . B.unpack <$> A.takeWhile A.isDigit
smpServersP :: A.Parser [SMPServerWithAuth]
smpServersP = strP `A.sepBy1` A.char ';'
+xftpServersP :: A.Parser [XFTPServerWithAuth]
+xftpServersP = strP `A.sepBy1` A.char ';'
+
parseLogLevel :: ReadM ChatLogLevel
parseLogLevel = eitherReader $ \case
"debug" -> Right CLLDebug
diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs
index b914dd2e3..3d782603a 100644
--- a/tests/ChatClient.hs
+++ b/tests/ChatClient.hs
@@ -26,6 +26,9 @@ import Simplex.Chat.Store
import Simplex.Chat.Terminal
import Simplex.Chat.Terminal.Output (newChatTerminal)
import Simplex.Chat.Types (AgentUserId (..), Profile, User (..))
+import Simplex.FileTransfer.Description (kb, mb)
+import Simplex.FileTransfer.Server (runXFTPServerBlocking)
+import Simplex.FileTransfer.Server.Env (XFTPServerConfig (..), defaultFileExpiration)
import Simplex.Messaging.Agent.Env.SQLite
import Simplex.Messaging.Agent.RetryInterval
import Simplex.Messaging.Client (ProtocolClientConfig (..), defaultNetworkConfig)
@@ -55,6 +58,7 @@ testOpts =
dbKey = "",
-- dbKey = "this is a pass-phrase to encrypt the database",
smpServers = ["smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001"],
+ xftpServers = ["xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:7002"],
networkConfig = defaultNetworkConfig,
logLevel = CLLImportant,
logConnections = False,
@@ -305,6 +309,42 @@ serverCfg =
withSmpServer :: IO () -> IO ()
withSmpServer = serverBracket (`runSMPServerBlocking` serverCfg)
+xftpTestPort :: ServiceName
+xftpTestPort = "7002"
+
+xftpServerFiles :: FilePath
+xftpServerFiles = "tests/tmp/xftp-server-files"
+
+xftpServerConfig :: XFTPServerConfig
+xftpServerConfig =
+ XFTPServerConfig
+ { xftpPort = xftpTestPort,
+ fileIdSize = 16,
+ storeLogFile = Nothing,
+ filesPath = xftpServerFiles,
+ fileSizeQuota = Nothing,
+ allowedChunkSizes = [kb 128, kb 256, mb 1, mb 4],
+ allowNewFiles = True,
+ newFileBasicAuth = Nothing,
+ fileExpiration = Just defaultFileExpiration,
+ caCertificateFile = "tests/fixtures/tls/ca.crt",
+ privateKeyFile = "tests/fixtures/tls/server.key",
+ certificateFile = "tests/fixtures/tls/server.crt",
+ logStatsInterval = Nothing,
+ logStatsStartTime = 0,
+ serverStatsLogFile = "tests/tmp/xftp-server-stats.daily.log",
+ serverStatsBackupFile = Nothing,
+ logTLSErrors = True
+ }
+
+withXFTPServer :: IO () -> IO ()
+withXFTPServer =
+ serverBracket
+ ( \started -> do
+ createDirectoryIfMissing False xftpServerFiles
+ runXFTPServerBlocking started xftpServerConfig
+ )
+
serverBracket :: (TMVar Bool -> IO ()) -> IO () -> IO ()
serverBracket server f = do
started <- newEmptyTMVarIO
diff --git a/tests/ChatTests/Files.hs b/tests/ChatTests/Files.hs
index 16335d628..396cd4839 100644
--- a/tests/ChatTests/Files.hs
+++ b/tests/ChatTests/Files.hs
@@ -920,22 +920,23 @@ testAsyncGroupFileTransfer tmp = do
testXFTPFileTransfer :: HasCallStack => FilePath -> IO ()
testXFTPFileTransfer =
testChatCfg2 cfg aliceProfile bobProfile $ \alice bob -> do
- connectUsers alice bob
+ withXFTPServer $ do
+ connectUsers alice bob
- alice #> "/f @bob ./tests/fixtures/test.pdf"
- alice <## "use /fc 1 to cancel sending"
- bob <# "alice> sends file test.pdf (266.0 KiB / 272376 bytes)"
- bob <## "use /fr 1 [/ | ] to receive it"
- bob ##> "/fr 1 ./tests/tmp"
- bob <## "saving file 1 from alice to ./tests/tmp/test.pdf"
- -- alice <## "started sending file 1 (test.pdf) to bob" -- TODO "started uploading" ?
- alice <## "completed sending file 1 (test.pdf) to bob"
- bob <## "started receiving file 1 (test.pdf) from alice"
- bob <## "completed receiving file 1 (test.pdf) from alice"
+ alice #> "/f @bob ./tests/fixtures/test.pdf"
+ alice <## "use /fc 1 to cancel sending"
+ bob <# "alice> sends file test.pdf (266.0 KiB / 272376 bytes)"
+ bob <## "use /fr 1 [/ | ] to receive it"
+ bob ##> "/fr 1 ./tests/tmp"
+ bob <## "saving file 1 from alice to ./tests/tmp/test.pdf"
+ -- alice <## "started sending file 1 (test.pdf) to bob" -- TODO "started uploading" ?
+ alice <## "completed sending file 1 (test.pdf) to bob"
+ bob <## "started receiving file 1 (test.pdf) from alice"
+ bob <## "completed receiving file 1 (test.pdf) from alice"
- src <- B.readFile "./tests/fixtures/test.pdf"
- dest <- B.readFile "./tests/tmp/test.pdf"
- dest `shouldBe` src
+ src <- B.readFile "./tests/fixtures/test.pdf"
+ dest <- B.readFile "./tests/tmp/test.pdf"
+ dest `shouldBe` src
where
cfg = testCfg {xftpFileConfig = Just $ XFTPFileConfig {minFileSize = 0}, tempDir = Just "./tests/tmp"}
From f379fd0f8c7fd21f24cf533fcdfbaf2b0ccccbdb Mon Sep 17 00:00:00 2001
From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
Date: Thu, 16 Mar 2023 13:58:01 +0000
Subject: [PATCH 08/17] xftp: sending file completion status (#2016)
* xftp: sending file completion status
* fix type
* fix type 2
* fix
---
src/Simplex/Chat.hs | 16 +++++++++-------
1 file changed, 9 insertions(+), 7 deletions(-)
diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs
index 51b0c5278..e8c0790bc 100644
--- a/src/Simplex/Chat.hs
+++ b/src/Simplex/Chat.hs
@@ -2198,13 +2198,17 @@ processAgentMsgSndFile _corrId aFileId msg =
(Just sharedMsgId, Nothing) -> do
(ft, sfts) <- withStore $ \db -> getSndFileTransfer db user fileId
when (length rfds < length sfts) $ throwChatError $ CEInternalError "not enough XFTP file descriptions to send"
+ -- TODO either update database status or move to SFPROG
toView $ CRSndFileProgressXFTP user ci ft 1 1
case (rfds, sfts, d, cInfo) of
- (rfd : _, sft : _, SMDSnd, DirectChat ct) ->
- sendFileDescription sft rfd sharedMsgId $ sendDirectContactMessage ct
+ (rfd : _, sft : _, SMDSnd, DirectChat ct) -> do
+ msgDeliveryId <- sendFileDescription sft rfd sharedMsgId $ sendDirectContactMessage ct
+ withStore' $ \db -> updateSndFTDeliveryXFTP db sft msgDeliveryId
(_, _, SMDSnd, GroupChat g@GroupInfo {groupId}) -> do
ms <- withStore' $ \db -> getGroupMembers db user g
forM_ (zip rfds $ memberFTs ms) $ \mt -> sendToMember mt `catchError` (toView . CRChatError (Just user))
+ -- TODO update database status and send event to view CRSndFileCompleteXFTP
+ pure ()
where
memberFTs :: [GroupMember] -> [(Connection, SndFileTransfer)]
memberFTs ms = M.elems $ M.intersectionWith (,) (M.fromList mConns') (M.fromList sfts')
@@ -2217,18 +2221,16 @@ processAgentMsgSndFile _corrId aFileId msg =
useMember _ = Nothing
sendToMember :: (ValidFileDescription 'FRecipient, (Connection, SndFileTransfer)) -> m ()
sendToMember (rfd, (conn, sft)) =
- sendFileDescription sft rfd sharedMsgId $ \msg' -> sendDirectMessage conn msg' $ GroupId groupId
+ void $ sendFileDescription sft rfd sharedMsgId $ \msg' -> sendDirectMessage conn msg' $ GroupId groupId
_ -> pure ()
_ -> pure () -- TODO error?
where
- sendFileDescription :: SndFileTransfer -> ValidFileDescription 'FRecipient -> SharedMsgId -> (ChatMsgEvent 'Json -> m (SndMessage, Int64)) -> m ()
+ sendFileDescription :: SndFileTransfer -> ValidFileDescription 'FRecipient -> SharedMsgId -> (ChatMsgEvent 'Json -> m (SndMessage, Int64)) -> m Int64
sendFileDescription sft rfd msgId sendMsg = do
let rfdText = safeDecodeUtf8 $ strEncode rfd
withStore' $ \db -> updateSndFTDescrXFTP db user sft rfdText
partSize <- asks $ xftpDescrPartSize . config
- msgDeliveryId <- sendParts 1 partSize rfdText
- -- msgDeliveryId <- sendFileDescription_ rfd sharedMsgId sendMsg
- withStore' $ \db -> updateSndFTDeliveryXFTP db sft msgDeliveryId
+ sendParts 1 partSize rfdText
where
sendParts partNo partSize rfdText = do
let (part, rest) = T.splitAt partSize rfdText
From 60d6a47bdba5a024ffc6621c1f2a1e2da9c7736f Mon Sep 17 00:00:00 2001
From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
Date: Tue, 21 Mar 2023 15:21:14 +0400
Subject: [PATCH 09/17] xftp: delete agent rcv files on completion, error, item
delete (#2040)
---
cabal.project | 2 +-
scripts/nix/sha256map.nix | 2 +-
simplex-chat.cabal | 1 +
src/Simplex/Chat.hs | 69 ++++++++++++------
.../M20230321_agent_file_deleted.hs | 20 ++++++
src/Simplex/Chat/Migrations/chat_schema.sql | 6 +-
src/Simplex/Chat/Store.hs | 71 +++++++++++--------
src/Simplex/Chat/Types.hs | 28 ++++++--
stack.yaml | 2 +-
tests/Test.hs | 2 +-
10 files changed, 140 insertions(+), 63 deletions(-)
create mode 100644 src/Simplex/Chat/Migrations/M20230321_agent_file_deleted.hs
diff --git a/cabal.project b/cabal.project
index 354d04277..b3943d990 100644
--- a/cabal.project
+++ b/cabal.project
@@ -7,7 +7,7 @@ constraints: zip +disable-bzip2 +disable-zstd
source-repository-package
type: git
location: https://github.com/simplex-chat/simplexmq.git
- tag: 40da7e76ddd5694da386720f61a69d5a15812a81
+ tag: 7e265e5c59dedaabd0b77ce55d49e0bc6fa92f20
source-repository-package
type: git
diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix
index 974347c1f..1bf57f9ba 100644
--- a/scripts/nix/sha256map.nix
+++ b/scripts/nix/sha256map.nix
@@ -1,5 +1,5 @@
{
- "https://github.com/simplex-chat/simplexmq.git"."40da7e76ddd5694da386720f61a69d5a15812a81" = "16lv8h18v96r71wil6d9lac93y1rchrzmqfxqbxya4jgmyl8m9bc";
+ "https://github.com/simplex-chat/simplexmq.git"."7e265e5c59dedaabd0b77ce55d49e0bc6fa92f20" = "162j0187kzwihg0pa91mwqavk93jdx5y5davl7fik8q6svvwqrpq";
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
"https://github.com/kazu-yamamoto/http2.git"."78e18f52295a7f89e828539a03fbcb24931461a3" = "05q165anvv0qrcxqbvq1dlvw0l8gmsa9kl6sazk1mfhz2g0yimdk";
"https://github.com/simplex-chat/direct-sqlcipher.git"."34309410eb2069b029b8fc1872deb1e0db123294" = "0kwkmhyfsn2lixdlgl15smgr1h5gjk7fky6abzh8rng2h5ymnffd";
diff --git a/simplex-chat.cabal b/simplex-chat.cabal
index f25ff03e6..782dfc74f 100644
--- a/simplex-chat.cabal
+++ b/simplex-chat.cabal
@@ -86,6 +86,7 @@ library
Simplex.Chat.Migrations.M20230206_item_deleted_by_group_member_id
Simplex.Chat.Migrations.M20230303_group_link_role
Simplex.Chat.Migrations.M20230304_file_description
+ Simplex.Chat.Migrations.M20230321_agent_file_deleted
Simplex.Chat.Mobile
Simplex.Chat.Mobile.WebRTC
Simplex.Chat.Options
diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs
index d3044b267..7464c1f3a 100644
--- a/src/Simplex/Chat.hs
+++ b/src/Simplex/Chat.hs
@@ -1761,21 +1761,21 @@ toFSFilePath f =
maybe f (<> "/" <> f) <$> (readTVarIO =<< asks filesFolder)
acceptFileReceive :: forall m. ChatMonad m => User -> RcvFileTransfer -> Maybe Bool -> Maybe FilePath -> m AChatItem
-acceptFileReceive user@User {userId} RcvFileTransfer {fileId, rcvFileDescription, fileInvitation = FileInvitation {fileName = fName, fileConnReq, fileInline, fileSize}, fileStatus, grpMemberId} rcvInline_ filePath_ = do
+acceptFileReceive user@User {userId} RcvFileTransfer {fileId, xftpRcvFile, fileInvitation = FileInvitation {fileName = fName, fileConnReq, fileInline, fileSize}, fileStatus, grpMemberId} rcvInline_ filePath_ = do
unless (fileStatus == RFSNew) $ case fileStatus of
RFSCancelled _ -> throwChatError $ CEFileCancelled fName
_ -> throwChatError $ CEFileAlreadyReceiving fName
- case (rcvFileDescription, fileConnReq) of
+ case (xftpRcvFile, fileConnReq) of
-- direct file protocol
(Nothing, Just connReq) -> do
connIds <- joinAgentConnectionAsync user True connReq . directMessage $ XFileAcpt fName
filePath <- getRcvFilePath fileId filePath_ fName True
withStore $ \db -> acceptRcvFileTransfer db user fileId connIds ConnJoined filePath
-- XFTP
- (Just rfd, _) -> do
+ (Just XFTPRcvFile {rcvFileDescription}, _) -> do
filePath <- getRcvFilePath fileId filePath_ fName False
ci <- withStore $ \db -> xftpAcceptRcvFT db user fileId filePath
- receiveViaCompleteFD user fileId filePath rfd
+ receiveViaCompleteFD user fileId rcvFileDescription
pure ci
-- group & direct file protocol
_ -> do
@@ -1818,12 +1818,12 @@ acceptFileReceive user@User {userId} RcvFileTransfer {fileId, rcvFileDescription
|| (rcvInline_ == Just True && fileSize <= fileChunkSize * offerChunks)
)
-receiveViaCompleteFD :: ChatMonad m => User -> FileTransferId -> FilePath -> RcvFileDescr -> m ()
-receiveViaCompleteFD user fileId filePath RcvFileDescr {fileDescrText, fileDescrComplete} =
+receiveViaCompleteFD :: ChatMonad m => User -> FileTransferId -> RcvFileDescr -> m ()
+receiveViaCompleteFD user fileId RcvFileDescr {fileDescrText, fileDescrComplete} =
when fileDescrComplete $ do
rd <- parseRcvFileDescription fileDescrText
tmp <- readTVarIO =<< asks tempDirectory
- aFileId <- withAgent $ \a -> xftpReceiveFile a (aUserId user) rd tmp filePath
+ aFileId <- withAgent $ \a -> xftpReceiveFile a (aUserId user) rd tmp
startReceivingFile user fileId
withStore' $ \db -> updateRcvFileAgentId db fileId (AgentRcvFileId aFileId)
@@ -2185,7 +2185,7 @@ processAgentMsgSndFile _corrId aFileId msg =
where
process :: User -> m ()
process user = do
- fileId <- withStore $ \db -> getAgentSndFileIdXFTP db user $ AgentSndFileId aFileId
+ fileId <- withStore $ \db -> getXFTPSndFileDBId db user $ AgentSndFileId aFileId
case msg of
SFPROG _sent _total -> do
-- update chat item status
@@ -2249,23 +2249,29 @@ processAgentMsgRcvFile _corrId aFileId msg =
where
process :: User -> m ()
process user = do
- fileId <- withStore (`getAgentRcvFileIdXFTP` AgentRcvFileId aFileId)
+ fileId <- withStore (`getXFTPRcvFileDBId` AgentRcvFileId aFileId)
case msg of
RFPROG _sent _total -> do
-- update chat item status
-- send status to view
pure ()
- RFDONE -> do
- ci <- withStore $ \db -> do
- liftIO $ do
- updateRcvFileStatus db fileId FSComplete
- updateCIFileStatus db user fileId CIFSRcvComplete
- getChatItemByFileId db user fileId
- -- ack to agent
- toView $ CRRcvFileComplete user ci
+ RFDONE xftpPath -> do
+ ft <- withStore $ \db -> getRcvFileTransfer db user fileId
+ case liveRcvFileTransferPath ft of
+ Nothing -> throwChatError $ CEInternalError "no target path for received XFTP file"
+ Just targetPath -> do
+ renameFile xftpPath targetPath
+ ci <- withStore $ \db -> do
+ liftIO $ do
+ updateRcvFileStatus db fileId FSComplete
+ updateCIFileStatus db user fileId CIFSRcvComplete
+ getChatItemByFileId db user fileId
+ agentXFTPDeleteRcvFile user aFileId fileId
+ toView $ CRRcvFileComplete user ci
RFERR _e -> do
-- update chat item status
-- send status to view
+ agentXFTPDeleteRcvFile user aFileId fileId
pure ()
processAgentMessageConn :: forall m. ChatMonad m => User -> ACorrId -> ConnId -> ACommand 'Agent 'AEConn -> m ()
@@ -2936,7 +2942,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
ft <- getRcvFileTransfer db user fileId
pure (rfd, ft)
case fileStatus of
- RFSAccepted RcvFileInfo {filePath} -> receiveViaCompleteFD user fileId filePath rfd
+ RFSAccepted _ -> receiveViaCompleteFD user fileId rfd
_ -> pure ()
cancelMessageFile :: Contact -> SharedMsgId -> MsgMeta -> m ()
@@ -3719,7 +3725,7 @@ isFileActive fileId files = do
isJust . M.lookup fileId <$> readTVarIO fs
cancelRcvFileTransfer :: ChatMonad m => User -> RcvFileTransfer -> m (Maybe ConnId)
-cancelRcvFileTransfer user ft@RcvFileTransfer {fileId, rcvFileInline} =
+cancelRcvFileTransfer user ft@RcvFileTransfer {fileId, xftpRcvFile, rcvFileInline} =
cancel' `catchError` (\e -> toView (CRChatError (Just user) e) $> fileConnId)
where
cancel' = do
@@ -3728,14 +3734,26 @@ cancelRcvFileTransfer user ft@RcvFileTransfer {fileId, rcvFileInline} =
updateFileCancelled db user fileId CIFSRcvCancelled
updateRcvFileStatus db fileId FSCancelled
deleteRcvFileChunks db ft
+ case xftpRcvFile of
+ Just XFTPRcvFile {agentRcvFileId = Just (AgentRcvFileId aFileId), agentRcvFileDeleted} ->
+ unless agentRcvFileDeleted $ agentXFTPDeleteRcvFile user aFileId fileId
+ _ -> pure ()
pure fileConnId
- fileConnId = if isNothing rcvFileInline then liveRcvFileTransferConnId ft else Nothing
+ fileConnId = if isNothing xftpRcvFile && isNothing rcvFileInline then liveRcvFileTransferConnId ft else Nothing
cancelSndFile :: ChatMonad m => User -> FileTransferMeta -> [SndFileTransfer] -> Bool -> m [ConnId]
-cancelSndFile user FileTransferMeta {fileId} fts sendCancel = do
+cancelSndFile user FileTransferMeta {fileId, xftpSndFile} fts sendCancel = do
withStore' (\db -> updateFileCancelled db user fileId CIFSSndCancelled)
`catchError` (toView . CRChatError (Just user))
- catMaybes <$> forM fts (\ft -> cancelSndFileTransfer user ft sendCancel)
+ case xftpSndFile of
+ Nothing ->
+ catMaybes <$> forM fts (\ft -> cancelSndFileTransfer user ft sendCancel)
+ Just _patternAgentSndFileId -> do
+ forM_ fts (\ft -> cancelSndFileTransfer user ft False)
+ -- TODO unless agentSndFileDeleted, do agentXFTPDeleteSndFile:
+ -- TODO - with agent xftpDeleteSndFile
+ -- TODO - with store setSndFTAgentDeleted
+ pure []
cancelSndFileTransfer :: ChatMonad m => User -> SndFileTransfer -> Bool -> m (Maybe ConnId)
cancelSndFileTransfer user@User {userId} ft@SndFileTransfer {fileId, connId, agentConnId = AgentConnId acId, fileStatus, fileInline} sendCancel =
@@ -3753,7 +3771,7 @@ cancelSndFileTransfer user@User {userId} ft@SndFileTransfer {fileId, connId, age
void . sendDirectMessage conn (BFileChunk sharedMsgId FileChunkCancel) $ ConnectionId connId
_ -> withAgent $ \a -> void . sendMessage a acId SMP.noMsgFlags $ smpEncode FileChunkCancel
pure fileConnId
- fileConnId = if isJust fileInline then Nothing else Just acId
+ fileConnId = if isNothing fileInline then Just acId else Nothing
closeFileHandle :: ChatMonad m => Int64 -> (ChatController -> TVar (Map Int64 Handle)) -> m ()
closeFileHandle fileId files = do
@@ -3961,6 +3979,11 @@ deleteAgentConnectionsAsync _ [] = pure ()
deleteAgentConnectionsAsync user acIds =
withAgent (`deleteConnectionsAsync` acIds) `catchError` (toView . CRChatError (Just user))
+agentXFTPDeleteRcvFile :: ChatMonad m => User -> RcvFileId -> FileTransferId -> m ()
+agentXFTPDeleteRcvFile user aFileId fileId = do
+ withAgent $ \a -> xftpDeleteRcvFile a (aUserId user) aFileId
+ withStore' $ \db -> setRcvFTAgentDeleted db fileId
+
userProfileToSend :: User -> Maybe Profile -> Maybe Contact -> Profile
userProfileToSend user@User {profile = p} incognitoProfile ct =
let p' = fromMaybe (fromLocalProfile p) incognitoProfile
diff --git a/src/Simplex/Chat/Migrations/M20230321_agent_file_deleted.hs b/src/Simplex/Chat/Migrations/M20230321_agent_file_deleted.hs
new file mode 100644
index 000000000..15a08febf
--- /dev/null
+++ b/src/Simplex/Chat/Migrations/M20230321_agent_file_deleted.hs
@@ -0,0 +1,20 @@
+{-# LANGUAGE QuasiQuotes #-}
+
+module Simplex.Chat.Migrations.M20230321_agent_file_deleted where
+
+import Database.SQLite.Simple (Query)
+import Database.SQLite.Simple.QQ (sql)
+
+m20230321_agent_file_deleted :: Query
+m20230321_agent_file_deleted =
+ [sql|
+PRAGMA ignore_check_constraints=ON;
+
+ALTER TABLE files ADD COLUMN agent_snd_file_deleted INTEGER DEFAULT 0 CHECK (agent_snd_file_deleted NOT NULL);
+UPDATE files SET agent_snd_file_deleted = 0;
+
+ALTER TABLE rcv_files ADD COLUMN agent_rcv_file_deleted INTEGER DEFAULT 0 CHECK (agent_rcv_file_deleted NOT NULL);
+UPDATE rcv_files SET agent_rcv_file_deleted = 0;
+
+PRAGMA ignore_check_constraints=OFF;
+|]
diff --git a/src/Simplex/Chat/Migrations/chat_schema.sql b/src/Simplex/Chat/Migrations/chat_schema.sql
index 1e36360b4..68ebe73e2 100644
--- a/src/Simplex/Chat/Migrations/chat_schema.sql
+++ b/src/Simplex/Chat/Migrations/chat_schema.sql
@@ -195,7 +195,8 @@ CREATE TABLE files(
ci_file_status TEXT,
file_inline TEXT,
agent_snd_file_id BLOB NULL,
- private_snd_file_descr TEXT NULL
+ private_snd_file_descr TEXT NULL,
+ agent_snd_file_deleted INTEGER DEFAULT 0 CHECK(agent_snd_file_deleted NOT NULL)
);
CREATE TABLE snd_files(
file_id INTEGER NOT NULL REFERENCES files ON DELETE CASCADE,
@@ -222,7 +223,8 @@ CREATE TABLE rcv_files(
file_inline TEXT,
file_descr_id INTEGER NULL
REFERENCES xftp_file_descriptions ON DELETE SET NULL,
- agent_rcv_file_id BLOB NULL
+ agent_rcv_file_id BLOB NULL,
+ agent_rcv_file_deleted INTEGER DEFAULT 0 CHECK(agent_rcv_file_deleted NOT NULL)
);
CREATE TABLE snd_file_chunks(
file_id INTEGER NOT NULL,
diff --git a/src/Simplex/Chat/Store.hs b/src/Simplex/Chat/Store.hs
index 492dc0ad1..be444e545 100644
--- a/src/Simplex/Chat/Store.hs
+++ b/src/Simplex/Chat/Store.hs
@@ -159,8 +159,8 @@ module Simplex.Chat.Store
createSndFTDescrXFTP,
updateSndFTDescrXFTP,
updateSndFTDeliveryXFTP,
- getAgentSndFileIdXFTP,
- getAgentRcvFileIdXFTP,
+ getXFTPSndFileDBId,
+ getXFTPRcvFileDBId,
updateFileCancelled,
updateCIFileStatus,
getSharedMsgIdByFileId,
@@ -184,6 +184,7 @@ module Simplex.Chat.Store
acceptRcvInlineFT,
startRcvInlineFT,
xftpAcceptRcvFT,
+ setRcvFTAgentDeleted,
updateRcvFileStatus,
createRcvFileChunk,
updatedRcvFileChunkStored,
@@ -357,6 +358,7 @@ import Simplex.Chat.Migrations.M20230129_drop_chat_items_group_idx
import Simplex.Chat.Migrations.M20230206_item_deleted_by_group_member_id
import Simplex.Chat.Migrations.M20230303_group_link_role
import Simplex.Chat.Migrations.M20230304_file_description
+import Simplex.Chat.Migrations.M20230321_agent_file_deleted
import Simplex.Chat.Protocol
import Simplex.Chat.Types
import Simplex.Chat.Util (week)
@@ -424,7 +426,8 @@ schemaMigrations =
("20230129_drop_chat_items_group_idx", m20230129_drop_chat_items_group_idx),
("20230206_item_deleted_by_group_member_id", m20230206_item_deleted_by_group_member_id),
("20230303_group_link_role", m20230303_group_link_role),
- ("20230304_file_description", m20230304_file_description)
+ ("20230304_file_description", m20230304_file_description),
+ ("20230321_agent_file_deleted", m20230321_agent_file_deleted)
]
-- | The list of migrations in ascending order by date
@@ -2801,13 +2804,13 @@ updateSndFTDeliveryXFTP db SndFileTransfer {connId, fileId, fileDescrId} msgDeli
"UPDATE snd_files SET last_inline_msg_delivery_id = ? WHERE connection_id = ? AND file_id = ? AND file_descr_id = ?"
(msgDeliveryId, connId, fileId, fileDescrId)
-getAgentSndFileIdXFTP :: DB.Connection -> User -> AgentSndFileId -> ExceptT StoreError IO Int64
-getAgentSndFileIdXFTP db User {userId} aSndFileId =
+getXFTPSndFileDBId :: DB.Connection -> User -> AgentSndFileId -> ExceptT StoreError IO FileTransferId
+getXFTPSndFileDBId db User {userId} aSndFileId =
ExceptT . firstRow fromOnly (SESndFileNotFoundXFTP aSndFileId) $
DB.query db "SELECT file_id FROM files WHERE user_id = ? AND agent_snd_file_id = ?" (userId, aSndFileId)
-getAgentRcvFileIdXFTP :: DB.Connection -> AgentRcvFileId -> ExceptT StoreError IO FileTransferId
-getAgentRcvFileIdXFTP db aRcvFileId =
+getXFTPRcvFileDBId :: DB.Connection -> AgentRcvFileId -> ExceptT StoreError IO FileTransferId
+getXFTPRcvFileDBId db aRcvFileId =
ExceptT . firstRow fromOnly (SERcvFileNotFoundXFTP aRcvFileId) $
DB.query db "SELECT file_id FROM rcv_files WHERE agent_rcv_file_id = ?" (Only aRcvFileId)
@@ -2956,14 +2959,15 @@ createRcvFileTransfer db userId Contact {contactId, localDisplayName = c} f@File
"INSERT INTO files (user_id, contact_id, file_name, file_size, chunk_size, file_inline, ci_file_status, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)"
(userId, contactId, fileName, fileSize, chunkSize, fileInline, CIFSRcvInvitation, currentTs, currentTs)
insertedRowId db
- rfd <- mapM (createRcvFD_ db userId currentTs) fileDescr
- let rfdId = (fileDescrId :: RcvFileDescr -> Int64) <$> rfd
+ rfd_ <- mapM (createRcvFD_ db userId currentTs) fileDescr
+ let rfdId = (fileDescrId :: RcvFileDescr -> Int64) <$> rfd_
+ xftpRcvFile = (\rfd -> XFTPRcvFile {rcvFileDescription = rfd, agentRcvFileId = Nothing, agentRcvFileDeleted = False}) <$> rfd_
liftIO $
DB.execute
db
"INSERT INTO rcv_files (file_id, file_status, file_queue_info, file_inline, rcv_file_inline, file_descr_id, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)"
(fileId, FSNew, fileConnReq, fileInline, rcvFileInline, rfdId, currentTs, currentTs)
- pure RcvFileTransfer {fileId, fileInvitation = f, fileStatus = RFSNew, rcvFileInline, rcvFileDescription = rfd, senderDisplayName = c, chunkSize, cancelled = False, grpMemberId = Nothing}
+ pure RcvFileTransfer {fileId, xftpRcvFile, fileInvitation = f, fileStatus = RFSNew, rcvFileInline, senderDisplayName = c, chunkSize, cancelled = False, grpMemberId = Nothing}
createRcvGroupFileTransfer :: DB.Connection -> UserId -> GroupMember -> FileInvitation -> Maybe InlineFileMode -> Integer -> ExceptT StoreError IO RcvFileTransfer
createRcvGroupFileTransfer db userId GroupMember {groupId, groupMemberId, localDisplayName = c} f@FileInvitation {fileName, fileSize, fileConnReq, fileInline, fileDescr} rcvFileInline chunkSize = do
@@ -2974,14 +2978,15 @@ createRcvGroupFileTransfer db userId GroupMember {groupId, groupMemberId, localD
"INSERT INTO files (user_id, group_id, file_name, file_size, chunk_size, file_inline, ci_file_status, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)"
(userId, groupId, fileName, fileSize, chunkSize, fileInline, CIFSRcvInvitation, currentTs, currentTs)
insertedRowId db
- rfd <- mapM (createRcvFD_ db userId currentTs) fileDescr
- let rfdId = (fileDescrId :: RcvFileDescr -> Int64) <$> rfd
+ rfd_ <- mapM (createRcvFD_ db userId currentTs) fileDescr
+ let rfdId = (fileDescrId :: RcvFileDescr -> Int64) <$> rfd_
+ xftpRcvFile = (\rfd -> XFTPRcvFile {rcvFileDescription = rfd, agentRcvFileId = Nothing, agentRcvFileDeleted = False}) <$> rfd_
liftIO $
DB.execute
db
"INSERT INTO rcv_files (file_id, file_status, file_queue_info, file_inline, rcv_file_inline, group_member_id, file_descr_id, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)"
(fileId, FSNew, fileConnReq, fileInline, rcvFileInline, groupMemberId, rfdId, currentTs, currentTs)
- pure RcvFileTransfer {fileId, fileInvitation = f, fileStatus = RFSNew, rcvFileInline, rcvFileDescription = rfd, senderDisplayName = c, chunkSize, cancelled = False, grpMemberId = Just groupMemberId}
+ pure RcvFileTransfer {fileId, xftpRcvFile, fileInvitation = f, fileStatus = RFSNew, rcvFileInline, senderDisplayName = c, chunkSize, cancelled = False, grpMemberId = Just groupMemberId}
createRcvFD_ :: DB.Connection -> UserId -> UTCTime -> FileDescr -> ExceptT StoreError IO RcvFileDescr
createRcvFD_ db userId currentTs FileDescr {fileDescrText, fileDescrPartNo, fileDescrComplete} = do
@@ -3063,7 +3068,7 @@ getRcvFileTransfer db User {userId} fileId = do
[sql|
SELECT r.file_status, r.file_queue_info, r.group_member_id, f.file_name,
f.file_size, f.chunk_size, f.cancelled, cs.local_display_name, m.local_display_name,
- f.file_path, r.file_inline, r.rcv_file_inline, c.connection_id, c.agent_conn_id
+ f.file_path, r.file_inline, r.rcv_file_inline, r.agent_rcv_file_id, r.agent_rcv_file_deleted, c.connection_id, c.agent_conn_id
FROM rcv_files r
JOIN files f USING (file_id)
LEFT JOIN connections c ON r.file_id = c.rcv_file_id
@@ -3072,30 +3077,30 @@ getRcvFileTransfer db User {userId} fileId = do
WHERE f.user_id = ? AND f.file_id = ?
|]
(userId, fileId)
- rfd <- liftIO $ getRcvFileDescrByFileId_ db fileId
- rcvFileTransfer rfd rftRow
+ rfd_ <- liftIO $ getRcvFileDescrByFileId_ db fileId
+ rcvFileTransfer rfd_ rftRow
where
rcvFileTransfer ::
Maybe RcvFileDescr ->
- (FileStatus, Maybe ConnReqInvitation, Maybe Int64, String, Integer, Integer, Maybe Bool) :. (Maybe ContactName, Maybe ContactName, Maybe FilePath, Maybe InlineFileMode, Maybe InlineFileMode) :. (Maybe Int64, Maybe AgentConnId) ->
+ (FileStatus, Maybe ConnReqInvitation, Maybe Int64, String, Integer, Integer, Maybe Bool) :. (Maybe ContactName, Maybe ContactName, Maybe FilePath, Maybe InlineFileMode, Maybe InlineFileMode, Maybe AgentRcvFileId, Bool) :. (Maybe Int64, Maybe AgentConnId) ->
ExceptT StoreError IO RcvFileTransfer
- rcvFileTransfer rcvFileDescription ((fileStatus', fileConnReq, grpMemberId, fileName, fileSize, chunkSize, cancelled_) :. (contactName_, memberName_, filePath_, fileInline, rcvFileInline) :. (connId_, agentConnId_)) = do
- let fileInv = FileInvitation {fileName, fileSize, fileDigest = Nothing, fileConnReq, fileInline, fileDescr = Nothing}
- fileInfo = (filePath_, connId_, agentConnId_)
+ rcvFileTransfer rfd_ ((fileStatus', fileConnReq, grpMemberId, fileName, fileSize, chunkSize, cancelled_) :. (contactName_, memberName_, filePath_, fileInline, rcvFileInline, agentRcvFileId, agentRcvFileDeleted) :. (connId_, agentConnId_)) =
case contactName_ <|> memberName_ of
Nothing -> throwError $ SERcvFileInvalid fileId
Just name -> do
case fileStatus' of
- FSNew -> pure $ ft name fileInv RFSNew
- FSAccepted -> ft name fileInv . RFSAccepted <$> rfi fileInfo
- FSConnected -> ft name fileInv . RFSConnected <$> rfi fileInfo
- FSComplete -> ft name fileInv . RFSComplete <$> rfi fileInfo
- FSCancelled -> ft name fileInv . RFSCancelled <$> rfi_ fileInfo
+ FSNew -> pure $ ft name RFSNew
+ FSAccepted -> ft name . RFSAccepted <$> rfi
+ FSConnected -> ft name . RFSConnected <$> rfi
+ FSComplete -> ft name . RFSComplete <$> rfi
+ FSCancelled -> ft name . RFSCancelled <$> rfi_
where
- ft senderDisplayName fileInvitation fileStatus =
- RcvFileTransfer {fileId, fileInvitation, fileStatus, rcvFileInline, rcvFileDescription, senderDisplayName, chunkSize, cancelled, grpMemberId}
- rfi fileInfo = maybe (throwError $ SERcvFileInvalid fileId) pure =<< rfi_ fileInfo
- rfi_ = \case
+ ft senderDisplayName fileStatus =
+ let fileInvitation = FileInvitation {fileName, fileSize, fileDigest = Nothing, fileConnReq, fileInline, fileDescr = Nothing}
+ xftpRcvFile = (\rfd -> XFTPRcvFile {rcvFileDescription = rfd, agentRcvFileId, agentRcvFileDeleted}) <$> rfd_
+ in RcvFileTransfer {fileId, xftpRcvFile, fileInvitation, fileStatus, rcvFileInline, senderDisplayName, chunkSize, cancelled, grpMemberId}
+ rfi = maybe (throwError $ SERcvFileInvalid fileId) pure =<< rfi_
+ rfi_ = case (filePath_, connId_, agentConnId_) of
(Just filePath, connId, agentConnId) -> pure $ Just RcvFileInfo {filePath, connId, agentConnId}
_ -> pure Nothing
cancelled = fromMaybe False cancelled_
@@ -3146,6 +3151,14 @@ acceptRcvFT_ db User {userId} fileId filePath rcvFileInline currentTs = do
"UPDATE rcv_files SET rcv_file_inline = ?, file_status = ?, updated_at = ? WHERE file_id = ?"
(rcvFileInline, FSAccepted, currentTs, fileId)
+setRcvFTAgentDeleted :: DB.Connection -> FileTransferId -> IO ()
+setRcvFTAgentDeleted db fileId = do
+ currentTs <- getCurrentTime
+ DB.execute
+ db
+ "UPDATE rcv_files SET agent_rcv_file_deleted = 1, updated_at = ? WHERE file_id = ?"
+ (currentTs, fileId)
+
updateRcvFileStatus :: DB.Connection -> FileTransferId -> FileStatus -> IO ()
updateRcvFileStatus db fileId status = do
currentTs <- getCurrentTime
diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs
index 5140ca7f7..d0fc6acf1 100644
--- a/src/Simplex/Chat/Types.hs
+++ b/src/Simplex/Chat/Types.hs
@@ -1543,10 +1543,10 @@ instance ToJSON InlineFileMode where
data RcvFileTransfer = RcvFileTransfer
{ fileId :: FileTransferId,
+ xftpRcvFile :: Maybe XFTPRcvFile,
fileInvitation :: FileInvitation,
fileStatus :: RcvFileStatus,
rcvFileInline :: Maybe InlineFileMode,
- rcvFileDescription :: Maybe RcvFileDescr,
senderDisplayName :: ContactName,
chunkSize :: Integer,
cancelled :: Bool,
@@ -1556,6 +1556,15 @@ data RcvFileTransfer = RcvFileTransfer
instance ToJSON RcvFileTransfer where toEncoding = J.genericToEncoding J.defaultOptions
+data XFTPRcvFile = XFTPRcvFile
+ { rcvFileDescription :: RcvFileDescr,
+ agentRcvFileId :: Maybe AgentRcvFileId,
+ agentRcvFileDeleted :: Bool
+ }
+ deriving (Eq, Show, Generic)
+
+instance ToJSON XFTPRcvFile where toEncoding = J.genericToEncoding J.defaultOptions
+
data RcvFileDescr = RcvFileDescr
{ fileDescrId :: Int64,
fileDescrText :: Text,
@@ -1587,15 +1596,23 @@ data RcvFileInfo = RcvFileInfo
instance ToJSON RcvFileInfo where toEncoding = J.genericToEncoding J.defaultOptions
-liveRcvFileTransferConnId :: RcvFileTransfer -> Maybe ConnId
-liveRcvFileTransferConnId RcvFileTransfer {fileStatus} = case fileStatus of
- RFSAccepted fi -> acId fi
- RFSConnected fi -> acId fi
+liveRcvFileTransferInfo :: RcvFileTransfer -> Maybe RcvFileInfo
+liveRcvFileTransferInfo RcvFileTransfer {fileStatus} = case fileStatus of
+ RFSAccepted fi -> Just fi
+ RFSConnected fi -> Just fi
_ -> Nothing
+
+liveRcvFileTransferConnId :: RcvFileTransfer -> Maybe ConnId
+liveRcvFileTransferConnId ft = acId =<< liveRcvFileTransferInfo ft
where
acId RcvFileInfo {agentConnId = Just (AgentConnId cId)} = Just cId
acId _ = Nothing
+liveRcvFileTransferPath :: RcvFileTransfer -> Maybe FilePath
+liveRcvFileTransferPath ft = fp <$> liveRcvFileTransferInfo ft
+ where
+ fp RcvFileInfo {filePath} = filePath
+
newtype AgentConnId = AgentConnId ConnId
deriving (Eq, Show)
@@ -1689,6 +1706,7 @@ instance ToJSON FileTransferMeta where toEncoding = J.genericToEncoding J.defaul
data XFTPSndFile = XFTPSndFile
{ agentSndFileId :: AgentSndFileId,
privateSndFileDescr :: Maybe Text
+ -- TODO agentSndFileDeleted :: Bool
}
deriving (Eq, Show, Generic)
diff --git a/stack.yaml b/stack.yaml
index c4fbfa130..4bca20bc3 100644
--- a/stack.yaml
+++ b/stack.yaml
@@ -49,7 +49,7 @@ extra-deps:
# - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561
# - ../simplexmq
- github: simplex-chat/simplexmq
- commit: 40da7e76ddd5694da386720f61a69d5a15812a81
+ commit: 7e265e5c59dedaabd0b77ce55d49e0bc6fa92f20
- github: kazu-yamamoto/http2
commit: 78e18f52295a7f89e828539a03fbcb24931461a3
# - ../direct-sqlcipher
diff --git a/tests/Test.hs b/tests/Test.hs
index 0cbf14796..4ea3e9ef5 100644
--- a/tests/Test.hs
+++ b/tests/Test.hs
@@ -25,7 +25,7 @@ main = do
testBracket test = do
t <- getSystemTime
let ts = show (systemSeconds t) <> show (systemNanoseconds t)
- withSmpServer $ withTmpFiles $ withTempDirectory "tests" ("tmp" <> ts) test
+ withSmpServer $ withTmpFiles $ withTempDirectory "tests/tmp" ts test
logCfg :: LogConfig
logCfg = LogConfig {lc_file = Nothing, lc_stderr = True}
From 47c6daf0cc1f29da45fa066fa411f34eedb545d4 Mon Sep 17 00:00:00 2001
From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
Date: Wed, 22 Mar 2023 18:48:38 +0400
Subject: [PATCH 10/17] xftp: set app tmp directory (#2054)
---
cabal.project | 2 +-
scripts/nix/sha256map.nix | 2 +-
src/Simplex/Chat.hs | 26 ++++++++----
src/Simplex/Chat/Controller.hs | 1 +
stack.yaml | 2 +-
tests/ChatClient.hs | 2 +-
tests/ChatTests/Files.hs | 74 ++++++++++++++++++++++++++++++++++
7 files changed, 98 insertions(+), 11 deletions(-)
diff --git a/cabal.project b/cabal.project
index b3943d990..332ebdc79 100644
--- a/cabal.project
+++ b/cabal.project
@@ -7,7 +7,7 @@ constraints: zip +disable-bzip2 +disable-zstd
source-repository-package
type: git
location: https://github.com/simplex-chat/simplexmq.git
- tag: 7e265e5c59dedaabd0b77ce55d49e0bc6fa92f20
+ tag: 0df7733125add475f9de88a362bcd526091f187c
source-repository-package
type: git
diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix
index 1bf57f9ba..fab104c40 100644
--- a/scripts/nix/sha256map.nix
+++ b/scripts/nix/sha256map.nix
@@ -1,5 +1,5 @@
{
- "https://github.com/simplex-chat/simplexmq.git"."7e265e5c59dedaabd0b77ce55d49e0bc6fa92f20" = "162j0187kzwihg0pa91mwqavk93jdx5y5davl7fik8q6svvwqrpq";
+ "https://github.com/simplex-chat/simplexmq.git"."0df7733125add475f9de88a362bcd526091f187c" = "09s2dimdq88lm4mb2xcl5vch2qb21llj8ss649vlxpkm69njpyj0";
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
"https://github.com/kazu-yamamoto/http2.git"."78e18f52295a7f89e828539a03fbcb24931461a3" = "05q165anvv0qrcxqbvq1dlvw0l8gmsa9kl6sazk1mfhz2g0yimdk";
"https://github.com/simplex-chat/direct-sqlcipher.git"."34309410eb2069b029b8fc1872deb1e0db123294" = "0kwkmhyfsn2lixdlgl15smgr1h5gjk7fky6abzh8rng2h5ymnffd";
diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs
index 7464c1f3a..d7963c287 100644
--- a/src/Simplex/Chat.hs
+++ b/src/Simplex/Chat.hs
@@ -78,7 +78,7 @@ import qualified Simplex.Messaging.TMap as TM
import Simplex.Messaging.Transport.Client (defaultSocksProxy)
import Simplex.Messaging.Util
import System.Exit (exitFailure, exitSuccess)
-import System.FilePath (combine, splitExtensions, takeFileName)
+import System.FilePath (combine, splitExtensions, takeFileName, (>))
import System.IO (Handle, IOMode (..), SeekMode (..), hFlush, openFile, stdout)
import Text.Read (readMaybe)
import UnliftIO.Async
@@ -219,9 +219,15 @@ startChatController subConns enableExpireCIs = do
then Just <$> async (subscribeUsers users)
else pure Nothing
atomically . writeTVar s $ Just (a1, a2)
+ startXFTP
startCleanupManager
when enableExpireCIs $ startExpireCIs users
pure a1
+ startXFTP = do
+ tmp <- readTVarIO =<< asks tempDirectory
+ runExceptT (withAgent $ \a -> xftpStartWorkers a tmp) >>= \case
+ Left e -> liftIO $ print $ "Error starting XFTP workers: " <> show e
+ Right _ -> pure ()
startCleanupManager = do
cleanupAsync <- asks cleanupManagerAsync
readTVarIO cleanupAsync >>= \case
@@ -355,6 +361,11 @@ processChatCommand = \case
withAgent (`suspendAgent` t)
ok_
ResubscribeAllConnections -> withStore' getUsers >>= subscribeUsers >> ok_
+ -- has to be called before StartChat
+ SetTempFolder tf -> do
+ createDirectoryIfMissing True tf
+ asks tempDirectory >>= atomically . (`writeTVar` Just tf)
+ ok_
SetFilesFolder ff -> do
createDirectoryIfMissing True ff
asks filesFolder >>= atomically . (`writeTVar` Just ff)
@@ -535,8 +546,8 @@ processChatCommand = \case
let fileName = takeFileName file
fileDescr = FileDescr {fileDescrText = "", fileDescrPartNo = 0, fileDescrComplete = False}
fInv = xftpFileInvitation fileName fileSize fileDescr
- tmp <- readTVarIO =<< asks tempDirectory
- aFileId <- withAgent $ \a -> xftpSendFile a (aUserId user) file n tmp
+ fsFilePath <- toFSFilePath file
+ aFileId <- withAgent $ \a -> xftpSendFile a (aUserId user) fsFilePath n
-- TODO CRSndFileStart event for XFTP
ft@FileTransferMeta {fileId} <- withStore' $ \db -> createSndFileTransferXFTP db user contactOrGroup file fInv $ AgentSndFileId aFileId
let ciFile = CIFile {fileId, fileName, fileSize, filePath = Just file, fileStatus = CIFSSndStored}
@@ -1758,7 +1769,7 @@ callStatusItemContent user Contact {contactId} chatItemId receivedStatus = do
-- used during file transfer for actual operations with file system
toFSFilePath :: ChatMonad m => FilePath -> m FilePath
toFSFilePath f =
- maybe f (<> "/" <> f) <$> (readTVarIO =<< asks filesFolder)
+ maybe f (> f) <$> (readTVarIO =<< asks filesFolder)
acceptFileReceive :: forall m. ChatMonad m => User -> RcvFileTransfer -> Maybe Bool -> Maybe FilePath -> m AChatItem
acceptFileReceive user@User {userId} RcvFileTransfer {fileId, xftpRcvFile, fileInvitation = FileInvitation {fileName = fName, fileConnReq, fileInline, fileSize}, fileStatus, grpMemberId} rcvInline_ filePath_ = do
@@ -1822,8 +1833,7 @@ receiveViaCompleteFD :: ChatMonad m => User -> FileTransferId -> RcvFileDescr ->
receiveViaCompleteFD user fileId RcvFileDescr {fileDescrText, fileDescrComplete} =
when fileDescrComplete $ do
rd <- parseRcvFileDescription fileDescrText
- tmp <- readTVarIO =<< asks tempDirectory
- aFileId <- withAgent $ \a -> xftpReceiveFile a (aUserId user) rd tmp
+ aFileId <- withAgent $ \a -> xftpReceiveFile a (aUserId user) rd
startReceivingFile user fileId
withStore' $ \db -> updateRcvFileAgentId db fileId (AgentRcvFileId aFileId)
@@ -2260,7 +2270,8 @@ processAgentMsgRcvFile _corrId aFileId msg =
case liveRcvFileTransferPath ft of
Nothing -> throwChatError $ CEInternalError "no target path for received XFTP file"
Just targetPath -> do
- renameFile xftpPath targetPath
+ fsTargetPath <- toFSFilePath targetPath
+ renameFile xftpPath fsTargetPath
ci <- withStore $ \db -> do
liftIO $ do
updateRcvFileStatus db fileId FSComplete
@@ -4202,6 +4213,7 @@ chatCommandP =
"/_app activate" $> APIActivateChat,
"/_app suspend " *> (APISuspendChat <$> A.decimal),
"/_resubscribe all" $> ResubscribeAllConnections,
+ "/_temp_folder " *> (SetTempFolder <$> filePath),
"/_files_folder " *> (SetFilesFolder <$> filePath),
"/_db export " *> (APIExportArchive <$> jsonP),
"/_db import " *> (APIImportArchive <$> jsonP),
diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs
index 0cb8f8441..a4c77ca88 100644
--- a/src/Simplex/Chat/Controller.hs
+++ b/src/Simplex/Chat/Controller.hs
@@ -196,6 +196,7 @@ data ChatCommand
| APIActivateChat
| APISuspendChat {suspendTimeout :: Int}
| ResubscribeAllConnections
+ | SetTempFolder FilePath
| SetFilesFolder FilePath
| SetIncognito Bool
| APIExportArchive ArchiveConfig
diff --git a/stack.yaml b/stack.yaml
index 4bca20bc3..6e935ec9f 100644
--- a/stack.yaml
+++ b/stack.yaml
@@ -49,7 +49,7 @@ extra-deps:
# - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561
# - ../simplexmq
- github: simplex-chat/simplexmq
- commit: 7e265e5c59dedaabd0b77ce55d49e0bc6fa92f20
+ commit: 0df7733125add475f9de88a362bcd526091f187c
- github: kazu-yamamoto/http2
commit: 78e18f52295a7f89e828539a03fbcb24931461a3
# - ../direct-sqlcipher
diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs
index 3d782603a..c0107c6f7 100644
--- a/tests/ChatClient.hs
+++ b/tests/ChatClient.hs
@@ -320,7 +320,7 @@ xftpServerConfig =
XFTPServerConfig
{ xftpPort = xftpTestPort,
fileIdSize = 16,
- storeLogFile = Nothing,
+ storeLogFile = Just "tests/tmp/xftp-server-store.log",
filesPath = xftpServerFiles,
fileSizeQuota = Nothing,
allowedChunkSizes = [kb 128, kb 256, mb 1, mb 4],
diff --git a/tests/ChatTests/Files.hs b/tests/ChatTests/Files.hs
index 396cd4839..8b776fec2 100644
--- a/tests/ChatTests/Files.hs
+++ b/tests/ChatTests/Files.hs
@@ -50,6 +50,8 @@ chatFileTests = do
xit "send and receive file to group, fully asynchronous" testAsyncGroupFileTransfer
describe "file transfer over XFTP" $ do
it "send and receive file" testXFTPFileTransfer
+ it "with relative paths: send and receive file" testXFTPWithRelativePaths
+ it "continue receiving file after restart" testXFTPContinueRcv
runTestFileTransfer :: HasCallStack => TestCC -> TestCC -> IO ()
runTestFileTransfer alice bob = do
@@ -940,6 +942,78 @@ testXFTPFileTransfer =
where
cfg = testCfg {xftpFileConfig = Just $ XFTPFileConfig {minFileSize = 0}, tempDir = Just "./tests/tmp"}
+testXFTPWithRelativePaths :: HasCallStack => FilePath -> IO ()
+testXFTPWithRelativePaths =
+ testChatCfg2 cfg aliceProfile bobProfile $ \alice bob -> do
+ withXFTPServer $ do
+ -- agent is passed xftp work directory only on chat start,
+ -- so for test we work around by stopping and starting chat
+ alice ##> "/_stop"
+ alice <## "chat stopped"
+ alice #$> ("/_files_folder ./tests/fixtures", id, "ok")
+ alice #$> ("/_temp_folder ./tests/tmp/alice_xftp", id, "ok")
+ alice ##> "/_start"
+ alice <## "chat started"
+
+ bob ##> "/_stop"
+ bob <## "chat stopped"
+ bob #$> ("/_files_folder ./tests/tmp/bob_files", id, "ok")
+ bob #$> ("/_temp_folder ./tests/tmp/bob_xftp", id, "ok")
+ bob ##> "/_start"
+ bob <## "chat started"
+
+ connectUsers alice bob
+
+ alice #> "/f @bob test.pdf"
+ alice <## "use /fc 1 to cancel sending"
+ bob <# "alice> sends file test.pdf (266.0 KiB / 272376 bytes)"
+ bob <## "use /fr 1 [/ | ] to receive it"
+ bob ##> "/fr 1"
+ bob <## "saving file 1 from alice to test.pdf"
+ -- alice <## "started sending file 1 (test.pdf) to bob" -- TODO "started uploading" ?
+ alice <## "completed sending file 1 (test.pdf) to bob"
+ bob <## "started receiving file 1 (test.pdf) from alice"
+ bob <## "completed receiving file 1 (test.pdf) from alice"
+
+ src <- B.readFile "./tests/fixtures/test.pdf"
+ dest <- B.readFile "./tests/tmp/bob_files/test.pdf"
+ dest `shouldBe` src
+ where
+ cfg = testCfg {xftpFileConfig = Just $ XFTPFileConfig {minFileSize = 0}}
+
+testXFTPContinueRcv :: HasCallStack => FilePath -> IO ()
+testXFTPContinueRcv tmp = do
+ withXFTPServer $ do
+ withNewTestChatCfg tmp cfg "alice" aliceProfile $ \alice -> do
+ withNewTestChatCfg tmp cfg "bob" bobProfile $ \bob -> do
+ connectUsers alice bob
+
+ alice #> "/f @bob ./tests/fixtures/test.pdf"
+ alice <## "use /fc 1 to cancel sending"
+ bob <# "alice> sends file test.pdf (266.0 KiB / 272376 bytes)"
+ bob <## "use /fr 1 [/ | ] to receive it"
+ -- alice <## "started sending file 1 (test.pdf) to bob" -- TODO "started uploading" ?
+ alice <## "completed sending file 1 (test.pdf) to bob"
+
+ -- server is down - file is not received
+ withTestChatCfg tmp cfg "bob" $ \bob -> do
+ bob <## "1 contacts connected (use /cs for the list)"
+ bob ##> "/fr 1 ./tests/tmp"
+ bob <## "started receiving file 1 (test.pdf) from alice"
+ bob <## "saving file 1 from alice to ./tests/tmp/test.pdf"
+ (bob )
+
+ withXFTPServer $ do
+ -- server is up - file reception is continued
+ withTestChatCfg tmp cfg "bob" $ \bob -> do
+ bob <## "1 contacts connected (use /cs for the list)"
+ bob <## "completed receiving file 1 (test.pdf) from alice"
+ src <- B.readFile "./tests/fixtures/test.pdf"
+ dest <- B.readFile "./tests/tmp/test.pdf"
+ dest `shouldBe` src
+ where
+ cfg = testCfg {xftpFileConfig = Just $ XFTPFileConfig {minFileSize = 0}, tempDir = Just "./tests/tmp"}
+
startFileTransfer :: HasCallStack => TestCC -> TestCC -> IO ()
startFileTransfer alice bob =
startFileTransfer' alice bob "test.jpg" "136.5 KiB / 139737 bytes"
From 2a9c138a23b3eb0adcf6a235816cdda8e689c293 Mon Sep 17 00:00:00 2001
From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
Date: Wed, 22 Mar 2023 22:20:12 +0400
Subject: [PATCH 11/17] xftp: set xftp config (#2059)
---
src/Simplex/Chat.hs | 18 +++++++++++++++++-
src/Simplex/Chat/Controller.hs | 1 +
tests/ChatTests/Files.hs | 30 ++++++++++++++++++++++++++++++
3 files changed, 48 insertions(+), 1 deletion(-)
diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs
index d7963c287..d58b0a4bd 100644
--- a/src/Simplex/Chat.hs
+++ b/src/Simplex/Chat.hs
@@ -59,7 +59,7 @@ import Simplex.Chat.Store
import Simplex.Chat.Types
import Simplex.Chat.Util (diffInMicros, diffInSeconds)
import Simplex.FileTransfer.Client.Presets (defaultXFTPServers)
-import Simplex.FileTransfer.Description (ValidFileDescription)
+import Simplex.FileTransfer.Description (ValidFileDescription, gb, kb, mb)
import Simplex.FileTransfer.Protocol (FileParty (..))
import Simplex.Messaging.Agent as Agent
import Simplex.Messaging.Agent.Client (AgentStatsKey (..))
@@ -370,6 +370,9 @@ processChatCommand = \case
createDirectoryIfMissing True ff
asks filesFolder >>= atomically . (`writeTVar` Just ff)
ok_
+ APISetXFTPConfig cfg -> do
+ asks userXFTPFileConfig >>= atomically . (`writeTVar` cfg)
+ ok_
SetIncognito onOff -> do
asks incognitoMode >>= atomically . (`writeTVar` onOff)
ok_
@@ -4215,6 +4218,8 @@ chatCommandP =
"/_resubscribe all" $> ResubscribeAllConnections,
"/_temp_folder " *> (SetTempFolder <$> filePath),
"/_files_folder " *> (SetFilesFolder <$> filePath),
+ "/_xftp " *> (APISetXFTPConfig <$> ("on " *> (Just <$> jsonP) <|> ("off" $> Nothing))),
+ "/xftp " *> (APISetXFTPConfig <$> ("on " *> (Just <$> xftpCfgP) <|> ("off" $> Nothing))),
"/_db export " *> (APIExportArchive <$> jsonP),
"/_db import " *> (APIImportArchive <$> jsonP),
"/_db delete" $> APIDeleteStorage,
@@ -4474,6 +4479,17 @@ chatCommandP =
logErrors <- " log=" *> onOffP <|> pure False
let tcpTimeout = 1000000 * fromMaybe (maybe 5 (const 10) socksProxy) t_
pure $ fullNetworkConfig socksProxy tcpTimeout logErrors
+ xftpCfgP = do
+ minFileSize <- "minFileSize=" *> fileSizeP
+ pure $ XFTPFileConfig {minFileSize}
+ -- TODO move to Utils in simplexmq
+ fileSizeP =
+ A.choice
+ [ gb <$> A.decimal <* "gb",
+ mb <$> A.decimal <* "mb",
+ kb <$> A.decimal <* "kb",
+ A.decimal
+ ]
dbKeyP = nonEmptyKey <$?> strP
nonEmptyKey k@(DBEncryptionKey s) = if null s then Left "empty key" else Right k
autoAcceptP =
diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs
index a4c77ca88..c1192dfe5 100644
--- a/src/Simplex/Chat/Controller.hs
+++ b/src/Simplex/Chat/Controller.hs
@@ -198,6 +198,7 @@ data ChatCommand
| ResubscribeAllConnections
| SetTempFolder FilePath
| SetFilesFolder FilePath
+ | APISetXFTPConfig (Maybe XFTPFileConfig)
| SetIncognito Bool
| APIExportArchive ArchiveConfig
| APIImportArchive ArchiveConfig
diff --git a/tests/ChatTests/Files.hs b/tests/ChatTests/Files.hs
index 8b776fec2..2f2d93744 100644
--- a/tests/ChatTests/Files.hs
+++ b/tests/ChatTests/Files.hs
@@ -50,6 +50,7 @@ chatFileTests = do
xit "send and receive file to group, fully asynchronous" testAsyncGroupFileTransfer
describe "file transfer over XFTP" $ do
it "send and receive file" testXFTPFileTransfer
+ it "with changed XFTP config: send and receive file" testXFTPWithChangedConfig
it "with relative paths: send and receive file" testXFTPWithRelativePaths
it "continue receiving file after restart" testXFTPContinueRcv
@@ -942,6 +943,35 @@ testXFTPFileTransfer =
where
cfg = testCfg {xftpFileConfig = Just $ XFTPFileConfig {minFileSize = 0}, tempDir = Just "./tests/tmp"}
+testXFTPWithChangedConfig :: HasCallStack => FilePath -> IO ()
+testXFTPWithChangedConfig =
+ testChatCfg2 cfg aliceProfile bobProfile $ \alice bob -> do
+ withXFTPServer $ do
+ alice #$> ("/_xftp off", id, "ok")
+ alice #$> ("/_xftp on {\"minFileSize\":1024}", id, "ok")
+
+ bob #$> ("/xftp off", id, "ok")
+ bob #$> ("/xftp on minFileSize=1kb", id, "ok")
+
+ connectUsers alice bob
+
+ alice #> "/f @bob ./tests/fixtures/test.pdf"
+ alice <## "use /fc 1 to cancel sending"
+ bob <# "alice> sends file test.pdf (266.0 KiB / 272376 bytes)"
+ bob <## "use /fr 1 [/ | ] to receive it"
+ bob ##> "/fr 1 ./tests/tmp"
+ bob <## "saving file 1 from alice to ./tests/tmp/test.pdf"
+ -- alice <## "started sending file 1 (test.pdf) to bob" -- TODO "started uploading" ?
+ alice <## "completed sending file 1 (test.pdf) to bob"
+ bob <## "started receiving file 1 (test.pdf) from alice"
+ bob <## "completed receiving file 1 (test.pdf) from alice"
+
+ src <- B.readFile "./tests/fixtures/test.pdf"
+ dest <- B.readFile "./tests/tmp/test.pdf"
+ dest `shouldBe` src
+ where
+ cfg = testCfg {tempDir = Just "./tests/tmp"}
+
testXFTPWithRelativePaths :: HasCallStack => FilePath -> IO ()
testXFTPWithRelativePaths =
testChatCfg2 cfg aliceProfile bobProfile $ \alice bob -> do
From 8d6fe2be99d22766ae98e883835e9ebd0496bfcf Mon Sep 17 00:00:00 2001
From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
Date: Thu, 23 Mar 2023 17:29:04 +0400
Subject: [PATCH 12/17] core: restore stateTVar imports
---
src/Simplex/Chat.hs | 2 +-
src/Simplex/Chat/Store.hs | 1 +
2 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs
index 392e260e7..a07979fab 100644
--- a/src/Simplex/Chat.hs
+++ b/src/Simplex/Chat.hs
@@ -15,7 +15,7 @@
module Simplex.Chat where
import Control.Applicative (optional, (<|>))
-import Control.Concurrent.STM (retry)
+import Control.Concurrent.STM (retry, stateTVar)
import Control.Logger.Simple
import Control.Monad.Except
import Control.Monad.IO.Unlift
diff --git a/src/Simplex/Chat/Store.hs b/src/Simplex/Chat/Store.hs
index b443fa36e..d9f01985b 100644
--- a/src/Simplex/Chat/Store.hs
+++ b/src/Simplex/Chat/Store.hs
@@ -275,6 +275,7 @@ module Simplex.Chat.Store
where
import Control.Applicative ((<|>))
+import Control.Concurrent.STM (stateTVar)
import Control.Exception (Exception)
import qualified Control.Exception as E
import Control.Monad.Except
From aeb732c2f6feb2c8336db851fbed5735f5832872 Mon Sep 17 00:00:00 2001
From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
Date: Fri, 24 Mar 2023 15:20:15 +0400
Subject: [PATCH 13/17] ios: support XFTP files (#2064)
---
apps/ios/Shared/Model/SimpleXAPI.swift | 14 ++++++++
.../Views/Chat/ChatItem/CIFileView.swift | 25 +++++++++++---
.../Views/Chat/ChatItem/CIVoiceView.swift | 2 +-
.../Chat/ChatItem/FramedCIVoiceView.swift | 2 +-
.../ExperimentalFeaturesView.swift | 14 ++++++--
.../Views/UserSettings/SettingsView.swift | 12 +++----
.../ios/SimpleX NSE/NotificationService.swift | 17 +++++++++-
apps/ios/SimpleXChat/APITypes.swift | 14 ++++++++
apps/ios/SimpleXChat/AppGroup.swift | 11 +++++-
apps/ios/SimpleXChat/ChatTypes.swift | 34 +++++++++++++------
apps/ios/SimpleXChat/FileUtils.swift | 7 +++-
11 files changed, 124 insertions(+), 28 deletions(-)
diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift
index d5ad40b85..9c23b01cf 100644
--- a/apps/ios/Shared/Model/SimpleXAPI.swift
+++ b/apps/ios/Shared/Model/SimpleXAPI.swift
@@ -215,12 +215,24 @@ func apiSuspendChat(timeoutMicroseconds: Int) {
logger.error("apiSuspendChat error: \(String(describing: r))")
}
+func apiSetTempFolder(tempFolder: String) throws {
+ let r = chatSendCmdSync(.setTempFolder(tempFolder: tempFolder))
+ if case .cmdOk = r { return }
+ throw r
+}
+
func apiSetFilesFolder(filesFolder: String) throws {
let r = chatSendCmdSync(.setFilesFolder(filesFolder: filesFolder))
if case .cmdOk = r { return }
throw r
}
+func setXFTPConfig(_ cfg: XFTPFileConfig?) throws {
+ let r = chatSendCmdSync(.apiSetXFTPConfig(config: cfg))
+ if case .cmdOk = r { return }
+ throw r
+}
+
func apiSetIncognito(incognito: Bool) throws {
let r = chatSendCmdSync(.setIncognito(incognito: incognito))
if case .cmdOk = r { return }
@@ -992,7 +1004,9 @@ func initializeChat(start: Bool, dbKey: String? = nil, refreshInvitations: Bool
if encryptionStartedDefault.get() {
encryptionStartedDefault.set(false)
}
+ try apiSetTempFolder(tempFolder: getTempFilesDirectory().path)
try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path)
+ try setXFTPConfig(getXFTPCfg())
try apiSetIncognito(incognito: incognitoGroupDefault.get())
m.chatInitialized = true
m.currentUser = try apiGetActiveUser()
diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift
index 3f04253e5..08170f825 100644
--- a/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift
+++ b/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift
@@ -16,8 +16,8 @@ struct CIFileView: View {
var body: some View {
let metaReserve = edited
- ? " "
- : " "
+ ? " "
+ : " "
Button(action: fileAction) {
HStack(alignment: .bottom, spacing: 6) {
fileIndicator()
@@ -45,7 +45,24 @@ struct CIFileView: View {
.padding(.leading, 10)
.padding(.trailing, 12)
}
- .disabled(file == nil || (file?.fileStatus != .rcvInvitation && file?.fileStatus != .rcvAccepted && file?.fileStatus != .rcvComplete))
+ .disabled(!itemInteractive)
+ }
+
+ var itemInteractive: Bool {
+ if let file = file {
+ switch (file.fileStatus) {
+ case .sndStored: return false
+ case .sndTransfer: return false
+ case .sndComplete: return false
+ case .sndCancelled: return false
+ case .rcvInvitation: return true
+ case .rcvAccepted: return true
+ case .rcvTransfer: return false
+ case .rcvComplete: return true
+ case .rcvCancelled: return false
+ }
+ }
+ return false
}
func fileSizeValid() -> Bool {
@@ -155,7 +172,7 @@ struct CIFileView_Previews: PreviewProvider {
ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getFileMsgContentSample(), revealed: Binding.constant(false))
ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getFileMsgContentSample(fileName: "some_long_file_name_here", fileStatus: .rcvInvitation), revealed: Binding.constant(false))
ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvAccepted), revealed: Binding.constant(false))
- ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvTransfer), revealed: Binding.constant(false))
+ ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvTransfer(rcvProgress: 7, rcvTotal: 10)), revealed: Binding.constant(false))
ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvCancelled), revealed: Binding.constant(false))
ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getFileMsgContentSample(fileSize: 1_000_000_000, fileStatus: .rcvInvitation), revealed: Binding.constant(false))
ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getFileMsgContentSample(text: "Hello there", fileStatus: .rcvInvitation), revealed: Binding.constant(false))
diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift
index e17968b7e..111643e6a 100644
--- a/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift
+++ b/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift
@@ -243,7 +243,7 @@ struct CIVoiceView_Previews: PreviewProvider {
)
ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: sentVoiceMessage, revealed: Binding.constant(false))
ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getVoiceMsgContentSample(), revealed: Binding.constant(false))
- ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getVoiceMsgContentSample(fileStatus: .rcvTransfer), revealed: Binding.constant(false))
+ ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getVoiceMsgContentSample(fileStatus: .rcvTransfer(rcvProgress: 7, rcvTotal: 10)), revealed: Binding.constant(false))
ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: voiceMessageWtFile, revealed: Binding.constant(false))
}
.previewLayout(.fixed(width: 360, height: 360))
diff --git a/apps/ios/Shared/Views/Chat/ChatItem/FramedCIVoiceView.swift b/apps/ios/Shared/Views/Chat/ChatItem/FramedCIVoiceView.swift
index 5d25a489a..34c3ecb4a 100644
--- a/apps/ios/Shared/Views/Chat/ChatItem/FramedCIVoiceView.swift
+++ b/apps/ios/Shared/Views/Chat/ChatItem/FramedCIVoiceView.swift
@@ -62,7 +62,7 @@ struct FramedCIVoiceView_Previews: PreviewProvider {
Group {
ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: sentVoiceMessage, revealed: Binding.constant(false))
ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getVoiceMsgContentSample(text: "Hello there"), revealed: Binding.constant(false))
- ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getVoiceMsgContentSample(text: "Hello there", fileStatus: .rcvTransfer), revealed: Binding.constant(false))
+ ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getVoiceMsgContentSample(text: "Hello there", fileStatus: .rcvTransfer(rcvProgress: 7, rcvTotal: 10)), revealed: Binding.constant(false))
ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getVoiceMsgContentSample(text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."), revealed: Binding.constant(false))
ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: voiceMessageWithQuote, revealed: Binding.constant(false))
}
diff --git a/apps/ios/Shared/Views/UserSettings/ExperimentalFeaturesView.swift b/apps/ios/Shared/Views/UserSettings/ExperimentalFeaturesView.swift
index 0fa754ec2..fa8be9f06 100644
--- a/apps/ios/Shared/Views/UserSettings/ExperimentalFeaturesView.swift
+++ b/apps/ios/Shared/Views/UserSettings/ExperimentalFeaturesView.swift
@@ -7,15 +7,23 @@
//
import SwiftUI
+import SimpleXChat
struct ExperimentalFeaturesView: View {
- @AppStorage(DEFAULT_EXPERIMENTAL_CALLS) private var enableCalls = false
+ @AppStorage(GROUP_DEFAULT_XFTP_SEND_ENABLED, store: UserDefaults(suiteName: APP_GROUP_NAME)!) private var xftpSendEnabled = false
var body: some View {
List {
Section("") {
- settingsRow("video") {
- Toggle("Audio & video calls", isOn: $enableCalls)
+ settingsRow("arrow.up.doc") {
+ Toggle("Send files via XFTP", isOn: $xftpSendEnabled)
+ .onChange(of: xftpSendEnabled) { _ in
+ do {
+ try setXFTPConfig(getXFTPCfg())
+ } catch {
+ logger.error("setXFTPConfig: cannot set XFTP config \(responseError(error))")
+ }
+ }
}
}
}
diff --git a/apps/ios/Shared/Views/UserSettings/SettingsView.swift b/apps/ios/Shared/Views/UserSettings/SettingsView.swift
index cb58d2fea..7d5f0115d 100644
--- a/apps/ios/Shared/Views/UserSettings/SettingsView.swift
+++ b/apps/ios/Shared/Views/UserSettings/SettingsView.swift
@@ -277,12 +277,12 @@ struct SettingsView: View {
.padding(.leading, indent)
}
}
-// NavigationLink {
-// ExperimentalFeaturesView()
-// .navigationTitle("Experimental features")
-// } label: {
-// settingsRow("gauge") { Text("Experimental features") }
-// }
+ NavigationLink {
+ ExperimentalFeaturesView()
+ .navigationTitle("Experimental features")
+ } label: {
+ settingsRow("gauge") { Text("Experimental features") }
+ }
NavigationLink {
VersionView()
.navigationBarTitle("App version")
diff --git a/apps/ios/SimpleX NSE/NotificationService.swift b/apps/ios/SimpleX NSE/NotificationService.swift
index 5eda201f2..3740ba464 100644
--- a/apps/ios/SimpleX NSE/NotificationService.swift
+++ b/apps/ios/SimpleX NSE/NotificationService.swift
@@ -199,6 +199,7 @@ class NotificationService: UNNotificationServiceExtension {
var chatStarted = false
var networkConfig: NetCfg = getNetCfg()
+var xftpConfig: XFTPFileConfig? = getXFTPCfg()
func startChat() -> DBMigrationResult? {
hs_init(0, nil)
@@ -212,10 +213,12 @@ func startChat() -> DBMigrationResult? {
logger.debug("active user \(String(describing: user))")
do {
try setNetworkConfig(networkConfig)
+ try apiSetTempFolder(tempFolder: getTempFilesDirectory().path)
+ try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path)
+ try setXFTPConfig(xftpConfig)
let justStarted = try apiStartChat()
chatStarted = true
if justStarted {
- try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path)
try apiSetIncognito(incognito: incognitoGroupDefault.get())
chatLastStartGroupDefault.set(Date.now)
Task { await receiveMessages() }
@@ -329,12 +332,24 @@ func apiStartChat() throws -> Bool {
}
}
+func apiSetTempFolder(tempFolder: String) throws {
+ let r = sendSimpleXCmd(.setTempFolder(tempFolder: tempFolder))
+ if case .cmdOk = r { return }
+ throw r
+}
+
func apiSetFilesFolder(filesFolder: String) throws {
let r = sendSimpleXCmd(.setFilesFolder(filesFolder: filesFolder))
if case .cmdOk = r { return }
throw r
}
+func setXFTPConfig(_ cfg: XFTPFileConfig?) throws {
+ let r = sendSimpleXCmd(.apiSetXFTPConfig(config: cfg))
+ if case .cmdOk = r { return }
+ throw r
+}
+
func apiSetIncognito(incognito: Bool) throws {
let r = sendSimpleXCmd(.setIncognito(incognito: incognito))
if case .cmdOk = r { return }
diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift
index ededd5b26..defc069a7 100644
--- a/apps/ios/SimpleXChat/APITypes.swift
+++ b/apps/ios/SimpleXChat/APITypes.swift
@@ -26,7 +26,9 @@ public enum ChatCommand {
case apiStopChat
case apiActivateChat
case apiSuspendChat(timeoutMicroseconds: Int)
+ case setTempFolder(tempFolder: String)
case setFilesFolder(filesFolder: String)
+ case apiSetXFTPConfig(config: XFTPFileConfig?)
case setIncognito(incognito: Bool)
case apiExportArchive(config: ArchiveConfig)
case apiImportArchive(config: ArchiveConfig)
@@ -117,7 +119,13 @@ public enum ChatCommand {
case .apiStopChat: return "/_stop"
case .apiActivateChat: return "/_app activate"
case let .apiSuspendChat(timeoutMicroseconds): return "/_app suspend \(timeoutMicroseconds)"
+ case let .setTempFolder(tempFolder): return "/_temp_folder \(tempFolder)"
case let .setFilesFolder(filesFolder): return "/_files_folder \(filesFolder)"
+ case let .apiSetXFTPConfig(cfg): if let cfg = cfg {
+ return "/_xftp on \(encodeJSON(cfg))"
+ } else {
+ return "/_xftp off"
+ }
case let .setIncognito(incognito): return "/incognito \(onOff(incognito))"
case let .apiExportArchive(cfg): return "/_db export \(encodeJSON(cfg))"
case let .apiImportArchive(cfg): return "/_db import \(encodeJSON(cfg))"
@@ -219,7 +227,9 @@ public enum ChatCommand {
case .apiStopChat: return "apiStopChat"
case .apiActivateChat: return "apiActivateChat"
case .apiSuspendChat: return "apiSuspendChat"
+ case .setTempFolder: return "setTempFolder"
case .setFilesFolder: return "setFilesFolder"
+ case .apiSetXFTPConfig: return "apiSetXFTPConfig"
case .setIncognito: return "setIncognito"
case .apiExportArchive: return "apiExportArchive"
case .apiImportArchive: return "apiImportArchive"
@@ -712,6 +722,10 @@ struct ComposedMessage: Encodable {
var msgContent: MsgContent
}
+public struct XFTPFileConfig: Encodable {
+ var minFileSize: Int64
+}
+
public struct ArchiveConfig: Encodable {
var archivePath: String
var disableCompression: Bool?
diff --git a/apps/ios/SimpleXChat/AppGroup.swift b/apps/ios/SimpleXChat/AppGroup.swift
index 3ea392c22..a39419b43 100644
--- a/apps/ios/SimpleXChat/AppGroup.swift
+++ b/apps/ios/SimpleXChat/AppGroup.swift
@@ -30,6 +30,7 @@ let GROUP_DEFAULT_INCOGNITO = "incognito"
let GROUP_DEFAULT_STORE_DB_PASSPHRASE = "storeDBPassphrase"
let GROUP_DEFAULT_INITIAL_RANDOM_DB_PASSPHRASE = "initialRandomDBPassphrase"
public let GROUP_DEFAULT_CALL_KIT_ENABLED = "callKitEnabled"
+public let GROUP_DEFAULT_XFTP_SEND_ENABLED = "xftpSendEnabled"
public let APP_GROUP_NAME = "group.chat.simplex.app"
@@ -52,7 +53,8 @@ public func registerGroupDefaults() {
GROUP_DEFAULT_INITIAL_RANDOM_DB_PASSPHRASE: false,
GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES: true,
GROUP_DEFAULT_PRIVACY_TRANSFER_IMAGES_INLINE: false,
- GROUP_DEFAULT_CALL_KIT_ENABLED: true
+ GROUP_DEFAULT_CALL_KIT_ENABLED: true,
+ GROUP_DEFAULT_XFTP_SEND_ENABLED: false
])
}
@@ -123,6 +125,8 @@ public let initialRandomDBPassphraseGroupDefault = BoolDefault(defaults: groupDe
public let callKitEnabledGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_CALL_KIT_ENABLED)
+public let xftpSendEnabledGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_XFTP_SEND_ENABLED)
+
public class DateDefault {
var defaults: UserDefaults
var key: String
@@ -195,6 +199,11 @@ public class Default {
}
}
+public func getXFTPCfg() -> XFTPFileConfig? {
+ let xftpSendEnabled = xftpSendEnabledGroupDefault.get()
+ return xftpSendEnabled ? XFTPFileConfig(minFileSize: 0) : nil
+}
+
public func getNetCfg() -> NetCfg {
let onionHosts = networkUseOnionHostsGroupDefault.get()
let (hostMode, requiredHostMode) = onionHosts.hostMode
diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift
index d5c692f1f..bcd033f67 100644
--- a/apps/ios/SimpleXChat/ChatTypes.swift
+++ b/apps/ios/SimpleXChat/ChatTypes.swift
@@ -2237,16 +2237,30 @@ public struct CIFile: Decodable {
}
}
-public enum CIFileStatus: String, Decodable {
- case sndStored = "snd_stored"
- case sndTransfer = "snd_transfer"
- case sndComplete = "snd_complete"
- case sndCancelled = "snd_cancelled"
- case rcvInvitation = "rcv_invitation"
- case rcvAccepted = "rcv_accepted"
- case rcvTransfer = "rcv_transfer"
- case rcvComplete = "rcv_complete"
- case rcvCancelled = "rcv_cancelled"
+public enum CIFileStatus: Decodable {
+ case sndStored
+ case sndTransfer(sndProgress: Int, sndTotal: Int)
+ case sndComplete
+ case sndCancelled
+ case rcvInvitation
+ case rcvAccepted
+ case rcvTransfer(rcvProgress: Int, rcvTotal: Int)
+ case rcvComplete
+ case rcvCancelled
+
+ var id: String {
+ switch self {
+ case .sndStored: return "sndStored"
+ case let .sndTransfer(sndProgress, sndTotal): return "sndTransfer \(sndProgress) \(sndTotal)"
+ case .sndComplete: return "sndComplete"
+ case .sndCancelled: return "sndCancelled"
+ case .rcvInvitation: return "rcvInvitation"
+ case .rcvAccepted: return "rcvAccepted"
+ case let .rcvTransfer(rcvProgress, rcvTotal): return "rcvTransfer \(rcvProgress) \(rcvTotal)"
+ case .rcvComplete: return "rcvComplete"
+ case .rcvCancelled: return "rcvCancelled"
+ }
+ }
}
public enum MsgContent {
diff --git a/apps/ios/SimpleXChat/FileUtils.swift b/apps/ios/SimpleXChat/FileUtils.swift
index 7df65f244..09cc0b996 100644
--- a/apps/ios/SimpleXChat/FileUtils.swift
+++ b/apps/ios/SimpleXChat/FileUtils.swift
@@ -16,7 +16,8 @@ public let MAX_IMAGE_SIZE: Int64 = 236700
public let MAX_IMAGE_SIZE_AUTO_RCV: Int64 = MAX_IMAGE_SIZE * 2
-public let MAX_FILE_SIZE: Int64 = 8000000
+//public let MAX_FILE_SIZE_SMP: Int64 = 8000000 // TODO distinguish between XFTP and SMP files
+public let MAX_FILE_SIZE: Int64 = 1_073_741_824
public let MAX_VOICE_MESSAGE_LENGTH = TimeInterval(30)
@@ -158,6 +159,10 @@ public func removeLegacyDatabaseAndFiles() -> Bool {
return r1 && r2
}
+public func getTempFilesDirectory() -> URL {
+ getAppDirectory().appendingPathComponent("temp_files", isDirectory: true)
+}
+
public func getAppFilesDirectory() -> URL {
getAppDirectory().appendingPathComponent("app_files", isDirectory: true)
}
From 1ba210fe771208ef20d2b83fccf467701e5465ff Mon Sep 17 00:00:00 2001
From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
Date: Fri, 24 Mar 2023 19:06:36 +0400
Subject: [PATCH 14/17] android: support XFTP files (#2070)
---
.../java/chat/simplex/app/model/ChatModel.kt | 38 +++++++++----------
.../java/chat/simplex/app/model/SimpleXAPI.kt | 33 +++++++++++++++-
.../simplex/app/views/chat/item/CIFileView.kt | 26 ++++++-------
.../app/views/chat/item/CIImageView.kt | 12 +++---
.../app/views/chat/item/CIVoiceView.kt | 6 +--
.../chat/simplex/app/views/helpers/Util.kt | 7 +++-
.../usersettings/ExperimentalFeaturesView.kt | 14 ++++---
.../app/views/usersettings/SettingsView.kt | 4 +-
.../app/src/main/res/values/strings.xml | 1 +
9 files changed, 91 insertions(+), 50 deletions(-)
diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt
index b573a1375..58238b48b 100644
--- a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt
+++ b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt
@@ -1674,15 +1674,15 @@ class CIFile(
val fileStatus: CIFileStatus
) {
val loaded: Boolean = when (fileStatus) {
- CIFileStatus.SndStored -> true
- CIFileStatus.SndTransfer -> true
- CIFileStatus.SndComplete -> true
- CIFileStatus.SndCancelled -> true
- CIFileStatus.RcvInvitation -> false
- CIFileStatus.RcvAccepted -> false
- CIFileStatus.RcvTransfer -> false
- CIFileStatus.RcvCancelled -> false
- CIFileStatus.RcvComplete -> true
+ is CIFileStatus.SndStored -> true
+ is CIFileStatus.SndTransfer -> true
+ is CIFileStatus.SndComplete -> true
+ is CIFileStatus.SndCancelled -> true
+ is CIFileStatus.RcvInvitation -> false
+ is CIFileStatus.RcvAccepted -> false
+ is CIFileStatus.RcvTransfer -> false
+ is CIFileStatus.RcvCancelled -> false
+ is CIFileStatus.RcvComplete -> true
}
companion object {
@@ -1698,16 +1698,16 @@ class CIFile(
}
@Serializable
-enum class CIFileStatus {
- @SerialName("snd_stored") SndStored,
- @SerialName("snd_transfer") SndTransfer,
- @SerialName("snd_complete") SndComplete,
- @SerialName("snd_cancelled") SndCancelled,
- @SerialName("rcv_invitation") RcvInvitation,
- @SerialName("rcv_accepted") RcvAccepted,
- @SerialName("rcv_transfer") RcvTransfer,
- @SerialName("rcv_complete") RcvComplete,
- @SerialName("rcv_cancelled") RcvCancelled;
+sealed class CIFileStatus {
+ @Serializable @SerialName("sndStored") object SndStored: CIFileStatus()
+ @Serializable @SerialName("sndTransfer") class SndTransfer(val sndProgress: Int, val sndTotal: Int): CIFileStatus()
+ @Serializable @SerialName("sndComplete") object SndComplete: CIFileStatus()
+ @Serializable @SerialName("sndCancelled") object SndCancelled: CIFileStatus()
+ @Serializable @SerialName("rcvInvitation") object RcvInvitation: CIFileStatus()
+ @Serializable @SerialName("rcvAccepted") object RcvAccepted: CIFileStatus()
+ @Serializable @SerialName("rcvTransfer") class RcvTransfer(val rcvProgress: Int, val rcvTotal: Int): CIFileStatus()
+ @Serializable @SerialName("rcvComplete") object RcvComplete: CIFileStatus()
+ @Serializable @SerialName("rcvCancelled") object RcvCancelled: CIFileStatus()
}
@Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt
index 35b918776..58d41984b 100644
--- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt
+++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt
@@ -146,6 +146,8 @@ class AppPreferences(val context: Context) {
val whatsNewVersion = mkStrPreference(SHARED_PREFS_WHATS_NEW_VERSION, null)
+ val xftpSendEnabled = mkBoolPreference(SHARED_PREFS_XFTP_SEND_ENABLED, false)
+
private fun mkIntPreference(prefName: String, default: Int) =
SharedPreference(
get = fun() = sharedPreferences.getInt(prefName, default),
@@ -241,6 +243,7 @@ class AppPreferences(val context: Context) {
private const val SHARED_PREFS_CURRENT_THEME = "CurrentTheme"
private const val SHARED_PREFS_PRIMARY_COLOR = "PrimaryColor"
private const val SHARED_PREFS_WHATS_NEW_VERSION = "WhatsNewVersion"
+ private const val SHARED_PREFS_XFTP_SEND_ENABLED = "XFTPSendEnabled"
}
}
@@ -266,6 +269,9 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
try {
if (chatModel.chatRunning.value == true) return
apiSetNetworkConfig(getNetCfg())
+ apiSetTempFolder(getTempFilesDirectory(appContext))
+ apiSetFilesFolder(getAppFilesDirectory(appContext))
+ apiSetXFTPConfig(getXFTPCfg())
val justStarted = apiStartChat()
val users = listUsers()
chatModel.users.clear()
@@ -273,7 +279,6 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
if (justStarted) {
chatModel.currentUser.value = user
chatModel.userCreated.value = true
- apiSetFilesFolder(getAppFilesDirectory(appContext))
apiSetIncognito(chatModel.incognito.value)
getUserChatData()
chatModel.onboardingStage.value = OnboardingStage.OnboardingComplete
@@ -434,12 +439,24 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
}
}
+ private suspend fun apiSetTempFolder(tempFolder: String) {
+ val r = sendCmd(CC.SetTempFolder(tempFolder))
+ if (r is CR.CmdOk) return
+ throw Error("failed to set temp folder: ${r.responseType} ${r.details}")
+ }
+
private suspend fun apiSetFilesFolder(filesFolder: String) {
val r = sendCmd(CC.SetFilesFolder(filesFolder))
if (r is CR.CmdOk) return
throw Error("failed to set files folder: ${r.responseType} ${r.details}")
}
+ suspend fun apiSetXFTPConfig(cfg: XFTPFileConfig?) {
+ val r = sendCmd(CC.ApiSetXFTPConfig(cfg))
+ if (r is CR.CmdOk) return
+ throw Error("apiSetXFTPConfig bad response: ${r.responseType} ${r.details}")
+ }
+
suspend fun apiSetIncognito(incognito: Boolean) {
val r = sendCmd(CC.SetIncognito(incognito))
if (r is CR.CmdOk) return
@@ -1683,6 +1700,11 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
}
}
+ fun getXFTPCfg(): XFTPFileConfig? {
+ val prefXFTPSendEnabled = appPrefs.xftpSendEnabled.get()
+ return if (prefXFTPSendEnabled) XFTPFileConfig(minFileSize = 0) else null
+ }
+
fun getNetCfg(): NetCfg {
val useSocksProxy = appPrefs.networkUseSocksProxy.get()
val socksProxy = if (useSocksProxy) ":9050" else null
@@ -1758,7 +1780,9 @@ sealed class CC {
class ApiDeleteUser(val userId: Long, val delSMPQueues: Boolean): CC()
class StartChat(val expire: Boolean): CC()
class ApiStopChat: CC()
+ class SetTempFolder(val tempFolder: String): CC()
class SetFilesFolder(val filesFolder: String): CC()
+ class ApiSetXFTPConfig(val config: XFTPFileConfig?): CC()
class SetIncognito(val incognito: Boolean): CC()
class ApiExportArchive(val config: ArchiveConfig): CC()
class ApiImportArchive(val config: ArchiveConfig): CC()
@@ -1835,7 +1859,9 @@ sealed class CC {
is ApiDeleteUser -> "/_delete user $userId del_smp=${onOff(delSMPQueues)}"
is StartChat -> "/_start subscribe=on expire=${onOff(expire)}"
is ApiStopChat -> "/_stop"
+ is SetTempFolder -> "/_temp_folder $tempFolder"
is SetFilesFolder -> "/_files_folder $filesFolder"
+ is ApiSetXFTPConfig -> if (config != null) "/_xftp on ${json.encodeToString(config)}" else "/_xftp off"
is SetIncognito -> "/incognito ${onOff(incognito)}"
is ApiExportArchive -> "/_db export ${json.encodeToString(config)}"
is ApiImportArchive -> "/_db import ${json.encodeToString(config)}"
@@ -1913,7 +1939,9 @@ sealed class CC {
is ApiDeleteUser -> "apiDeleteUser"
is StartChat -> "startChat"
is ApiStopChat -> "apiStopChat"
+ is SetTempFolder -> "setTempFolder"
is SetFilesFolder -> "setFilesFolder"
+ is ApiSetXFTPConfig -> "apiSetXFTPConfig"
is SetIncognito -> "setIncognito"
is ApiExportArchive -> "apiExportArchive"
is ApiImportArchive -> "apiImportArchive"
@@ -2027,6 +2055,9 @@ sealed class ChatPagination {
@Serializable
class ComposedMessage(val filePath: String?, val quotedItemId: Long?, val msgContent: MsgContent)
+@Serializable
+class XFTPFileConfig(val minFileSize: Long)
+
@Serializable
class ArchiveConfig(val archivePath: String, val disableCompression: Boolean? = null, val parentTempDirectory: String? = null)
diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIFileView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIFileView.kt
index b9e3bbc62..0df2c1518 100644
--- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIFileView.kt
+++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIFileView.kt
@@ -72,7 +72,7 @@ fun CIFileView(
fun fileAction() {
if (file != null) {
when (file.fileStatus) {
- CIFileStatus.RcvInvitation -> {
+ is CIFileStatus.RcvInvitation -> {
if (fileSizeValid()) {
receiveFile(file.fileId)
} else {
@@ -82,12 +82,12 @@ fun CIFileView(
)
}
}
- CIFileStatus.RcvAccepted ->
+ is CIFileStatus.RcvAccepted ->
AlertManager.shared.showAlertMsg(
generalGetString(R.string.waiting_for_file),
String.format(generalGetString(R.string.file_will_be_received_when_contact_is_online), MAX_FILE_SIZE)
)
- CIFileStatus.RcvComplete -> {
+ is CIFileStatus.RcvComplete -> {
val filePath = getLoadedFilePath(context, file)
if (filePath != null) {
saveFileLauncher.launch(file.fileName)
@@ -120,19 +120,19 @@ fun CIFileView(
) {
if (file != null) {
when (file.fileStatus) {
- CIFileStatus.SndStored -> fileIcon()
- CIFileStatus.SndTransfer -> progressIndicator()
- CIFileStatus.SndComplete -> fileIcon(innerIcon = Icons.Filled.Check)
- CIFileStatus.SndCancelled -> fileIcon(innerIcon = Icons.Outlined.Close)
- CIFileStatus.RcvInvitation ->
+ is CIFileStatus.SndStored -> fileIcon()
+ is CIFileStatus.SndTransfer -> progressIndicator()
+ is CIFileStatus.SndComplete -> fileIcon(innerIcon = Icons.Filled.Check)
+ is CIFileStatus.SndCancelled -> fileIcon(innerIcon = Icons.Outlined.Close)
+ is CIFileStatus.RcvInvitation ->
if (fileSizeValid())
fileIcon(innerIcon = Icons.Outlined.ArrowDownward, color = MaterialTheme.colors.primary)
else
fileIcon(innerIcon = Icons.Outlined.PriorityHigh, color = WarningOrange)
- CIFileStatus.RcvAccepted -> fileIcon(innerIcon = Icons.Outlined.MoreHoriz)
- CIFileStatus.RcvTransfer -> progressIndicator()
- CIFileStatus.RcvComplete -> fileIcon()
- CIFileStatus.RcvCancelled -> fileIcon(innerIcon = Icons.Outlined.Close)
+ is CIFileStatus.RcvAccepted -> fileIcon(innerIcon = Icons.Outlined.MoreHoriz)
+ is CIFileStatus.RcvTransfer -> progressIndicator()
+ is CIFileStatus.RcvComplete -> fileIcon()
+ is CIFileStatus.RcvCancelled -> fileIcon(innerIcon = Icons.Outlined.Close)
}
} else {
fileIcon()
@@ -191,7 +191,7 @@ class ChatItemProvider: PreviewParameterProvider {
ChatItem.getFileMsgContentSample(),
ChatItem.getFileMsgContentSample(fileName = "some_long_file_name_here", fileStatus = CIFileStatus.RcvInvitation),
ChatItem.getFileMsgContentSample(fileStatus = CIFileStatus.RcvAccepted),
- ChatItem.getFileMsgContentSample(fileStatus = CIFileStatus.RcvTransfer),
+ ChatItem.getFileMsgContentSample(fileStatus = CIFileStatus.RcvTransfer(rcvProgress = 7, rcvTotal = 10)),
ChatItem.getFileMsgContentSample(fileStatus = CIFileStatus.RcvCancelled),
ChatItem.getFileMsgContentSample(fileSize = 1_000_000_000, fileStatus = CIFileStatus.RcvInvitation),
ChatItem.getFileMsgContentSample(text = "Hello there", fileStatus = CIFileStatus.RcvInvitation),
diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIImageView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIImageView.kt
index 644bacfe6..7fb2e92f9 100644
--- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIImageView.kt
+++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIImageView.kt
@@ -55,33 +55,33 @@ fun CIImageView(
contentAlignment = Alignment.Center
) {
when (file.fileStatus) {
- CIFileStatus.SndTransfer ->
+ is CIFileStatus.SndTransfer ->
CircularProgressIndicator(
Modifier.size(16.dp),
color = Color.White,
strokeWidth = 2.dp
)
- CIFileStatus.SndComplete ->
+ is CIFileStatus.SndComplete ->
Icon(
Icons.Filled.Check,
stringResource(R.string.icon_descr_image_snd_complete),
Modifier.fillMaxSize(),
tint = Color.White
)
- CIFileStatus.RcvAccepted ->
+ is CIFileStatus.RcvAccepted ->
Icon(
Icons.Outlined.MoreHoriz,
stringResource(R.string.icon_descr_waiting_for_image),
Modifier.fillMaxSize(),
tint = Color.White
)
- CIFileStatus.RcvTransfer ->
+ is CIFileStatus.RcvTransfer ->
CircularProgressIndicator(
Modifier.size(16.dp),
color = Color.White,
strokeWidth = 2.dp
)
- CIFileStatus.RcvInvitation ->
+ is CIFileStatus.RcvInvitation ->
Icon(
Icons.Outlined.ArrowDownward,
stringResource(R.string.icon_descr_asked_to_receive),
@@ -187,7 +187,7 @@ fun CIImageView(
generalGetString(R.string.waiting_for_image),
generalGetString(R.string.image_will_be_received_when_contact_is_online)
)
- CIFileStatus.RcvTransfer -> {} // ?
+ CIFileStatus.RcvTransfer(rcvProgress = 7, rcvTotal = 10) -> {} // ?
CIFileStatus.RcvComplete -> {} // ?
CIFileStatus.RcvCancelled -> {} // TODO
else -> {}
diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIVoiceView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIVoiceView.kt
index a42acadea..c20df776a 100644
--- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIVoiceView.kt
+++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIVoiceView.kt
@@ -210,9 +210,9 @@ private fun VoiceMsgIndicator(
PlayPauseButton(audioPlaying, sent, angle, strokeWidth, strokeColor, true, error, play, pause, longClick = longClick)
}
} else {
- if (file?.fileStatus == CIFileStatus.RcvInvitation
- || file?.fileStatus == CIFileStatus.RcvTransfer
- || file?.fileStatus == CIFileStatus.RcvAccepted
+ if (file?.fileStatus is CIFileStatus.RcvInvitation
+ || file?.fileStatus is CIFileStatus.RcvTransfer
+ || file?.fileStatus is CIFileStatus.RcvAccepted
) {
Box(
Modifier
diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt
index 17a462f58..d5e2978d9 100644
--- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt
+++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt
@@ -234,12 +234,17 @@ const val MAX_VOICE_SIZE_AUTO_RCV: Long = MAX_IMAGE_SIZE
const val MAX_VOICE_SIZE_FOR_SENDING: Long = 94680 // 6 chunks * 15780 bytes per chunk
const val MAX_VOICE_MILLIS_FOR_SENDING: Int = 43_000
-const val MAX_FILE_SIZE: Long = 8000000
+//const val MAX_FILE_SIZE_SMP: Long = 8000000 // TODO distinguish between XFTP and SMP files
+const val MAX_FILE_SIZE: Long = 1_073_741_824
fun getFilesDirectory(context: Context): String {
return context.filesDir.toString()
}
+fun getTempFilesDirectory(context: Context): String {
+ return "${getFilesDirectory(context)}/temp_files"
+}
+
fun getAppFilesDirectory(context: Context): String {
return "${getFilesDirectory(context)}/app_files"
}
diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/ExperimentalFeaturesView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/ExperimentalFeaturesView.kt
index 3fb3ee9db..637bda9eb 100644
--- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/ExperimentalFeaturesView.kt
+++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/ExperimentalFeaturesView.kt
@@ -5,18 +5,18 @@ import androidx.compose.foundation.layout.*
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.outlined.Videocam
+import androidx.compose.material.icons.outlined.UploadFile
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.MutableState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import chat.simplex.app.R
import chat.simplex.app.model.ChatModel
+import chat.simplex.app.views.helpers.withApi
@Composable
-fun ExperimentalFeaturesView(chatModel: ChatModel, enableCalls: MutableState) {
+fun ExperimentalFeaturesView(chatModel: ChatModel) {
Column(
Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.Start
@@ -27,7 +27,11 @@ fun ExperimentalFeaturesView(chatModel: ChatModel, enableCalls: MutableStateMESSAGES
CALLS
Incognito mode
+ Send files via XFTP
Your chat database
From c79eb36a7abb36ba61c59771cd57a7a0dd208aa0 Mon Sep 17 00:00:00 2001
From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
Date: Mon, 27 Mar 2023 12:37:22 +0100
Subject: [PATCH 15/17] core: update file status on XFTP progress events
(#2079)
* core: update file status on XFTP progress events
* update simplexmq
---
cabal.project | 2 +-
scripts/nix/sha256map.nix | 2 +-
src/Simplex/Chat.hs | 21 +++++++++++++--------
src/Simplex/Chat/Controller.hs | 4 ++--
src/Simplex/Chat/Messages.hs | 10 +++++-----
stack.yaml | 2 +-
6 files changed, 23 insertions(+), 18 deletions(-)
diff --git a/cabal.project b/cabal.project
index 332ebdc79..b05e83ecd 100644
--- a/cabal.project
+++ b/cabal.project
@@ -7,7 +7,7 @@ constraints: zip +disable-bzip2 +disable-zstd
source-repository-package
type: git
location: https://github.com/simplex-chat/simplexmq.git
- tag: 0df7733125add475f9de88a362bcd526091f187c
+ tag: c5eb65fec873e0493c28af8b190c3458445d1811
source-repository-package
type: git
diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix
index fab104c40..e5f9c20ab 100644
--- a/scripts/nix/sha256map.nix
+++ b/scripts/nix/sha256map.nix
@@ -1,5 +1,5 @@
{
- "https://github.com/simplex-chat/simplexmq.git"."0df7733125add475f9de88a362bcd526091f187c" = "09s2dimdq88lm4mb2xcl5vch2qb21llj8ss649vlxpkm69njpyj0";
+ "https://github.com/simplex-chat/simplexmq.git"."c5eb65fec873e0493c28af8b190c3458445d1811" = "1cqxl2862fxfl9zv2i1ckvq4xcminslqwfgy5q1w71qk0g2gg96h";
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
"https://github.com/kazu-yamamoto/http2.git"."78e18f52295a7f89e828539a03fbcb24931461a3" = "05q165anvv0qrcxqbvq1dlvw0l8gmsa9kl6sazk1mfhz2g0yimdk";
"https://github.com/simplex-chat/direct-sqlcipher.git"."34309410eb2069b029b8fc1872deb1e0db123294" = "0kwkmhyfsn2lixdlgl15smgr1h5gjk7fky6abzh8rng2h5ymnffd";
diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs
index a07979fab..939c8985a 100644
--- a/src/Simplex/Chat.hs
+++ b/src/Simplex/Chat.hs
@@ -2274,10 +2274,13 @@ processAgentMsgSndFile _corrId aFileId msg =
process user = do
fileId <- withStore $ \db -> getXFTPSndFileDBId db user $ AgentSndFileId aFileId
case msg of
- SFPROG _sent _total -> do
- -- update chat item status
- -- send status to view
- pure ()
+ SFPROG sndProgress sndTotal -> do
+ let status = CIFSSndTransfer {sndProgress, sndTotal}
+ (ci, ft) <- withStore $ \db -> do
+ liftIO $ updateCIFileStatus db user fileId status
+ ft <- getFileTransferMeta db user fileId
+ (,ft) <$> getChatItemByFileId db user fileId
+ toView $ CRSndFileProgressXFTP user ci ft sndProgress sndTotal
SFDONE _sndDescr rfds -> do
ci@(AChatItem _ d cInfo _ci@ChatItem {meta = CIMeta {itemSharedMsgId = msgId_, itemDeleted}}) <-
withStore $ \db -> getChatItemByFileId db user fileId
@@ -2338,10 +2341,12 @@ processAgentMsgRcvFile _corrId aFileId msg =
process user = do
fileId <- withStore (`getXFTPRcvFileDBId` AgentRcvFileId aFileId)
case msg of
- RFPROG _sent _total -> do
- -- update chat item status
- -- send status to view
- pure ()
+ RFPROG rcvProgress rcvTotal -> do
+ let status = CIFSRcvTransfer {rcvProgress, rcvTotal}
+ ci <- withStore $ \db -> do
+ liftIO $ updateCIFileStatus db user fileId status
+ getChatItemByFileId db user fileId
+ toView $ CRRcvFileProgressXFTP user ci rcvProgress rcvTotal
RFDONE xftpPath -> do
ft <- withStore $ \db -> getRcvFileTransfer db user fileId
case liveRcvFileTransferPath ft of
diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs
index 7a4e91150..7c3712397 100644
--- a/src/Simplex/Chat/Controller.hs
+++ b/src/Simplex/Chat/Controller.hs
@@ -443,7 +443,7 @@ data ChatResponse
| CRRcvFileAcceptedSndCancelled {user :: User, rcvFileTransfer :: RcvFileTransfer}
| CRRcvFileDescrNotReady {user :: User, chatItem :: AChatItem}
| CRRcvFileStart {user :: User, chatItem :: AChatItem}
- | CRRcvFileProgressXFTP {user :: User, chatItem :: AChatItem, receivedChunks :: Int, totalChunks :: Int}
+ | CRRcvFileProgressXFTP {user :: User, chatItem :: AChatItem, receivedSize :: Int64, totalSize :: Int64}
| CRRcvFileComplete {user :: User, chatItem :: AChatItem}
| CRRcvFileCancelled {user :: User, rcvFileTransfer :: RcvFileTransfer}
| CRRcvFileSndCancelled {user :: User, rcvFileTransfer :: RcvFileTransfer}
@@ -453,7 +453,7 @@ data ChatResponse
| CRSndFileRcvCancelled {user :: User, chatItem :: AChatItem, sndFileTransfer :: SndFileTransfer}
| CRSndGroupFileCancelled {user :: User, chatItem :: AChatItem, fileTransferMeta :: FileTransferMeta, sndFileTransfers :: [SndFileTransfer]}
| CRSndFileStartXFTP {user :: User, chatItem :: AChatItem, fileTransferMeta :: FileTransferMeta}
- | CRSndFileProgressXFTP {user :: User, chatItem :: AChatItem, fileTransferMeta :: FileTransferMeta, sentChunks :: Int, totalChunks :: Int}
+ | CRSndFileProgressXFTP {user :: User, chatItem :: AChatItem, fileTransferMeta :: FileTransferMeta, sentSize :: Int64, totalSize :: Int64}
| CRSndFileCompleteXFTP {user :: User, chatItem :: AChatItem, fileTransferMeta :: FileTransferMeta}
| CRSndFileCancelledXFTP {user :: User, chatItem :: AChatItem, fileTransferMeta :: FileTransferMeta}
| CRUserProfileUpdated {user :: User, fromProfile :: Profile, toProfile :: Profile}
diff --git a/src/Simplex/Chat/Messages.hs b/src/Simplex/Chat/Messages.hs
index e6a45349e..f84d1ab29 100644
--- a/src/Simplex/Chat/Messages.hs
+++ b/src/Simplex/Chat/Messages.hs
@@ -419,12 +419,12 @@ instance MsgDirectionI d => ToJSON (CIFile d) where
data CIFileStatus (d :: MsgDirection) where
CIFSSndStored :: CIFileStatus 'MDSnd
- CIFSSndTransfer :: {sndProgress :: Int, sndTotal :: Int} -> CIFileStatus 'MDSnd
+ CIFSSndTransfer :: {sndProgress :: Int64, sndTotal :: Int64} -> CIFileStatus 'MDSnd
CIFSSndCancelled :: CIFileStatus 'MDSnd
CIFSSndComplete :: CIFileStatus 'MDSnd
CIFSRcvInvitation :: CIFileStatus 'MDRcv
CIFSRcvAccepted :: CIFileStatus 'MDRcv
- CIFSRcvTransfer :: {rcvProgress :: Int, rcvTotal :: Int} -> CIFileStatus 'MDRcv
+ CIFSRcvTransfer :: {rcvProgress :: Int64, rcvTotal :: Int64} -> CIFileStatus 'MDRcv
CIFSRcvComplete :: CIFileStatus 'MDRcv
CIFSRcvCancelled :: CIFileStatus 'MDRcv
@@ -484,18 +484,18 @@ instance StrEncoding ACIFileStatus where
"rcv_cancelled" -> pure $ AFS SMDRcv CIFSRcvCancelled
_ -> fail "bad file status"
where
- progress :: (Int -> Int -> a) -> A.Parser a
+ progress :: (Int64 -> Int64 -> a) -> A.Parser a
progress f = f <$> num <*> num <|> pure (f 0 1)
num = A.space *> A.decimal
data JSONCIFileStatus
= JCIFSSndStored
- | JCIFSSndTransfer {sndProgress :: Int, sndTotal :: Int}
+ | JCIFSSndTransfer {sndProgress :: Int64, sndTotal :: Int64}
| JCIFSSndCancelled
| JCIFSSndComplete
| JCIFSRcvInvitation
| JCIFSRcvAccepted
- | JCIFSRcvTransfer {rcvProgress :: Int, rcvTotal :: Int}
+ | JCIFSRcvTransfer {rcvProgress :: Int64, rcvTotal :: Int64}
| JCIFSRcvComplete
| JCIFSRcvCancelled
deriving (Generic)
diff --git a/stack.yaml b/stack.yaml
index 6e935ec9f..b19382c2a 100644
--- a/stack.yaml
+++ b/stack.yaml
@@ -49,7 +49,7 @@ extra-deps:
# - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561
# - ../simplexmq
- github: simplex-chat/simplexmq
- commit: 0df7733125add475f9de88a362bcd526091f187c
+ commit: c5eb65fec873e0493c28af8b190c3458445d1811
- github: kazu-yamamoto/http2
commit: 78e18f52295a7f89e828539a03fbcb24931461a3
# - ../direct-sqlcipher
From ffea61917d0507b3ded5ff61ad71b369eab39337 Mon Sep 17 00:00:00 2001
From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
Date: Mon, 27 Mar 2023 21:02:54 +0400
Subject: [PATCH 16/17] ios: display rcv & snd files progress (#2085)
* ios: display rcv & snd files progress
* remove animation
---
apps/ios/Shared/Model/SimpleXAPI.swift | 4 +++
.../Views/Chat/ChatItem/CIFileView.swift | 27 ++++++++++++++-----
apps/ios/SimpleXChat/APITypes.swift | 6 +++++
apps/ios/SimpleXChat/ChatTypes.swift | 4 +--
4 files changed, 32 insertions(+), 9 deletions(-)
diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift
index 9c23b01cf..7c22e69e1 100644
--- a/apps/ios/Shared/Model/SimpleXAPI.swift
+++ b/apps/ios/Shared/Model/SimpleXAPI.swift
@@ -1321,6 +1321,8 @@ func processReceivedMsg(_ res: ChatResponse) async {
chatItemSimpleUpdate(user, aChatItem)
case let .rcvFileComplete(user, aChatItem):
chatItemSimpleUpdate(user, aChatItem)
+ case let .rcvFileProgressXFTP(user, aChatItem, _, _):
+ chatItemSimpleUpdate(user, aChatItem)
case let .sndFileStart(user, aChatItem, _):
chatItemSimpleUpdate(user, aChatItem)
case let .sndFileComplete(user, aChatItem, _):
@@ -1332,6 +1334,8 @@ func processReceivedMsg(_ res: ChatResponse) async {
let fileName = cItem.file?.filePath {
removeFile(fileName)
}
+ case let .sndFileProgressXFTP(user, aChatItem, _, _, _):
+ chatItemSimpleUpdate(user, aChatItem)
case let .callInvitation(invitation):
m.callInvitations[invitation.contact.id] = invitation
activateCall(invitation)
diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift
index 08170f825..1445ea3df 100644
--- a/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift
+++ b/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift
@@ -48,7 +48,7 @@ struct CIFileView: View {
.disabled(!itemInteractive)
}
- var itemInteractive: Bool {
+ private var itemInteractive: Bool {
if let file = file {
switch (file.fileStatus) {
case .sndStored: return false
@@ -65,14 +65,14 @@ struct CIFileView: View {
return false
}
- func fileSizeValid() -> Bool {
+ private func fileSizeValid() -> Bool {
if let file = file {
return file.fileSize <= MAX_FILE_SIZE
}
return false
}
- func fileAction() {
+ private func fileAction() {
logger.debug("CIFileView fileAction")
if let file = file {
switch (file.fileStatus) {
@@ -107,11 +107,12 @@ struct CIFileView: View {
}
}
- @ViewBuilder func fileIndicator() -> some View {
+ @ViewBuilder private func fileIndicator() -> some View {
if let file = file {
switch file.fileStatus {
case .sndStored: fileIcon("doc.fill")
- case .sndTransfer: ProgressView().frame(width: 30, height: 30)
+ // case .sndTransfer: ProgressView().frame(width: 30, height: 30) // TODO use for SMP files
+ case let .sndTransfer(sndProgress, sndTotal): progressCircle(sndProgress, sndTotal)
case .sndComplete: fileIcon("doc.fill", innerIcon: "checkmark", innerIconSize: 10)
case .sndCancelled: fileIcon("doc.fill", innerIcon: "xmark", innerIconSize: 10)
case .rcvInvitation:
@@ -121,7 +122,8 @@ struct CIFileView: View {
fileIcon("doc.fill", color: .orange, innerIcon: "exclamationmark", innerIconSize: 12)
}
case .rcvAccepted: fileIcon("doc.fill", innerIcon: "ellipsis", innerIconSize: 12)
- case .rcvTransfer: ProgressView().frame(width: 30, height: 30)
+ // case .rcvTransfer: ProgressView().frame(width: 30, height: 30) // TODO use for SMP files
+ case let .rcvTransfer(rcvProgress, rcvTotal): progressCircle(rcvProgress, rcvTotal)
case .rcvComplete: fileIcon("doc.fill")
case .rcvCancelled: fileIcon("doc.fill", innerIcon: "xmark", innerIconSize: 10)
}
@@ -130,7 +132,7 @@ struct CIFileView: View {
}
}
- func fileIcon(_ icon: String, color: Color = Color(uiColor: .tertiaryLabel), innerIcon: String? = nil, innerIconSize: CGFloat? = nil) -> some View {
+ private func fileIcon(_ icon: String, color: Color = Color(uiColor: .tertiaryLabel), innerIcon: String? = nil, innerIconSize: CGFloat? = nil) -> some View {
ZStack(alignment: .center) {
Image(systemName: icon)
.resizable()
@@ -149,6 +151,17 @@ struct CIFileView: View {
}
}
}
+
+ private func progressCircle(_ progress: Int64, _ total: Int64) -> some View {
+ Circle()
+ .trim(from: 0, to: Double(progress) / Double(total))
+ .stroke(
+ Color.accentColor,
+ style: StrokeStyle(lineWidth: 3)
+ )
+ .rotationEffect(.degrees(-90))
+ .frame(width: 30, height: 30)
+ }
}
struct CIFileView_Previews: PreviewProvider {
diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift
index defc069a7..0f9dffedb 100644
--- a/apps/ios/SimpleXChat/APITypes.swift
+++ b/apps/ios/SimpleXChat/APITypes.swift
@@ -451,6 +451,7 @@ public enum ChatResponse: Decodable, Error {
case rcvFileAccepted(user: User, chatItem: AChatItem)
case rcvFileAcceptedSndCancelled(user: User, rcvFileTransfer: RcvFileTransfer)
case rcvFileStart(user: User, chatItem: AChatItem)
+ case rcvFileProgressXFTP(user: User, chatItem: AChatItem, receivedSize: Int64, totalSize: Int64)
case rcvFileComplete(user: User, chatItem: AChatItem)
// sending file events
case sndFileStart(user: User, chatItem: AChatItem, sndFileTransfer: SndFileTransfer)
@@ -458,6 +459,7 @@ public enum ChatResponse: Decodable, Error {
case sndFileCancelled(chatItem: AChatItem, sndFileTransfer: SndFileTransfer)
case sndFileRcvCancelled(user: User, chatItem: AChatItem, sndFileTransfer: SndFileTransfer)
case sndGroupFileCancelled(user: User, chatItem: AChatItem, fileTransferMeta: FileTransferMeta, sndFileTransfers: [SndFileTransfer])
+ case sndFileProgressXFTP(user: User, chatItem: AChatItem, fileTransferMeta: FileTransferMeta, sentSize: Int64, totalSize: Int64)
case callInvitation(callInvitation: RcvCallInvitation)
case callOffer(user: User, contact: Contact, callType: CallType, offer: WebRTCSession, sharedKey: String?, askConfirmation: Bool)
case callAnswer(user: User, contact: Contact, answer: WebRTCSession)
@@ -558,12 +560,14 @@ public enum ChatResponse: Decodable, Error {
case .rcvFileAccepted: return "rcvFileAccepted"
case .rcvFileAcceptedSndCancelled: return "rcvFileAcceptedSndCancelled"
case .rcvFileStart: return "rcvFileStart"
+ case .rcvFileProgressXFTP: return "rcvFileProgressXFTP"
case .rcvFileComplete: return "rcvFileComplete"
case .sndFileStart: return "sndFileStart"
case .sndFileComplete: return "sndFileComplete"
case .sndFileCancelled: return "sndFileCancelled"
case .sndFileRcvCancelled: return "sndFileRcvCancelled"
case .sndGroupFileCancelled: return "sndGroupFileCancelled"
+ case .sndFileProgressXFTP: return "sndFileProgressXFTP"
case .callInvitation: return "callInvitation"
case .callOffer: return "callOffer"
case .callAnswer: return "callAnswer"
@@ -667,12 +671,14 @@ public enum ChatResponse: Decodable, Error {
case let .rcvFileAccepted(u, chatItem): return withUser(u, String(describing: chatItem))
case .rcvFileAcceptedSndCancelled: return noDetails
case let .rcvFileStart(u, chatItem): return withUser(u, String(describing: chatItem))
+ case let .rcvFileProgressXFTP(u, chatItem, receivedSize, totalSize): return withUser(u, "chatItem: \(String(describing: chatItem))\nreceivedSize: \(receivedSize)\ntotalSize: \(totalSize)")
case let .rcvFileComplete(u, chatItem): return withUser(u, String(describing: chatItem))
case let .sndFileStart(u, chatItem, _): return withUser(u, String(describing: chatItem))
case let .sndFileComplete(u, chatItem, _): return withUser(u, String(describing: chatItem))
case let .sndFileCancelled(chatItem, _): return String(describing: chatItem)
case let .sndFileRcvCancelled(u, chatItem, _): return withUser(u, String(describing: chatItem))
case let .sndGroupFileCancelled(u, chatItem, _, _): return withUser(u, String(describing: chatItem))
+ case let .sndFileProgressXFTP(u, chatItem, _, sentSize, totalSize): return withUser(u, "chatItem: \(String(describing: chatItem))\nsentSize: \(sentSize)\ntotalSize: \(totalSize)")
case let .callInvitation(inv): return String(describing: inv)
case let .callOffer(u, contact, callType, offer, sharedKey, askConfirmation): return withUser(u, "contact: \(contact.id)\ncallType: \(String(describing: callType))\nsharedKey: \(sharedKey ?? "")\naskConfirmation: \(askConfirmation)\noffer: \(String(describing: offer))")
case let .callAnswer(u, contact, answer): return withUser(u, "contact: \(contact.id)\nanswer: \(String(describing: answer))")
diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift
index bcd033f67..d58347e31 100644
--- a/apps/ios/SimpleXChat/ChatTypes.swift
+++ b/apps/ios/SimpleXChat/ChatTypes.swift
@@ -2239,12 +2239,12 @@ public struct CIFile: Decodable {
public enum CIFileStatus: Decodable {
case sndStored
- case sndTransfer(sndProgress: Int, sndTotal: Int)
+ case sndTransfer(sndProgress: Int64, sndTotal: Int64)
case sndComplete
case sndCancelled
case rcvInvitation
case rcvAccepted
- case rcvTransfer(rcvProgress: Int, rcvTotal: Int)
+ case rcvTransfer(rcvProgress: Int64, rcvTotal: Int64)
case rcvComplete
case rcvCancelled
From 0c3dc8a6e9f558f6f21f0202f92727cb72c99e97 Mon Sep 17 00:00:00 2001
From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
Date: Mon, 27 Mar 2023 19:39:22 +0100
Subject: [PATCH 17/17] core: add down migrations and fix test
---
.../Migrations/M20230318_file_description.hs | 17 +++++++++++++++++
.../Migrations/M20230321_agent_file_deleted.hs | 8 ++++++++
src/Simplex/Chat/Store.hs | 4 ++--
tests/SchemaDump.hs | 4 ++--
4 files changed, 29 insertions(+), 4 deletions(-)
diff --git a/src/Simplex/Chat/Migrations/M20230318_file_description.hs b/src/Simplex/Chat/Migrations/M20230318_file_description.hs
index 76d4ec979..39f56b2a4 100644
--- a/src/Simplex/Chat/Migrations/M20230318_file_description.hs
+++ b/src/Simplex/Chat/Migrations/M20230318_file_description.hs
@@ -37,3 +37,20 @@ CREATE INDEX idx_rcv_files_file_descr_id ON rcv_files(file_descr_id);
ALTER TABLE rcv_files ADD COLUMN agent_rcv_file_id BLOB NULL;
|]
+
+down_m20230318_file_description :: Query
+down_m20230318_file_description =
+ [sql|
+ALTER TABLE rcv_files DROP COLUMN agent_rcv_file_id;
+
+DROP INDEX idx_rcv_files_file_descr_id;
+ALTER TABLE rcv_files DROP COLUMN file_descr_id;
+
+DROP INDEX idx_snd_files_file_descr_id;
+ALTER TABLE snd_files DROP COLUMN file_descr_id;
+
+ALTER TABLE files DROP COLUMN private_snd_file_descr;
+ALTER TABLE files DROP COLUMN agent_snd_file_id;
+
+DROP TABLE xftp_file_descriptions;
+|]
diff --git a/src/Simplex/Chat/Migrations/M20230321_agent_file_deleted.hs b/src/Simplex/Chat/Migrations/M20230321_agent_file_deleted.hs
index 15a08febf..97c213ea4 100644
--- a/src/Simplex/Chat/Migrations/M20230321_agent_file_deleted.hs
+++ b/src/Simplex/Chat/Migrations/M20230321_agent_file_deleted.hs
@@ -18,3 +18,11 @@ UPDATE rcv_files SET agent_rcv_file_deleted = 0;
PRAGMA ignore_check_constraints=OFF;
|]
+
+down_m20230321_agent_file_deleted :: Query
+down_m20230321_agent_file_deleted =
+ [sql|
+ALTER TABLE rcv_files DROP COLUMN agent_rcv_file_deleted;
+
+ALTER TABLE files DROP COLUMN agent_snd_file_deleted;
+|]
diff --git a/src/Simplex/Chat/Store.hs b/src/Simplex/Chat/Store.hs
index aa3ff9833..1d45e2fe9 100644
--- a/src/Simplex/Chat/Store.hs
+++ b/src/Simplex/Chat/Store.hs
@@ -431,8 +431,8 @@ schemaMigrations =
("20230206_item_deleted_by_group_member_id", m20230206_item_deleted_by_group_member_id, Nothing),
("20230303_group_link_role", m20230303_group_link_role, Nothing),
("20230317_hidden_profiles", m20230317_hidden_profiles, Just down_m20230317_hidden_profiles),
- ("20230318_file_description", m20230318_file_description, Nothing),
- ("20230321_agent_file_deleted", m20230321_agent_file_deleted, Nothing)
+ ("20230318_file_description", m20230318_file_description, Just down_m20230318_file_description),
+ ("20230321_agent_file_deleted", m20230321_agent_file_deleted, Just down_m20230321_agent_file_deleted)
]
-- | The list of migrations in ascending order by date
diff --git a/tests/SchemaDump.hs b/tests/SchemaDump.hs
index 96197f129..5a6b53156 100644
--- a/tests/SchemaDump.hs
+++ b/tests/SchemaDump.hs
@@ -13,7 +13,7 @@ import qualified Simplex.Chat.Store as Store
import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..), closeSQLiteStore, createSQLiteStore, withConnection)
import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..), MigrationsToRun (..), toDownMigration)
import qualified Simplex.Messaging.Agent.Store.SQLite.Migrations as Migrations
-import Simplex.Messaging.Util (ifM)
+import Simplex.Messaging.Util (ifM, whenM)
import System.Directory (doesFileExist, removeFile)
import System.Process (readCreateProcess, shell)
import Test.Hspec
@@ -47,7 +47,7 @@ testSchemaMigrations = withTmpFiles $ do
mapM_ (testDownMigration st) $ drop (length noDownMigrations) Store.migrations
closeSQLiteStore st
removeFile testDB
- removeFile testSchema
+ whenM (doesFileExist testSchema) $ removeFile testSchema
where
testDownMigration st m = do
putStrLn $ "down migration " <> name m