core, ios: mark files to receive from NSE, receive marked files on chat start (#2218)

This commit is contained in:
spaced4ndy 2023-04-20 16:52:55 +04:00 committed by GitHub
parent 17bdd2a1d2
commit 4d700d113d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 152 additions and 22 deletions

View File

@ -1324,6 +1324,8 @@ func processReceivedMsg(_ res: ChatResponse) async {
if active(user) { if active(user) {
m.updateGroup(groupInfo) m.updateGroup(groupInfo)
} }
case let .rcvFileAccepted(user, aChatItem): // usually rcvFileAccepted is a response, but it's also an event for XFTP files auto-accepted from NSE
chatItemSimpleUpdate(user, aChatItem)
case let .rcvFileStart(user, aChatItem): case let .rcvFileStart(user, aChatItem):
chatItemSimpleUpdate(user, aChatItem) chatItemSimpleUpdate(user, aChatItem)
case let .rcvFileComplete(user, aChatItem): case let .rcvFileComplete(user, aChatItem):

View File

@ -272,28 +272,25 @@ func receivedMsgNtf(_ res: ChatResponse) async -> (String, NSENotification)? {
ntfBadgeCountGroupDefault.set(max(0, ntfBadgeCountGroupDefault.get() - 1)) ntfBadgeCountGroupDefault.set(max(0, ntfBadgeCountGroupDefault.get() - 1))
} }
if case .image = cItem.content.msgContent { if case .image = cItem.content.msgContent {
if let file = cItem.file, if let file = cItem.file,
file.fileProtocol == .smp, file.fileSize <= MAX_IMAGE_SIZE_AUTO_RCV,
file.fileSize <= MAX_IMAGE_SIZE_AUTO_RCV, privacyAcceptImagesGroupDefault.get() {
privacyAcceptImagesGroupDefault.get() { cItem = autoReceiveFile(file) ?? cItem
cItem = apiReceiveFile(fileId: file.fileId)?.chatItem ?? cItem }
}
} else if case .video = cItem.content.msgContent { } else if case .video = cItem.content.msgContent {
if let file = cItem.file, if let file = cItem.file,
file.fileProtocol == .smp,
file.fileSize <= MAX_VIDEO_SIZE_AUTO_RCV, file.fileSize <= MAX_VIDEO_SIZE_AUTO_RCV,
privacyAcceptImagesGroupDefault.get() { privacyAcceptImagesGroupDefault.get() {
cItem = apiReceiveFile(fileId: file.fileId)?.chatItem ?? cItem cItem = autoReceiveFile(file) ?? cItem
} }
} else if case .voice = cItem.content.msgContent { // TODO check inlineFileMode != IFMSent } else if case .voice = cItem.content.msgContent { // TODO check inlineFileMode != IFMSent
if let file = cItem.file, if let file = cItem.file,
file.fileProtocol == .smp,
file.fileSize <= MAX_IMAGE_SIZE, file.fileSize <= MAX_IMAGE_SIZE,
file.fileSize > MAX_VOICE_MESSAGE_SIZE_INLINE_SEND, file.fileSize > MAX_VOICE_MESSAGE_SIZE_INLINE_SEND,
privacyAcceptImagesGroupDefault.get() { privacyAcceptImagesGroupDefault.get() {
cItem = apiReceiveFile(fileId: file.fileId)?.chatItem ?? cItem cItem = autoReceiveFile(file) ?? cItem
} }
} }
let ntf: NSENotification = cInfo.ntfsEnabled ? .nse(notification: createMessageReceivedNtf(user, cInfo, cItem)) : .empty let ntf: NSENotification = cInfo.ntfsEnabled ? .nse(notification: createMessageReceivedNtf(user, cInfo, cItem)) : .empty
return cItem.showMutableNotification ? (aChatItem.chatId, ntf) : nil return cItem.showMutableNotification ? (aChatItem.chatId, ntf) : nil
case let .rcvFileSndCancelled(_, aChatItem, _): case let .rcvFileSndCancelled(_, aChatItem, _):
@ -401,6 +398,22 @@ func apiReceiveFile(fileId: Int64, inline: Bool? = nil) -> AChatItem? {
return nil return nil
} }
func apiSetFileToReceive(fileId: Int64) {
let r = sendSimpleXCmd(.setFileToReceive(fileId: fileId))
if case .cmdOk = r { return }
logger.error("setFileToReceive error: \(responseError(r))")
}
func autoReceiveFile(_ file: CIFile) -> ChatItem? {
switch file.fileProtocol {
case .smp:
return apiReceiveFile(fileId: file.fileId)?.chatItem
case .xftp:
apiSetFileToReceive(fileId: file.fileId)
return nil
}
}
func setNetworkConfig(_ cfg: NetCfg) throws { func setNetworkConfig(_ cfg: NetCfg) throws {
let r = sendSimpleXCmd(.apiSetNetworkConfig(networkConfig: cfg)) let r = sendSimpleXCmd(.apiSetNetworkConfig(networkConfig: cfg))
if case .cmdOk = r { return } if case .cmdOk = r { return }

View File

@ -100,6 +100,7 @@ public enum ChatCommand {
case apiChatRead(type: ChatType, id: Int64, itemRange: (Int64, Int64)) case apiChatRead(type: ChatType, id: Int64, itemRange: (Int64, Int64))
case apiChatUnread(type: ChatType, id: Int64, unreadChat: Bool) case apiChatUnread(type: ChatType, id: Int64, unreadChat: Bool)
case receiveFile(fileId: Int64, inline: Bool?) case receiveFile(fileId: Int64, inline: Bool?)
case setFileToReceive(fileId: Int64)
case cancelFile(fileId: Int64) case cancelFile(fileId: Int64)
case showVersion case showVersion
case string(String) case string(String)
@ -206,6 +207,7 @@ public enum ChatCommand {
return "/freceive \(fileId) inline=\(onOff(inline))" return "/freceive \(fileId) inline=\(onOff(inline))"
} }
return "/freceive \(fileId)" return "/freceive \(fileId)"
case let .setFileToReceive(fileId): return "/_set_file_to_receive \(fileId)"
case let .cancelFile(fileId): return "/fcancel \(fileId)" case let .cancelFile(fileId): return "/fcancel \(fileId)"
case .showVersion: return "/version" case .showVersion: return "/version"
case let .string(str): return str case let .string(str): return str
@ -302,6 +304,7 @@ public enum ChatCommand {
case .apiChatRead: return "apiChatRead" case .apiChatRead: return "apiChatRead"
case .apiChatUnread: return "apiChatUnread" case .apiChatUnread: return "apiChatUnread"
case .receiveFile: return "receiveFile" case .receiveFile: return "receiveFile"
case .setFileToReceive: return "setFileToReceive"
case .cancelFile: return "cancelFile" case .cancelFile: return "cancelFile"
case .showVersion: return "showVersion" case .showVersion: return "showVersion"
case .string: return "console command" case .string: return "console command"

View File

@ -91,6 +91,7 @@ library
Simplex.Chat.Migrations.M20230328_files_protocol Simplex.Chat.Migrations.M20230328_files_protocol
Simplex.Chat.Migrations.M20230402_protocol_servers Simplex.Chat.Migrations.M20230402_protocol_servers
Simplex.Chat.Migrations.M20230411_extra_xftp_file_descriptions Simplex.Chat.Migrations.M20230411_extra_xftp_file_descriptions
Simplex.Chat.Migrations.M20230420_rcv_files_to_receive
Simplex.Chat.Mobile Simplex.Chat.Mobile
Simplex.Chat.Mobile.WebRTC Simplex.Chat.Mobile.WebRTC
Simplex.Chat.Options Simplex.Chat.Options

View File

@ -225,7 +225,9 @@ startChatController subConns enableExpireCIs startXFTPWorkers = do
then Just <$> async (subscribeUsers users) then Just <$> async (subscribeUsers users)
else pure Nothing else pure Nothing
atomically . writeTVar s $ Just (a1, a2) atomically . writeTVar s $ Just (a1, a2)
when startXFTPWorkers startXFTP when startXFTPWorkers $ do
startXFTP
void $ forkIO $ startFilesToReceive users
startCleanupManager startCleanupManager
when enableExpireCIs $ startExpireCIs users when enableExpireCIs $ startExpireCIs users
pure a1 pure a1
@ -257,6 +259,22 @@ subscribeUsers users = do
subscribe :: [User] -> m () subscribe :: [User] -> m ()
subscribe = mapM_ $ runExceptT . subscribeUserConnections Agent.subscribeConnections subscribe = mapM_ $ runExceptT . subscribeUserConnections Agent.subscribeConnections
startFilesToReceive :: forall m. ChatMonad' m => [User] -> m ()
startFilesToReceive users = do
let (us, us') = partition activeUser users
startReceive us
startReceive us'
where
startReceive :: [User] -> m ()
startReceive = mapM_ $ runExceptT . startReceiveUserFiles
startReceiveUserFiles :: forall m. ChatMonad m => User -> m ()
startReceiveUserFiles user = do
filesToReceive <- withStore' (`getRcvFilesToReceive` user)
forM_ filesToReceive $ \ft ->
flip catchError (toView . CRChatError (Just user)) $
toView =<< receiveFile' user ft Nothing Nothing
restoreCalls :: ChatMonad' m => m () restoreCalls :: ChatMonad' m => m ()
restoreCalls = do restoreCalls = do
savedCalls <- fromRight [] <$> runExceptT (withStore' $ \db -> getCalls db) savedCalls <- fromRight [] <$> runExceptT (withStore' $ \db -> getCalls db)
@ -1385,13 +1403,11 @@ processChatCommand = \case
ReceiveFile fileId rcvInline_ filePath_ -> withUser $ \_ -> ReceiveFile fileId rcvInline_ filePath_ -> withUser $ \_ ->
withChatLock "receiveFile" . procCmd $ do withChatLock "receiveFile" . procCmd $ do
(user, ft) <- withStore $ \db -> getRcvFileTransferById db fileId (user, ft) <- withStore $ \db -> getRcvFileTransferById db fileId
(CRRcvFileAccepted user <$> acceptFileReceive user ft rcvInline_ filePath_) `catchError` processError user ft receiveFile' user ft rcvInline_ filePath_
where SetFileToReceive fileId -> withUser $ \_ -> do
processError user ft = \case withChatLock "setFileToReceive" . procCmd $ do
-- TODO AChatItem in Cancelled events withStore' (`setRcvFileToReceive` fileId)
ChatErrorAgent (SMP SMP.AUTH) _ -> pure $ CRRcvFileAcceptedSndCancelled user ft ok_
ChatErrorAgent (CONN DUPLICATE) _ -> pure $ CRRcvFileAcceptedSndCancelled user ft
e -> throwError e
CancelFile fileId -> withUser $ \user@User {userId} -> CancelFile fileId -> withUser $ \user@User {userId} ->
withChatLock "cancelFile" . procCmd $ withChatLock "cancelFile" . procCmd $
withStore (\db -> getFileTransfer db user fileId) >>= \case withStore (\db -> getFileTransfer db user fileId) >>= \case
@ -1904,6 +1920,16 @@ toFSFilePath :: ChatMonad m => FilePath -> m FilePath
toFSFilePath f = toFSFilePath f =
maybe f (</> f) <$> (readTVarIO =<< asks filesFolder) maybe f (</> f) <$> (readTVarIO =<< asks filesFolder)
receiveFile' :: ChatMonad m => User -> RcvFileTransfer -> Maybe Bool -> Maybe FilePath -> m ChatResponse
receiveFile' user ft rcvInline_ filePath_ = do
(CRRcvFileAccepted user <$> acceptFileReceive user ft rcvInline_ filePath_) `catchError` processError
where
processError = \case
-- TODO AChatItem in Cancelled events
ChatErrorAgent (SMP SMP.AUTH) _ -> pure $ CRRcvFileAcceptedSndCancelled user ft
ChatErrorAgent (CONN DUPLICATE) _ -> pure $ CRRcvFileAcceptedSndCancelled user ft
e -> throwError e
acceptFileReceive :: forall m. ChatMonad m => User -> RcvFileTransfer -> Maybe Bool -> Maybe FilePath -> m AChatItem 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 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 unless (fileStatus == RFSNew) $ case fileStatus of
@ -4668,6 +4694,7 @@ chatCommandP =
("/image_forward " <|> "/imgf ") *> (ForwardImage <$> chatNameP' <* A.space <*> A.decimal), ("/image_forward " <|> "/imgf ") *> (ForwardImage <$> chatNameP' <* A.space <*> A.decimal),
("/fdescription " <|> "/fd") *> (SendFileDescription <$> chatNameP' <* A.space <*> filePath), ("/fdescription " <|> "/fd") *> (SendFileDescription <$> chatNameP' <* A.space <*> filePath),
("/freceive " <|> "/fr ") *> (ReceiveFile <$> A.decimal <*> optional (" inline=" *> onOffP) <*> optional (A.space *> filePath)), ("/freceive " <|> "/fr ") *> (ReceiveFile <$> A.decimal <*> optional (" inline=" *> onOffP) <*> optional (A.space *> filePath)),
"/_set_file_to_receive " *> (SetFileToReceive <$> A.decimal),
("/fcancel " <|> "/fc ") *> (CancelFile <$> A.decimal), ("/fcancel " <|> "/fc ") *> (CancelFile <$> A.decimal),
("/fstatus " <|> "/fs ") *> (FileStatus <$> A.decimal), ("/fstatus " <|> "/fs ") *> (FileStatus <$> A.decimal),
"/simplex" $> ConnectSimplex, "/simplex" $> ConnectSimplex,

View File

@ -346,6 +346,7 @@ data ChatCommand
| ForwardImage ChatName FileTransferId | ForwardImage ChatName FileTransferId
| SendFileDescription ChatName FilePath | SendFileDescription ChatName FilePath
| ReceiveFile {fileId :: FileTransferId, fileInline :: Maybe Bool, filePath :: Maybe FilePath} | ReceiveFile {fileId :: FileTransferId, fileInline :: Maybe Bool, filePath :: Maybe FilePath}
| SetFileToReceive FileTransferId
| CancelFile FileTransferId | CancelFile FileTransferId
| FileStatus FileTransferId | FileStatus FileTransferId
| ShowProfile -- UserId (not used in UI) | ShowProfile -- UserId (not used in UI)

View File

@ -0,0 +1,18 @@
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Chat.Migrations.M20230420_rcv_files_to_receive where
import Database.SQLite.Simple (Query)
import Database.SQLite.Simple.QQ (sql)
m20230420_rcv_files_to_receive :: Query
m20230420_rcv_files_to_receive =
[sql|
ALTER TABLE rcv_files ADD COLUMN to_receive INTEGER;
|]
down_m20230420_rcv_files_to_receive :: Query
down_m20230420_rcv_files_to_receive =
[sql|
ALTER TABLE rcv_files DROP COLUMN to_receive;
|]

View File

@ -229,7 +229,8 @@ CREATE TABLE rcv_files(
file_descr_id INTEGER NULL file_descr_id INTEGER NULL
REFERENCES xftp_file_descriptions ON DELETE SET 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) agent_rcv_file_deleted INTEGER DEFAULT 0 CHECK(agent_rcv_file_deleted NOT NULL),
to_receive INTEGER
); );
CREATE TABLE snd_file_chunks( CREATE TABLE snd_file_chunks(
file_id INTEGER NOT NULL, file_id INTEGER NOT NULL,

View File

@ -190,6 +190,8 @@ module Simplex.Chat.Store
acceptRcvInlineFT, acceptRcvInlineFT,
startRcvInlineFT, startRcvInlineFT,
xftpAcceptRcvFT, xftpAcceptRcvFT,
setRcvFileToReceive,
getRcvFilesToReceive,
setRcvFTAgentDeleted, setRcvFTAgentDeleted,
updateRcvFileStatus, updateRcvFileStatus,
createRcvFileChunk, createRcvFileChunk,
@ -302,7 +304,7 @@ import Data.Text (Text)
import qualified Data.Text as T import qualified Data.Text as T
import Data.Text.Encoding (decodeLatin1, encodeUtf8) import Data.Text.Encoding (decodeLatin1, encodeUtf8)
import Data.Time (addUTCTime) import Data.Time (addUTCTime)
import Data.Time.Clock (UTCTime (..), getCurrentTime) import Data.Time.Clock (UTCTime (..), getCurrentTime, nominalDay)
import Data.Time.LocalTime (TimeZone, getCurrentTimeZone) import Data.Time.LocalTime (TimeZone, getCurrentTimeZone)
import Data.Type.Equality import Data.Type.Equality
import Database.SQLite.Simple (NamedParam (..), Only (..), Query (..), SQLError, (:.) (..)) import Database.SQLite.Simple (NamedParam (..), Only (..), Query (..), SQLError, (:.) (..))
@ -370,6 +372,7 @@ import Simplex.Chat.Migrations.M20230321_agent_file_deleted
import Simplex.Chat.Migrations.M20230328_files_protocol import Simplex.Chat.Migrations.M20230328_files_protocol
import Simplex.Chat.Migrations.M20230402_protocol_servers import Simplex.Chat.Migrations.M20230402_protocol_servers
import Simplex.Chat.Migrations.M20230411_extra_xftp_file_descriptions import Simplex.Chat.Migrations.M20230411_extra_xftp_file_descriptions
import Simplex.Chat.Migrations.M20230420_rcv_files_to_receive
import Simplex.Chat.Protocol import Simplex.Chat.Protocol
import Simplex.Chat.Types import Simplex.Chat.Types
import Simplex.Chat.Util (week) import Simplex.Chat.Util (week)
@ -443,7 +446,8 @@ schemaMigrations =
("20230321_agent_file_deleted", m20230321_agent_file_deleted, Just down_m20230321_agent_file_deleted), ("20230321_agent_file_deleted", m20230321_agent_file_deleted, Just down_m20230321_agent_file_deleted),
("20230328_files_protocol", m20230328_files_protocol, Just down_m20230328_files_protocol), ("20230328_files_protocol", m20230328_files_protocol, Just down_m20230328_files_protocol),
("20230402_protocol_servers", m20230402_protocol_servers, Just down_m20230402_protocol_servers), ("20230402_protocol_servers", m20230402_protocol_servers, Just down_m20230402_protocol_servers),
("20230411_extra_xftp_file_descriptions", m20230411_extra_xftp_file_descriptions, Just down_m20230411_extra_xftp_file_descriptions) ("20230411_extra_xftp_file_descriptions", m20230411_extra_xftp_file_descriptions, Just down_m20230411_extra_xftp_file_descriptions),
("20230420_rcv_files_to_receive", m20230420_rcv_files_to_receive, Just down_m20230420_rcv_files_to_receive)
] ]
-- | The list of migrations in ascending order by date -- | The list of migrations in ascending order by date
@ -3216,6 +3220,31 @@ acceptRcvFT_ db User {userId} fileId filePath rcvFileInline currentTs = do
"UPDATE rcv_files SET rcv_file_inline = ?, file_status = ?, updated_at = ? WHERE file_id = ?" "UPDATE rcv_files SET rcv_file_inline = ?, file_status = ?, updated_at = ? WHERE file_id = ?"
(rcvFileInline, FSAccepted, currentTs, fileId) (rcvFileInline, FSAccepted, currentTs, fileId)
setRcvFileToReceive :: DB.Connection -> FileTransferId -> IO ()
setRcvFileToReceive db fileId = do
currentTs <- getCurrentTime
DB.execute
db
"UPDATE rcv_files SET to_receive = 1, updated_at = ? WHERE file_id = ?"
(currentTs, fileId)
getRcvFilesToReceive :: DB.Connection -> User -> IO [RcvFileTransfer]
getRcvFilesToReceive db user@User {userId} = do
cutoffTs <- addUTCTime (- (2 * nominalDay)) <$> getCurrentTime
fileIds :: [Int64] <-
map fromOnly
<$> DB.query
db
[sql|
SELECT r.file_id
FROM rcv_files r
JOIN files f ON f.file_id = r.file_id
WHERE f.user_id = ? AND r.file_status = ?
AND r.to_receive = 1 AND r.created_at > ?
|]
(userId, FSNew, cutoffTs)
rights <$> mapM (runExceptT . getRcvFileTransfer db user) fileIds
setRcvFTAgentDeleted :: DB.Connection -> FileTransferId -> IO () setRcvFTAgentDeleted :: DB.Connection -> FileTransferId -> IO ()
setRcvFTAgentDeleted db fileId = do setRcvFTAgentDeleted db fileId = do
currentTs <- getCurrentTime currentTs <- getCurrentTime

View File

@ -65,6 +65,7 @@ chatFileTests = do
it "with changed XFTP config: send and receive file" testXFTPWithChangedConfig it "with changed XFTP config: send and receive file" testXFTPWithChangedConfig
it "with relative paths: send and receive file" testXFTPWithRelativePaths it "with relative paths: send and receive file" testXFTPWithRelativePaths
xit' "continue receiving file after restart" testXFTPContinueRcv xit' "continue receiving file after restart" testXFTPContinueRcv
it "receive file marked to receive on chat start" testXFTPMarkToReceive
it "error receiving file" testXFTPRcvError it "error receiving file" testXFTPRcvError
it "cancel receiving file, repeat receive" testXFTPCancelRcvRepeat it "cancel receiving file, repeat receive" testXFTPCancelRcvRepeat
@ -1233,6 +1234,40 @@ testXFTPContinueRcv tmp = do
where where
cfg = testCfg {xftpFileConfig = Just $ XFTPFileConfig {minFileSize = 0}, tempDir = Just "./tests/tmp"} cfg = testCfg {xftpFileConfig = Just $ XFTPFileConfig {minFileSize = 0}, tempDir = Just "./tests/tmp"}
testXFTPMarkToReceive :: HasCallStack => FilePath -> IO ()
testXFTPMarkToReceive = do
testChatCfg2 cfg aliceProfile bobProfile $ \alice bob -> do
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 [<dir>/ | <path>] to receive it"
-- alice <## "started sending file 1 (test.pdf) to bob" -- TODO "started uploading" ?
alice <## "completed uploading file 1 (test.pdf) for bob"
bob #$> ("/_set_file_to_receive 1", id, "ok")
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"
bob
<### [ "1 contacts connected (use /cs for the list)",
"started receiving file 1 (test.pdf) from alice",
"saving file 1 from alice to test.pdf"
]
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}}
testXFTPRcvError :: HasCallStack => FilePath -> IO () testXFTPRcvError :: HasCallStack => FilePath -> IO ()
testXFTPRcvError tmp = do testXFTPRcvError tmp = do
withXFTPServer $ do withXFTPServer $ do