From 4d700d113d6345af556179d7f91b802c25936889 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Thu, 20 Apr 2023 16:52:55 +0400 Subject: [PATCH] core, ios: mark files to receive from NSE, receive marked files on chat start (#2218) --- apps/ios/Shared/Model/SimpleXAPI.swift | 2 + .../ios/SimpleX NSE/NotificationService.swift | 35 ++++++++++----- apps/ios/SimpleXChat/APITypes.swift | 3 ++ simplex-chat.cabal | 1 + src/Simplex/Chat.hs | 43 +++++++++++++++---- src/Simplex/Chat/Controller.hs | 1 + .../M20230420_rcv_files_to_receive.hs | 18 ++++++++ src/Simplex/Chat/Migrations/chat_schema.sql | 3 +- src/Simplex/Chat/Store.hs | 33 +++++++++++++- tests/ChatTests/Files.hs | 35 +++++++++++++++ 10 files changed, 152 insertions(+), 22 deletions(-) create mode 100644 src/Simplex/Chat/Migrations/M20230420_rcv_files_to_receive.hs diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 5906c45b1..53bb7db2d 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -1324,6 +1324,8 @@ func processReceivedMsg(_ res: ChatResponse) async { if active(user) { 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): chatItemSimpleUpdate(user, aChatItem) case let .rcvFileComplete(user, aChatItem): diff --git a/apps/ios/SimpleX NSE/NotificationService.swift b/apps/ios/SimpleX NSE/NotificationService.swift index 6a941c749..b4a6bbd97 100644 --- a/apps/ios/SimpleX NSE/NotificationService.swift +++ b/apps/ios/SimpleX NSE/NotificationService.swift @@ -272,28 +272,25 @@ func receivedMsgNtf(_ res: ChatResponse) async -> (String, NSENotification)? { ntfBadgeCountGroupDefault.set(max(0, ntfBadgeCountGroupDefault.get() - 1)) } if case .image = cItem.content.msgContent { - if let file = cItem.file, - file.fileProtocol == .smp, - file.fileSize <= MAX_IMAGE_SIZE_AUTO_RCV, - privacyAcceptImagesGroupDefault.get() { - cItem = apiReceiveFile(fileId: file.fileId)?.chatItem ?? cItem - } + if let file = cItem.file, + file.fileSize <= MAX_IMAGE_SIZE_AUTO_RCV, + privacyAcceptImagesGroupDefault.get() { + cItem = autoReceiveFile(file) ?? cItem + } } else if case .video = cItem.content.msgContent { if let file = cItem.file, - file.fileProtocol == .smp, file.fileSize <= MAX_VIDEO_SIZE_AUTO_RCV, privacyAcceptImagesGroupDefault.get() { - cItem = apiReceiveFile(fileId: file.fileId)?.chatItem ?? cItem + cItem = autoReceiveFile(file) ?? cItem } } else if case .voice = cItem.content.msgContent { // TODO check inlineFileMode != IFMSent if let file = cItem.file, - file.fileProtocol == .smp, file.fileSize <= MAX_IMAGE_SIZE, file.fileSize > MAX_VOICE_MESSAGE_SIZE_INLINE_SEND, 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 return cItem.showMutableNotification ? (aChatItem.chatId, ntf) : nil case let .rcvFileSndCancelled(_, aChatItem, _): @@ -401,6 +398,22 @@ func apiReceiveFile(fileId: Int64, inline: Bool? = nil) -> AChatItem? { 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 { let r = sendSimpleXCmd(.apiSetNetworkConfig(networkConfig: cfg)) if case .cmdOk = r { return } diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index c13f33833..b1021c513 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -100,6 +100,7 @@ public enum ChatCommand { case apiChatRead(type: ChatType, id: Int64, itemRange: (Int64, Int64)) case apiChatUnread(type: ChatType, id: Int64, unreadChat: Bool) case receiveFile(fileId: Int64, inline: Bool?) + case setFileToReceive(fileId: Int64) case cancelFile(fileId: Int64) case showVersion case string(String) @@ -206,6 +207,7 @@ public enum ChatCommand { return "/freceive \(fileId) inline=\(onOff(inline))" } return "/freceive \(fileId)" + case let .setFileToReceive(fileId): return "/_set_file_to_receive \(fileId)" case let .cancelFile(fileId): return "/fcancel \(fileId)" case .showVersion: return "/version" case let .string(str): return str @@ -302,6 +304,7 @@ public enum ChatCommand { case .apiChatRead: return "apiChatRead" case .apiChatUnread: return "apiChatUnread" case .receiveFile: return "receiveFile" + case .setFileToReceive: return "setFileToReceive" case .cancelFile: return "cancelFile" case .showVersion: return "showVersion" case .string: return "console command" diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 76b4ae83d..1fcdd6c73 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -91,6 +91,7 @@ library Simplex.Chat.Migrations.M20230328_files_protocol Simplex.Chat.Migrations.M20230402_protocol_servers Simplex.Chat.Migrations.M20230411_extra_xftp_file_descriptions + Simplex.Chat.Migrations.M20230420_rcv_files_to_receive Simplex.Chat.Mobile Simplex.Chat.Mobile.WebRTC Simplex.Chat.Options diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index cc1a28939..9c7db2e5b 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -225,7 +225,9 @@ startChatController subConns enableExpireCIs startXFTPWorkers = do then Just <$> async (subscribeUsers users) else pure Nothing atomically . writeTVar s $ Just (a1, a2) - when startXFTPWorkers startXFTP + when startXFTPWorkers $ do + startXFTP + void $ forkIO $ startFilesToReceive users startCleanupManager when enableExpireCIs $ startExpireCIs users pure a1 @@ -257,6 +259,22 @@ subscribeUsers users = do subscribe :: [User] -> m () 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 = do savedCalls <- fromRight [] <$> runExceptT (withStore' $ \db -> getCalls db) @@ -1385,13 +1403,11 @@ processChatCommand = \case ReceiveFile fileId rcvInline_ filePath_ -> withUser $ \_ -> withChatLock "receiveFile" . procCmd $ do (user, ft) <- withStore $ \db -> getRcvFileTransferById db fileId - (CRRcvFileAccepted user <$> acceptFileReceive user ft rcvInline_ filePath_) `catchError` processError user ft - where - processError user ft = \case - -- TODO AChatItem in Cancelled events - ChatErrorAgent (SMP SMP.AUTH) _ -> pure $ CRRcvFileAcceptedSndCancelled user ft - ChatErrorAgent (CONN DUPLICATE) _ -> pure $ CRRcvFileAcceptedSndCancelled user ft - e -> throwError e + receiveFile' user ft rcvInline_ filePath_ + SetFileToReceive fileId -> withUser $ \_ -> do + withChatLock "setFileToReceive" . procCmd $ do + withStore' (`setRcvFileToReceive` fileId) + ok_ CancelFile fileId -> withUser $ \user@User {userId} -> withChatLock "cancelFile" . procCmd $ withStore (\db -> getFileTransfer db user fileId) >>= \case @@ -1904,6 +1920,16 @@ toFSFilePath :: ChatMonad m => FilePath -> m FilePath toFSFilePath f = 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 user@User {userId} RcvFileTransfer {fileId, xftpRcvFile, fileInvitation = FileInvitation {fileName = fName, fileConnReq, fileInline, fileSize}, fileStatus, grpMemberId} rcvInline_ filePath_ = do unless (fileStatus == RFSNew) $ case fileStatus of @@ -4668,6 +4694,7 @@ chatCommandP = ("/image_forward " <|> "/imgf ") *> (ForwardImage <$> chatNameP' <* A.space <*> A.decimal), ("/fdescription " <|> "/fd") *> (SendFileDescription <$> chatNameP' <* 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), ("/fstatus " <|> "/fs ") *> (FileStatus <$> A.decimal), "/simplex" $> ConnectSimplex, diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index e345dcf47..b793d8497 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -346,6 +346,7 @@ data ChatCommand | ForwardImage ChatName FileTransferId | SendFileDescription ChatName FilePath | ReceiveFile {fileId :: FileTransferId, fileInline :: Maybe Bool, filePath :: Maybe FilePath} + | SetFileToReceive FileTransferId | CancelFile FileTransferId | FileStatus FileTransferId | ShowProfile -- UserId (not used in UI) diff --git a/src/Simplex/Chat/Migrations/M20230420_rcv_files_to_receive.hs b/src/Simplex/Chat/Migrations/M20230420_rcv_files_to_receive.hs new file mode 100644 index 000000000..0b6329bc6 --- /dev/null +++ b/src/Simplex/Chat/Migrations/M20230420_rcv_files_to_receive.hs @@ -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; +|] diff --git a/src/Simplex/Chat/Migrations/chat_schema.sql b/src/Simplex/Chat/Migrations/chat_schema.sql index fa4211fff..5f24c8a3c 100644 --- a/src/Simplex/Chat/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Migrations/chat_schema.sql @@ -229,7 +229,8 @@ CREATE TABLE rcv_files( file_descr_id INTEGER NULL REFERENCES xftp_file_descriptions ON DELETE SET 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( file_id INTEGER NOT NULL, diff --git a/src/Simplex/Chat/Store.hs b/src/Simplex/Chat/Store.hs index ffdf4382a..051a04450 100644 --- a/src/Simplex/Chat/Store.hs +++ b/src/Simplex/Chat/Store.hs @@ -190,6 +190,8 @@ module Simplex.Chat.Store acceptRcvInlineFT, startRcvInlineFT, xftpAcceptRcvFT, + setRcvFileToReceive, + getRcvFilesToReceive, setRcvFTAgentDeleted, updateRcvFileStatus, createRcvFileChunk, @@ -302,7 +304,7 @@ import Data.Text (Text) import qualified Data.Text as T import Data.Text.Encoding (decodeLatin1, encodeUtf8) 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.Type.Equality 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.M20230402_protocol_servers 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.Types import Simplex.Chat.Util (week) @@ -443,7 +446,8 @@ schemaMigrations = ("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), ("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 @@ -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 = ?" (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 fileId = do currentTs <- getCurrentTime diff --git a/tests/ChatTests/Files.hs b/tests/ChatTests/Files.hs index 0cf0fe79f..d758ac407 100644 --- a/tests/ChatTests/Files.hs +++ b/tests/ChatTests/Files.hs @@ -65,6 +65,7 @@ chatFileTests = do it "with changed XFTP config: send and receive file" testXFTPWithChangedConfig it "with relative paths: send and receive file" testXFTPWithRelativePaths xit' "continue receiving file after restart" testXFTPContinueRcv + it "receive file marked to receive on chat start" testXFTPMarkToReceive it "error receiving file" testXFTPRcvError it "cancel receiving file, repeat receive" testXFTPCancelRcvRepeat @@ -1233,6 +1234,40 @@ testXFTPContinueRcv tmp = do where 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 [/ | ] 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 tmp = do withXFTPServer $ do