From c51493e01647b8da402f023490b82080890d281c Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 4 Sep 2021 07:32:56 +0100 Subject: [PATCH] send files to contacts (#94) * schema for sending files * send file "invitation" * receive file "invitation" * send/receive file flow (with stubs) * update simplexmq * send and receive the file (WIP - only the first chunk) * sending and receiving file works (but it is slow) * use correct terminal output for file sending/receiving * improve file transfer, support cancellation * command to show file transfer status and progress * file transfer tests * resume file transfer on restart (WIP) * stabilize test of recipient cancelling file transfer * trying to improve file transfer on restart * update SMP block size and file chunk size * acquire agent lock before chat lock to test whether it avoids deadlock * fix resuming sending file on client restart * manual message ACK (prevents losing messages between agent and chat client and stabilizes resuming file reception after restart) * do NOT send file chunk after receiving it before it is appended to the file * update file chunk size for SMP block size 8192 (set in smpDefaultConfig) * save received files to ~/Downloads folder by default; create empty file when file is accepted * keep file handle used to create empty file * check message integrity * fix trying to resume sending file when it was not yet accepted * fix subscribing to pending connections on start * update simplexmq (fixes smp-server syntax parser) --- migrations/20210612_initial.sql | 55 ++++- src/Simplex/Chat.hs | 327 +++++++++++++++++++++++++--- src/Simplex/Chat/Controller.hs | 25 ++- src/Simplex/Chat/Protocol.hs | 20 ++ src/Simplex/Chat/Store.hs | 367 +++++++++++++++++++++++++++++--- src/Simplex/Chat/Styled.hs | 4 + src/Simplex/Chat/Types.hs | 78 ++++++- src/Simplex/Chat/Util.hs | 6 + src/Simplex/Chat/View.hs | 182 +++++++++++++++- src/Simplex/Chat/protocol.md | 1 + stack.yaml | 3 +- tests/ChatClient.hs | 4 +- tests/ChatTests.hs | 90 +++++++- tests/fixtures/test.jpg | Bin 0 -> 139737 bytes 14 files changed, 1088 insertions(+), 74 deletions(-) create mode 100644 tests/fixtures/test.jpg diff --git a/migrations/20210612_initial.sql b/migrations/20210612_initial.sql index 86abb601c..933ab1e60 100644 --- a/migrations/20210612_initial.sql +++ b/migrations/20210612_initial.sql @@ -130,17 +130,68 @@ CREATE TABLE group_member_intros ( UNIQUE (re_group_member_id, to_group_member_id) ); +CREATE TABLE files ( + file_id INTEGER PRIMARY KEY, + contact_id INTEGER REFERENCES contacts ON DELETE RESTRICT, + group_id INTEGER REFERENCES groups ON DELETE RESTRICT, + file_name TEXT NOT NULL, + file_path TEXT, + file_size INTEGER NOT NULL, + chunk_size INTEGER NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + user_id INTEGER NOT NULL REFERENCES users +); + +CREATE TABLE snd_files ( + file_id INTEGER NOT NULL REFERENCES files ON DELETE RESTRICT, + connection_id INTEGER NOT NULL REFERENCES connections ON DELETE RESTRICT, + file_status TEXT NOT NULL, -- new, accepted, connected, completed + group_member_id INTEGER REFERENCES group_members ON DELETE RESTRICT, + PRIMARY KEY (file_id, connection_id) +) WITHOUT ROWID; + +CREATE TABLE rcv_files ( + file_id INTEGER PRIMARY KEY REFERENCES files ON DELETE RESTRICT, + file_status TEXT NOT NULL, -- new, accepted, connected, completed + group_member_id INTEGER REFERENCES group_members ON DELETE RESTRICT, + file_queue_info BLOB +); + +CREATE TABLE snd_file_chunks ( + file_id INTEGER NOT NULL, + connection_id INTEGER NOT NULL, + chunk_number INTEGER NOT NULL, + chunk_agent_msg_id INTEGER, + chunk_sent INTEGER NOT NULL DEFAULT 0, -- 0 (sent to agent), 1 (sent to server) + FOREIGN KEY (file_id, connection_id) REFERENCES snd_files ON DELETE CASCADE, + PRIMARY KEY (file_id, connection_id, chunk_number) +) WITHOUT ROWID; + +CREATE TABLE rcv_file_chunks ( + file_id INTEGER NOT NULL REFERENCES rcv_files, + chunk_number INTEGER NOT NULL, + chunk_agent_msg_id INTEGER NOT NULL, + chunk_stored INTEGER NOT NULL DEFAULT 0, -- 0 (received), 1 (appended to file) + PRIMARY KEY (file_id, chunk_number) +) WITHOUT ROWID; + CREATE TABLE connections ( -- all SMP agent connections connection_id INTEGER PRIMARY KEY, agent_conn_id BLOB NOT NULL UNIQUE, conn_level INTEGER NOT NULL DEFAULT 0, via_contact INTEGER REFERENCES contacts (contact_id), conn_status TEXT NOT NULL, - conn_type TEXT NOT NULL, -- contact, member, member_direct + conn_type TEXT NOT NULL, -- contact, member, rcv_file, snd_file contact_id INTEGER REFERENCES contacts ON DELETE RESTRICT, group_member_id INTEGER REFERENCES group_members ON DELETE RESTRICT, + snd_file_id INTEGER, + rcv_file_id INTEGER REFERENCES rcv_files (file_id) ON DELETE RESTRICT, created_at TEXT NOT NULL DEFAULT (datetime('now')), - user_id INTEGER NOT NULL REFERENCES users + user_id INTEGER NOT NULL REFERENCES users, + FOREIGN KEY (snd_file_id, connection_id) + REFERENCES snd_files (file_id, connection_id) + ON DELETE RESTRICT + DEFERRABLE INITIALLY DEFERRED ); CREATE TABLE events ( -- messages received by the agent, append only diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 4db11757e..f8023291f 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -12,7 +12,8 @@ module Simplex.Chat where -import Control.Applicative ((<|>)) +import Control.Applicative (optional, (<|>)) +import Control.Concurrent.STM (stateTVar) import Control.Logger.Simple import Control.Monad.Except import Control.Monad.IO.Unlift @@ -24,12 +25,14 @@ import Data.Bifunctor (first) import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B import Data.Functor (($>)) +import Data.Int (Int64) import Data.List (find) +import Data.Map.Strict (Map) +import qualified Data.Map.Strict as M import Data.Maybe (isJust, mapMaybe) import Data.Text (Text) import qualified Data.Text as T import Data.Text.Encoding (encodeUtf8) -import Numeric.Natural import Simplex.Chat.Controller import Simplex.Chat.Help import Simplex.Chat.Input @@ -40,18 +43,24 @@ import Simplex.Chat.Store import Simplex.Chat.Styled (plain) import Simplex.Chat.Terminal import Simplex.Chat.Types +import Simplex.Chat.Util (ifM, unlessM) import Simplex.Chat.View import Simplex.Messaging.Agent import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), defaultAgentConfig) import Simplex.Messaging.Agent.Protocol import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Parsers (parseAll) -import Simplex.Messaging.Util (raceAny_) +import qualified Simplex.Messaging.Protocol as SMP +import Simplex.Messaging.Util (bshow, raceAny_) import System.Exit (exitFailure, exitSuccess) -import System.IO (hFlush, stdout) +import System.FilePath (combine, splitExtensions, takeFileName) +import System.IO (Handle, IOMode (..), SeekMode (..), hFlush, openFile, stdout) import Text.Read (readMaybe) import UnliftIO.Async (race_) +import UnliftIO.Concurrent (forkIO, threadDelay) +import UnliftIO.Directory (doesDirectoryExist, doesFileExist, getFileSize, getHomeDirectory, getTemporaryDirectory) import qualified UnliftIO.Exception as E +import UnliftIO.IO (hClose, hSeek, hTell) import UnliftIO.STM data ChatCommand @@ -70,17 +79,16 @@ data ChatCommand | DeleteGroup GroupName | ListMembers GroupName | SendGroupMessage GroupName ByteString + | SendFile ContactName FilePath + | SendGroupFile GroupName FilePath + | ReceiveFile Int64 (Maybe FilePath) + | CancelFile Int64 + | FileStatus Int64 | UpdateProfile Profile | ShowProfile | QuitChat deriving (Show) -data ChatConfig = ChatConfig - { agentConfig :: AgentConfig, - dbPoolSize :: Int, - tbqSize :: Natural - } - defaultChatConfig :: ChatConfig defaultChatConfig = ChatConfig @@ -92,7 +100,8 @@ defaultChatConfig = dbPoolSize = 1 }, dbPoolSize = 1, - tbqSize = 16 + tbqSize = 16, + fileChunkSize = 7050 } logCfg :: LogConfig @@ -107,7 +116,7 @@ simplexChat cfg opts t = >>= runSimplexChat newChatController :: WithTerminal t => ChatConfig -> ChatOpts -> t -> (Notification -> IO ()) -> IO ChatController -newChatController ChatConfig {agentConfig = cfg, dbPoolSize, tbqSize} ChatOpts {dbFile, smpServers} t sendNotification = do +newChatController config@ChatConfig {agentConfig = cfg, dbPoolSize, tbqSize} ChatOpts {dbFile, smpServers} t sendNotification = do chatStore <- createStore (dbFile <> ".chat.db") dbPoolSize currentUser <- newTVarIO =<< getCreateActiveUser chatStore chatTerminal <- newChatTerminal t @@ -116,6 +125,8 @@ newChatController ChatConfig {agentConfig = cfg, dbPoolSize, tbqSize} ChatOpts { inputQ <- newTBQueueIO tbqSize notifyQ <- newTBQueueIO tbqSize chatLock <- newTMVarIO () + sndFiles <- newTVarIO M.empty + rcvFiles <- newTVarIO M.empty pure ChatController {..} runSimplexChat :: ChatController -> IO () @@ -139,6 +150,7 @@ inputSubscriber :: (MonadUnliftIO m, MonadReader ChatController m) => m () inputSubscriber = do q <- asks inputQ l <- asks chatLock + a <- asks smpAgent forever $ atomically (readTBQueue q) >>= \case InputControl _ -> pure () @@ -151,10 +163,10 @@ inputSubscriber = do SendGroupMessage g msg -> showSentGroupMessage g msg _ -> printToView [plain s] user <- readTVarIO =<< asks currentUser - withLock l . void . runExceptT $ + withAgentLock a . withLock l . void . runExceptT $ processChatCommand user cmd `catchError` showChatError -processChatCommand :: ChatMonad m => User -> ChatCommand -> m () +processChatCommand :: forall m. ChatMonad m => User -> ChatCommand -> m () processChatCommand user@User {userId, profile} = \case ChatHelp -> printToView chatHelpInfo MarkdownHelp -> printToView markdownInfo @@ -247,6 +259,36 @@ processChatCommand user@User {userId, profile} = \case let msgEvent = XMsgNew $ MsgContent MTText [] [MsgContentBody {contentType = SimplexContentType XCText, contentData = msg}] sendGroupMessage members msgEvent setActive $ ActiveG gName + SendFile cName f -> do + unlessM (doesFileExist f) . chatError $ CEFileNotFound f + contact@Contact {contactId} <- withStore $ \st -> getContact st userId cName + (agentConnId, fileQInfo) <- withAgent createConnection + fileSize <- getFileSize f + let fileInv = FileInvitation {fileName = takeFileName f, fileSize, fileQInfo} + chSize <- asks $ fileChunkSize . config + ft <- withStore $ \st -> createSndFileTransfer st userId contactId f fileInv agentConnId chSize + sendDirectMessage (contactConnId contact) $ XFile fileInv + showSentFileInvitation cName ft + setActive $ ActiveC cName + SendGroupFile _gName _file -> pure () + ReceiveFile fileId filePath_ -> do + RcvFileTransfer {fileInvitation = FileInvitation {fileName, fileQInfo}, fileStatus} <- withStore $ \st -> getRcvFileTransfer st userId fileId + unless (fileStatus == RFSNew) . chatError $ CEFileAlreadyReceiving fileName + agentConnId <- withAgent $ \a -> joinConnection a fileQInfo . directMessage $ XFileAcpt fileName + filePath <- getRcvFilePath fileId filePath_ fileName + withStore $ \st -> acceptRcvFileTransfer st userId fileId agentConnId filePath + -- TODO include file sender in the message + showRcvFileAccepted fileId filePath + CancelFile fileId -> + withStore (\st -> getFileTransfer st userId fileId) >>= \case + FTSnd fts -> do + mapM_ cancelSndFileTransfer fts + showSndFileCancelled fileId + FTRcv ft -> do + cancelRcvFileTransfer ft + showRcvFileCancelled fileId + FileStatus fileId -> + withStore (\st -> getFileTransferProgress st userId fileId) >>= showFileTransferStatus UpdateProfile p -> unless (p == profile) $ do user' <- withStore $ \st -> updateUserProfile st user p asks currentUser >>= atomically . (`writeTVar` user') @@ -260,6 +302,37 @@ processChatCommand user@User {userId, profile} = \case contactMember Contact {contactId} = find $ \GroupMember {memberContactId = cId, memberStatus = s} -> cId == Just contactId && s /= GSMemRemoved && s /= GSMemLeft + getRcvFilePath :: Int64 -> Maybe FilePath -> String -> m FilePath + getRcvFilePath fileId filePath fileName = case filePath of + Nothing -> do + dir <- (`combine` "Downloads") <$> getHomeDirectory + ifM (doesDirectoryExist dir) (pure dir) getTemporaryDirectory + >>= (`uniqueCombine` fileName) + >>= createEmptyFile + Just fPath -> + ifM + (doesDirectoryExist fPath) + (fPath `uniqueCombine` fileName >>= createEmptyFile) + $ ifM + (doesFileExist fPath) + (chatError $ CEFileAlreadyExists fPath) + (createEmptyFile fPath) + where + createEmptyFile :: FilePath -> m FilePath + createEmptyFile fPath = emptyFile fPath `E.catch` (chatError . CEFileWrite fPath) + emptyFile :: FilePath -> m FilePath + emptyFile fPath = do + h <- getFileHandle fileId fPath rcvFiles AppendMode + liftIO $ B.hPut h "" >> hFlush h + pure fPath + uniqueCombine :: FilePath -> String -> m FilePath + uniqueCombine filePath fileName = tryCombine (0 :: Int) + where + tryCombine n = + let (name, ext) = splitExtensions fileName + suffix = if n == 0 then "" else "_" <> show n + f = filePath `combine` (name <> suffix <> ext) + in ifM (doesFileExist f) (tryCombine $ n + 1) (pure f) agentSubscriber :: (MonadUnliftIO m, MonadReader ChatController m) => m () agentSubscriber = do @@ -269,15 +342,15 @@ agentSubscriber = do forever $ do (_, connId, msg) <- atomically $ readTBQueue q user <- readTVarIO =<< asks currentUser - -- TODO handle errors properly withLock l . void . runExceptT $ - processAgentMessage user connId msg `catchError` (liftIO . print) + processAgentMessage user connId msg `catchError` showChatError subscribeUserConnections :: (MonadUnliftIO m, MonadReader ChatController m) => m () subscribeUserConnections = void . runExceptT $ do user <- readTVarIO =<< asks currentUser subscribeContacts user subscribeGroups user + subscribeFiles user subscribePendingConnections user where subscribeContacts user = do @@ -297,6 +370,27 @@ subscribeUserConnections = void . runExceptT $ do forM_ connectedMembers $ \(GroupMember {localDisplayName = c}, cId) -> subscribe cId `catchError` showMemberSubError g c showGroupSubscribed g + subscribeFiles user = do + withStore (`getLiveSndFileTransfers` user) >>= mapM_ subscribeSndFile + withStore (`getLiveRcvFileTransfers` user) >>= mapM_ subscribeRcvFile + where + subscribeSndFile ft@SndFileTransfer {fileId, fileStatus, agentConnId} = do + subscribe agentConnId `catchError` showSndFileSubError ft + void . forkIO $ do + threadDelay 1000000 + l <- asks chatLock + a <- asks smpAgent + unless (fileStatus == FSNew) . unlessM (isFileActive fileId sndFiles) $ + withAgentLock a . withLock l $ + sendFileChunk ft + subscribeRcvFile ft@RcvFileTransfer {fileStatus} = + case fileStatus of + RFSAccepted fInfo -> resume fInfo + RFSConnected fInfo -> resume fInfo + _ -> pure () + where + resume RcvFileInfo {agentConnId} = + subscribe agentConnId `catchError` showRcvFileSubError ft subscribePendingConnections user = do connections <- withStore (`getPendingConnections` user) forM_ connections $ \Connection {agentConnId} -> @@ -304,7 +398,7 @@ subscribeUserConnections = void . runExceptT $ do subscribe cId = withAgent (`subscribeConnection` cId) processAgentMessage :: forall m. ChatMonad m => User -> ConnId -> ACommand 'Agent -> m () -processAgentMessage user@User {userId, profile} agentConnId agentMessage = unless (sent agentMessage) $ do +processAgentMessage user@User {userId, profile} agentConnId agentMessage = do chatDirection <- withStore $ \st -> getConnectionChatDirection st user agentConnId forM_ (agentMsgConnStatus agentMessage) $ \status -> withStore $ \st -> updateConnectionStatus st (fromConnection chatDirection) status @@ -313,11 +407,11 @@ processAgentMessage user@User {userId, profile} agentConnId agentMessage = unles processDirectMessage agentMessage conn maybeContact ReceivedGroupMessage conn gName m -> processGroupMessage agentMessage conn gName m + RcvFileConnection conn ft -> + processRcvFileConn agentMessage conn ft + SndFileConnection conn ft -> + processSndFileConn agentMessage conn ft where - sent :: ACommand 'Agent -> Bool - sent SENT {} = True - sent _ = False - isMember :: MemberId -> Group -> Bool isMember memId Group {membership, members} = memberId membership == memId || isJust (find ((== memId) . memberId) members) @@ -343,12 +437,15 @@ processAgentMessage user@User {userId, profile} agentConnId agentMessage = unles acceptAgentConnection conn confId $ XInfo profile INFO connInfo -> saveConnInfo conn connInfo + MSG meta _ -> + withAckMessage agentConnId meta $ pure () _ -> pure () Just ct@Contact {localDisplayName = c} -> case agentMsg of - MSG meta msgBody -> do + MSG meta msgBody -> withAckMessage agentConnId meta $ do ChatMessage {chatMsgEvent} <- liftEither $ parseChatMessage msgBody case chatMsgEvent of XMsgNew (MsgContent MTText [] body) -> newTextMessage c meta $ find (isSimplexContentType XCText) body + XFile fInv -> processFileInvitation ct fInv XInfo p -> xInfo ct p XGrpInv gInv -> processGroupInvitation ct gInv XInfoProbe probe -> xInfoProbe ct probe @@ -461,7 +558,7 @@ processAgentMessage user@User {userId, profile} agentConnId agentMessage = unles when (contactIsReady ct) $ do notifyMemberConnected gName m when (memberCategory m == GCPreMember) $ probeMatchingContacts ct - MSG meta msgBody -> do + MSG meta msgBody -> withAckMessage agentConnId meta $ do ChatMessage {chatMsgEvent} <- liftEither $ parseChatMessage msgBody case chatMsgEvent of XMsgNew (MsgContent MTText [] body) -> @@ -476,6 +573,81 @@ processAgentMessage user@User {userId, profile} agentConnId agentMessage = unles _ -> messageError $ "unsupported message: " <> T.pack (show chatMsgEvent) _ -> pure () + processSndFileConn :: ACommand 'Agent -> Connection -> SndFileTransfer -> m () + processSndFileConn agentMsg conn ft@SndFileTransfer {fileId, fileName, fileStatus} = + case agentMsg of + REQ confId connInfo -> do + ChatMessage {chatMsgEvent} <- liftEither $ parseChatMessage connInfo + case chatMsgEvent of + XFileAcpt name + | name == fileName -> do + withStore $ \st -> updateSndFileStatus st ft FSAccepted + acceptAgentConnection conn confId XOk + | otherwise -> messageError "x.file.acpt: fileName is different from expected" + _ -> messageError "REQ from file connection must have x.file.acpt" + CON -> do + withStore $ \st -> updateSndFileStatus st ft FSConnected + showSndFileStart fileId + sendFileChunk ft + SENT msgId -> do + withStore $ \st -> updateSndFileChunkSent st ft msgId + unless (fileStatus == FSCancelled) $ sendFileChunk ft + MERR _ err -> do + cancelSndFileTransfer ft + case err of + SMP SMP.AUTH -> unless (fileStatus == FSCancelled) $ showSndFileRcvCancelled fileId + _ -> chatError $ CEFileSend fileId err + MSG meta _ -> + withAckMessage agentConnId meta $ pure () + _ -> pure () + + processRcvFileConn :: ACommand 'Agent -> Connection -> RcvFileTransfer -> m () + processRcvFileConn agentMsg _conn ft@RcvFileTransfer {fileId, chunkSize} = + case agentMsg of + CON -> do + withStore $ \st -> updateRcvFileStatus st ft FSConnected + showRcvFileStart fileId + MSG meta@MsgMeta {recipient = (msgId, _), integrity} msgBody -> withAckMessage agentConnId meta $ do + parseFileChunk msgBody >>= \case + (0, _) -> do + cancelRcvFileTransfer ft + showRcvFileSndCancelled fileId + (chunkNo, chunk) -> do + case integrity of + MsgOk -> pure () + MsgError MsgDuplicate -> pure () -- TODO remove once agent removes duplicates + MsgError e -> + badRcvFileChunk ft $ "invalid file chunk number " <> show chunkNo <> ": " <> show e + withStore (\st -> createRcvFileChunk st ft chunkNo msgId) >>= \case + RcvChunkOk -> + if B.length chunk /= fromInteger chunkSize + then badRcvFileChunk ft "incorrect chunk size" + else appendFileChunk ft chunkNo chunk + RcvChunkFinal -> + if B.length chunk > fromInteger chunkSize + then badRcvFileChunk ft "incorrect chunk size" + else do + appendFileChunk ft chunkNo chunk + withStore $ \st -> updateRcvFileStatus st ft FSComplete + showRcvFileComplete fileId + closeFileHandle fileId rcvFiles + withAgent (`deleteConnection` agentConnId) + RcvChunkDuplicate -> pure () + RcvChunkError -> badRcvFileChunk ft $ "incorrect chunk number " <> show chunkNo + _ -> pure () + + withAckMessage :: ConnId -> MsgMeta -> m () -> m () + withAckMessage cId MsgMeta {recipient = (msgId, _)} action = + action `E.finally` withAgent (\a -> ackMessage a cId msgId `catchError` \_ -> pure ()) + + badRcvFileChunk :: RcvFileTransfer -> String -> m () + badRcvFileChunk ft@RcvFileTransfer {fileStatus} err = + case fileStatus of + RFSCancelled _ -> pure () + _ -> do + cancelRcvFileTransfer ft + chatError $ CEFileRcvChunk err + notifyMemberConnected :: GroupName -> GroupMember -> m () notifyMemberConnected gName m@GroupMember {localDisplayName} = do showConnectedToGroupMember gName m @@ -496,10 +668,10 @@ processAgentMessage user@User {userId, profile} agentConnId agentMessage = unles withStore $ \st -> createSentProbeHash st userId probeId c messageWarning :: Text -> m () - messageWarning = liftIO . print + messageWarning = showMessageError "warning" messageError :: Text -> m () - messageError = liftIO . print + messageError = showMessageError "error" newTextMessage :: ContactName -> MsgMeta -> Maybe MsgContentBody -> m () newTextMessage c meta = \case @@ -519,6 +691,14 @@ processAgentMessage user@User {userId, profile} agentConnId agentMessage = unles setActive $ ActiveG gName _ -> messageError "x.msg.new: no expected message body" + processFileInvitation :: Contact -> FileInvitation -> m () + processFileInvitation Contact {contactId, localDisplayName = c} fInv = do + -- TODO chunk size has to be sent as part of invitation + chSize <- asks $ fileChunkSize . config + ft <- withStore $ \st -> createRcvFileTransfer st userId contactId fInv chSize + showReceivedFileInvitation c ft + setActive $ ActiveC c + processGroupInvitation :: Contact -> GroupInvitation -> m () processGroupInvitation ct@Contact {localDisplayName} inv@(GroupInvitation (fromMemId, fromRole) (memId, memRole) _ _) = do when (fromRole < GRAdmin || fromRole < memRole) $ chatError (CEGroupContactRole localDisplayName) @@ -662,7 +842,96 @@ processAgentMessage user@User {userId, profile} agentConnId agentMessage = unles mapM_ deleteMemberConnection ms showGroupDeleted gName m -chatError :: ChatMonad m => ChatErrorType -> m () +sendFileChunk :: ChatMonad m => SndFileTransfer -> m () +sendFileChunk ft@SndFileTransfer {fileId, fileStatus, agentConnId} = + unless (fileStatus == FSComplete || fileStatus == FSCancelled) $ + withStore (`createSndFileChunk` ft) >>= \case + Just chunkNo -> sendFileChunkNo ft chunkNo + Nothing -> do + withStore $ \st -> updateSndFileStatus st ft FSComplete + showSndFileComplete fileId + closeFileHandle fileId sndFiles + withAgent (`deleteConnection` agentConnId) + +sendFileChunkNo :: ChatMonad m => SndFileTransfer -> Integer -> m () +sendFileChunkNo ft@SndFileTransfer {agentConnId} chunkNo = do + bytes <- readFileChunk ft chunkNo + msgId <- withAgent $ \a -> sendMessage a agentConnId $ serializeFileChunk chunkNo bytes + withStore $ \st -> updateSndFileChunkMsg st ft chunkNo msgId + +readFileChunk :: ChatMonad m => SndFileTransfer -> Integer -> m ByteString +readFileChunk SndFileTransfer {fileId, filePath, chunkSize} chunkNo = + read_ `E.catch` (chatError . CEFileRead filePath) + where + read_ = do + h <- getFileHandle fileId filePath sndFiles ReadMode + pos <- hTell h + let pos' = (chunkNo - 1) * chunkSize + when (pos /= pos') $ hSeek h AbsoluteSeek pos' + liftIO . B.hGet h $ fromInteger chunkSize + +parseFileChunk :: ChatMonad m => ByteString -> m (Integer, ByteString) +parseFileChunk msg = + liftEither . first (ChatError . CEFileRcvChunk) $ + parseAll ((,) <$> A.decimal <* A.space <*> A.takeByteString) msg + +serializeFileChunk :: Integer -> ByteString -> ByteString +serializeFileChunk chunkNo bytes = bshow chunkNo <> " " <> bytes + +appendFileChunk :: ChatMonad m => RcvFileTransfer -> Integer -> ByteString -> m () +appendFileChunk ft@RcvFileTransfer {fileId, fileStatus} chunkNo chunk = + case fileStatus of + RFSConnected RcvFileInfo {filePath} -> append_ filePath + RFSCancelled _ -> pure () + _ -> chatError $ CEFileInternal "receiving file transfer not in progress" + where + append_ fPath = do + h <- getFileHandle fileId fPath rcvFiles AppendMode + E.try (liftIO $ B.hPut h chunk >> hFlush h) >>= \case + Left e -> chatError $ CEFileWrite fPath e + Right () -> withStore $ \st -> updatedRcvFileChunkStored st ft chunkNo + +getFileHandle :: ChatMonad m => Int64 -> FilePath -> (ChatController -> TVar (Map Int64 Handle)) -> IOMode -> m Handle +getFileHandle fileId filePath files ioMode = do + fs <- asks files + h_ <- M.lookup fileId <$> readTVarIO fs + maybe (newHandle fs) pure h_ + where + newHandle fs = do + -- TODO handle errors + h <- liftIO (openFile filePath ioMode) + atomically . modifyTVar fs $ M.insert fileId h + pure h + +isFileActive :: ChatMonad m => Int64 -> (ChatController -> TVar (Map Int64 Handle)) -> m Bool +isFileActive fileId files = do + fs <- asks files + isJust . M.lookup fileId <$> readTVarIO fs + +cancelRcvFileTransfer :: ChatMonad m => RcvFileTransfer -> m () +cancelRcvFileTransfer ft@RcvFileTransfer {fileId, fileStatus} = do + closeFileHandle fileId rcvFiles + withStore $ \st -> updateRcvFileStatus st ft FSCancelled + case fileStatus of + RFSAccepted RcvFileInfo {agentConnId} -> withAgent (`suspendConnection` agentConnId) + RFSConnected RcvFileInfo {agentConnId} -> withAgent (`suspendConnection` agentConnId) + _ -> pure () + +cancelSndFileTransfer :: ChatMonad m => SndFileTransfer -> m () +cancelSndFileTransfer ft@SndFileTransfer {agentConnId, fileStatus} = + unless (fileStatus == FSCancelled || fileStatus == FSComplete) $ do + withStore $ \st -> updateSndFileStatus st ft FSCancelled + withAgent $ \a -> do + void $ sendMessage a agentConnId "0 " + suspendConnection a agentConnId + +closeFileHandle :: ChatMonad m => Int64 -> (ChatController -> TVar (Map Int64 Handle)) -> m () +closeFileHandle fileId files = do + fs <- asks files + h_ <- atomically . stateTVar fs $ \m -> (M.lookup fileId m, M.delete fileId m) + mapM_ hClose h_ `E.catch` \(_ :: E.SomeException) -> pure () + +chatError :: ChatMonad m => ChatErrorType -> m a chatError = throwError . ChatError deleteMemberConnection :: ChatMonad m => GroupMember -> m () @@ -790,6 +1059,11 @@ chatCommandP = <|> ("/connect" <|> "/c") $> AddContact <|> ("/delete @" <|> "/delete " <|> "/d @" <|> "/d ") *> (DeleteContact <$> displayName) <|> A.char '@' *> (SendMessage <$> displayName <*> (A.space *> A.takeByteString)) + <|> ("/file #" <|> "/f #") *> (SendGroupFile <$> displayName <* A.space <*> filePath) + <|> ("/file @" <|> "/file " <|> "/f @" <|> "/f ") *> (SendFile <$> displayName <* A.space <*> filePath) + <|> ("/file_receive " <|> "/fr ") *> (ReceiveFile <$> A.decimal <*> optional (A.space *> filePath)) + <|> ("/file_cancel " <|> "/fc ") *> (CancelFile <$> A.decimal) + <|> ("/file_status " <|> "/fs ") *> (FileStatus <$> A.decimal) <|> ("/markdown" <|> "/m") $> MarkdownHelp <|> ("/profile " <|> "/p ") *> (UpdateProfile <$> userProfile) <|> ("/profile" <|> "/p") $> ShowProfile @@ -808,6 +1082,7 @@ chatCommandP = fullNameP name = do n <- (A.space *> A.takeByteString) <|> pure "" pure $ if B.null n then name else safeDecodeUtf8 n + filePath = T.unpack . safeDecodeUtf8 <$> A.takeByteString memberRole = (" owner" $> GROwner) <|> (" admin" $> GRAdmin) diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index b67bfbab7..ae6dfd5e7 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -11,15 +11,27 @@ import Control.Monad.Except import Control.Monad.IO.Unlift import Control.Monad.Reader import Crypto.Random (ChaChaDRG) +import Data.Int (Int64) +import Data.Map.Strict (Map) +import Numeric.Natural import Simplex.Chat.Notification import Simplex.Chat.Store (StoreError) import Simplex.Chat.Terminal import Simplex.Chat.Types import Simplex.Messaging.Agent (AgentClient) +import Simplex.Messaging.Agent.Env.SQLite (AgentConfig) import Simplex.Messaging.Agent.Protocol (AgentErrorType) import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore) +import System.IO (Handle) import UnliftIO.STM +data ChatConfig = ChatConfig + { agentConfig :: AgentConfig, + dbPoolSize :: Int, + tbqSize :: Natural, + fileChunkSize :: Integer + } + data ChatController = ChatController { currentUser :: TVar User, smpAgent :: AgentClient, @@ -29,7 +41,10 @@ data ChatController = ChatController inputQ :: TBQueue InputEvent, notifyQ :: TBQueue Notification, sendNotification :: Notification -> IO (), - chatLock :: TMVar () + chatLock :: TMVar (), + sndFiles :: TVar (Map Int64 Handle), + rcvFiles :: TVar (Map Int64 Handle), + config :: ChatConfig } data InputEvent = InputCommand String | InputControl Char @@ -51,6 +66,14 @@ data ChatErrorType | CEGroupMemberUserRemoved | CEGroupMemberNotFound ContactName | CEGroupInternal String + | CEFileNotFound String + | CEFileAlreadyReceiving String + | CEFileAlreadyExists FilePath + | CEFileRead FilePath SomeException + | CEFileWrite FilePath SomeException + | CEFileSend Int64 AgentErrorType + | CEFileRcvChunk String + | CEFileInternal String deriving (Show, Exception) type ChatMonad m = (MonadUnliftIO m, MonadReader ChatController m, MonadError ChatError m) diff --git a/src/Simplex/Chat/Protocol.hs b/src/Simplex/Chat/Protocol.hs index ee79c40aa..2ccb8289c 100644 --- a/src/Simplex/Chat/Protocol.hs +++ b/src/Simplex/Chat/Protocol.hs @@ -23,7 +23,10 @@ import qualified Data.ByteString.Lazy.Char8 as LB import Data.Int (Int64) import Data.List (find) import Data.Text (Text) +import qualified Data.Text as T +import Data.Text.Encoding (encodeUtf8) import Simplex.Chat.Types +import Simplex.Chat.Util (safeDecodeUtf8) import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Parsers (parseAll) import Simplex.Messaging.Util (bshow) @@ -33,6 +36,8 @@ data ChatDirection (p :: AParty) where SentDirectMessage :: Contact -> ChatDirection 'Client ReceivedGroupMessage :: Connection -> GroupName -> GroupMember -> ChatDirection 'Agent SentGroupMessage :: GroupName -> ChatDirection 'Client + SndFileConnection :: Connection -> SndFileTransfer -> ChatDirection 'Agent + RcvFileConnection :: Connection -> RcvFileTransfer -> ChatDirection 'Agent deriving instance Eq (ChatDirection p) @@ -42,9 +47,13 @@ fromConnection :: ChatDirection 'Agent -> Connection fromConnection = \case ReceivedDirectMessage conn _ -> conn ReceivedGroupMessage conn _ _ -> conn + SndFileConnection conn _ -> conn + RcvFileConnection conn _ -> conn data ChatMsgEvent = XMsgNew MsgContent + | XFile FileInvitation + | XFileAcpt String | XInfo Profile | XGrpInv GroupInvitation | XGrpAcpt MemberId @@ -100,6 +109,13 @@ toChatMessage RawChatMessage {chatMsgId, chatMsgEvent, chatMsgParams, chatMsgBod t <- toMsgType mt files <- mapM (toContentInfo <=< parseAll contentInfoP) rawFiles chatMsg . XMsgNew $ MsgContent {messageType = t, files, content = body} + ("x.file", [name, size, qInfo]) -> do + let fileName = T.unpack $ safeDecodeUtf8 name + fileSize <- parseAll A.decimal size + fileQInfo <- parseAll smpQueueInfoP qInfo + chatMsg . XFile $ FileInvitation {fileName, fileSize, fileQInfo} + ("x.file.acpt", [name]) -> + chatMsg . XFileAcpt . T.unpack $ safeDecodeUtf8 name ("x.info", []) -> do profile <- getJSON body chatMsg $ XInfo profile @@ -174,6 +190,10 @@ rawChatMessage ChatMessage {chatMsgId, chatMsgEvent, chatDAG} = XMsgNew MsgContent {messageType = t, files, content} -> let rawFiles = map (serializeContentInfo . rawContentInfo) files in rawMsg "x.msg.new" (rawMsgType t : rawFiles) content + XFile FileInvitation {fileName, fileSize, fileQInfo} -> + rawMsg "x.file" [encodeUtf8 $ T.pack fileName, bshow fileSize, serializeSmpQueueInfo fileQInfo] [] + XFileAcpt fileName -> + rawMsg "x.file.acpt" [encodeUtf8 $ T.pack fileName] [] XInfo profile -> rawMsg "x.info" [] [jsonBody profile] XGrpInv (GroupInvitation (fromMemId, fromRole) (memId, role) qInfo groupProfile) -> diff --git a/src/Simplex/Chat/Store.hs b/src/Simplex/Chat/Store.hs index a5f529eb5..4896ea56e 100644 --- a/src/Simplex/Chat/Store.hs +++ b/src/Simplex/Chat/Store.hs @@ -7,6 +7,7 @@ {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TupleSections #-} @@ -27,6 +28,9 @@ module Simplex.Chat.Store updateUserProfile, updateContactProfile, getUserContacts, + getLiveSndFileTransfers, + getLiveRcvFileTransfers, + getPendingSndChunks, getPendingConnections, getContactConnections, getConnectionChatDirection, @@ -58,6 +62,19 @@ module Simplex.Chat.Store matchReceivedProbeHash, matchSentProbe, mergeContactRecords, + createSndFileTransfer, + updateSndFileStatus, + createSndFileChunk, + updateSndFileChunkMsg, + updateSndFileChunkSent, + createRcvFileTransfer, + getRcvFileTransfer, + acceptRcvFileTransfer, + updateRcvFileStatus, + createRcvFileChunk, + updatedRcvFileChunkStored, + getFileTransfer, + getFileTransferProgress, ) where @@ -85,11 +102,11 @@ import qualified Database.SQLite.Simple as DB import Database.SQLite.Simple.QQ (sql) import Simplex.Chat.Protocol import Simplex.Chat.Types -import Simplex.Messaging.Agent.Protocol (AParty (..), ConnId, SMPQueueInfo) +import Simplex.Messaging.Agent.Protocol (AParty (..), AgentMsgId, ConnId, SMPQueueInfo) import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore (..), createSQLiteStore, withTransaction) import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..)) import qualified Simplex.Messaging.Crypto as C -import Simplex.Messaging.Util (bshow, liftIOEither) +import Simplex.Messaging.Util (bshow, liftIOEither, (<$$>)) import System.FilePath (takeBaseName, takeExtension) import UnliftIO.STM @@ -315,7 +332,7 @@ getContact_ db userId localDisplayName = do db [sql| SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, - c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.created_at + c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.created_at FROM connections c WHERE c.user_id = :user_id AND c.contact_id == :contact_id ORDER BY c.connection_id DESC @@ -334,9 +351,58 @@ getContact_ db userId localDisplayName = do getUserContacts :: MonadUnliftIO m => SQLiteStore -> User -> m [Contact] getUserContacts st User {userId} = liftIO . withTransaction st $ \db -> do - contactNames <- liftIO $ map fromOnly <$> DB.query db "SELECT local_display_name FROM contacts WHERE user_id = ?" (Only userId) + contactNames <- map fromOnly <$> DB.query db "SELECT local_display_name FROM contacts WHERE user_id = ?" (Only userId) rights <$> mapM (runExceptT . getContact_ db userId) contactNames +getLiveSndFileTransfers :: MonadUnliftIO m => SQLiteStore -> User -> m [SndFileTransfer] +getLiveSndFileTransfers st User {userId} = + liftIO . withTransaction st $ \db -> do + fileIds :: [Int64] <- + map fromOnly + <$> DB.query + db + [sql| + SELECT DISTINCT f.file_id + FROM files f + JOIN snd_files s + WHERE f.user_id = ? AND s.file_status IN (?, ?, ?) + |] + (userId, FSNew, FSAccepted, FSConnected) + concatMap (filter liveTransfer) . rights <$> mapM (getSndFileTransfers_ db userId) fileIds + where + liveTransfer :: SndFileTransfer -> Bool + liveTransfer SndFileTransfer {fileStatus} = fileStatus `elem` [FSNew, FSAccepted, FSConnected] + +getLiveRcvFileTransfers :: MonadUnliftIO m => SQLiteStore -> User -> m [RcvFileTransfer] +getLiveRcvFileTransfers st User {userId} = + liftIO . withTransaction st $ \db -> do + fileIds :: [Int64] <- + map fromOnly + <$> DB.query + db + [sql| + SELECT f.file_id + FROM files f + JOIN rcv_files r + WHERE f.user_id = ? AND r.file_status IN (?, ?) + |] + (userId, FSAccepted, FSConnected) + rights <$> mapM (getRcvFileTransfer_ db userId) fileIds + +getPendingSndChunks :: MonadUnliftIO m => SQLiteStore -> Int64 -> Int64 -> m [Integer] +getPendingSndChunks st fileId connId = + liftIO . withTransaction st $ \db -> + map fromOnly + <$> DB.query + db + [sql| + SELECT chunk_number + FROM snd_file_chunks + WHERE file_id = ? AND connection_id = ? AND chunk_agent_msg_id IS NULL + ORDER BY chunk_number + |] + (fileId, connId) + getPendingConnections :: MonadUnliftIO m => SQLiteStore -> User -> m [Connection] getPendingConnections st User {userId} = liftIO . withTransaction st $ \db -> @@ -344,12 +410,12 @@ getPendingConnections st User {userId} = <$> DB.queryNamed db [sql| - SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, - c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.created_at - FROM connections c - WHERE c.user_id = :user_id - AND c.conn_type = :conn_type - AND c.contact_id IS NULL + SELECT connection_id, agent_conn_id, conn_level, via_contact, + conn_status, conn_type, contact_id, group_member_id, snd_file_id, rcv_file_id, created_at + FROM connections + WHERE user_id = :user_id + AND conn_type = :conn_type + AND contact_id IS NULL |] [":user_id" := userId, ":conn_type" := ConnContact] @@ -361,7 +427,7 @@ getContactConnections st userId displayName = db [sql| SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, - c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.created_at + c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.created_at FROM connections c JOIN contacts cs ON c.contact_id == cs.contact_id WHERE c.user_id = :user_id @@ -373,22 +439,24 @@ getContactConnections st userId displayName = connections [] = Left $ SEContactNotFound displayName connections rows = Right $ map toConnection rows -type ConnectionRow = (Int64, ConnId, Int, Maybe Int64, ConnStatus, ConnType, Maybe Int64, Maybe Int64, UTCTime) +type ConnectionRow = (Int64, ConnId, Int, Maybe Int64, ConnStatus, ConnType, Maybe Int64, Maybe Int64, Maybe Int64, Maybe Int64, UTCTime) -type MaybeConnectionRow = (Maybe Int64, Maybe ConnId, Maybe Int, Maybe Int64, Maybe ConnStatus, Maybe ConnType, Maybe Int64, Maybe Int64, Maybe UTCTime) +type MaybeConnectionRow = (Maybe Int64, Maybe ConnId, Maybe Int, Maybe Int64, Maybe ConnStatus, Maybe ConnType, Maybe Int64, Maybe Int64, Maybe Int64, Maybe Int64, Maybe UTCTime) toConnection :: ConnectionRow -> Connection -toConnection (connId, agentConnId, connLevel, viaContact, connStatus, connType, contactId, groupMemberId, createdAt) = +toConnection (connId, agentConnId, connLevel, viaContact, connStatus, connType, contactId, groupMemberId, sndFileId, rcvFileId, createdAt) = let entityId = entityId_ connType in Connection {connId, agentConnId, connLevel, viaContact, connStatus, connType, entityId, createdAt} where entityId_ :: ConnType -> Maybe Int64 entityId_ ConnContact = contactId entityId_ ConnMember = groupMemberId + entityId_ ConnRcvFile = rcvFileId + entityId_ ConnSndFile = sndFileId toMaybeConnection :: MaybeConnectionRow -> Maybe Connection -toMaybeConnection (Just connId, Just agentConnId, Just connLevel, viaContact, Just connStatus, Just connType, contactId, groupMemberId, Just createdAt) = - Just $ toConnection (connId, agentConnId, connLevel, viaContact, connStatus, connType, contactId, groupMemberId, createdAt) +toMaybeConnection (Just connId, Just agentConnId, Just connLevel, viaContact, Just connStatus, Just connType, contactId, groupMemberId, sndFileId, rcvFileId, Just createdAt) = + Just $ toConnection (connId, agentConnId, connLevel, viaContact, connStatus, connType, contactId, groupMemberId, sndFileId, rcvFileId, createdAt) toMaybeConnection _ = Nothing getMatchingContacts :: MonadUnliftIO m => SQLiteStore -> UserId -> Contact -> m [Contact] @@ -515,15 +583,17 @@ getConnectionChatDirection :: StoreMonad m => SQLiteStore -> User -> ConnId -> m getConnectionChatDirection st User {userId, userContactId} agentConnId = liftIOEither . withTransaction st $ \db -> runExceptT $ do c@Connection {connType, entityId} <- getConnection_ db - case connType of - ConnMember -> - case entityId of - Nothing -> throwError $ SEInternal "group member without connection" - Just groupMemberId -> uncurry (ReceivedGroupMessage c) <$> getGroupAndMember_ db groupMemberId c - ConnContact -> - ReceivedDirectMessage c <$> case entityId of - Nothing -> pure Nothing - Just contactId -> Just <$> getContactRec_ db contactId c + case entityId of + Nothing -> + if connType == ConnContact + then pure $ ReceivedDirectMessage c Nothing + else throwError $ SEInternal $ "connection " <> bshow connType <> " without entity" + Just entId -> + case connType of + ConnMember -> uncurry (ReceivedGroupMessage c) <$> getGroupAndMember_ db entId c + ConnContact -> ReceivedDirectMessage c . Just <$> getContactRec_ db entId c + ConnSndFile -> SndFileConnection c <$> getConnSndFileTransfer_ db entId c + ConnRcvFile -> RcvFileConnection c <$> ExceptT (getRcvFileTransfer_ db userId entId) where getConnection_ :: DB.Connection -> ExceptT StoreError IO Connection getConnection_ db = ExceptT $ do @@ -532,7 +602,7 @@ getConnectionChatDirection st User {userId, userContactId} agentConnId = db [sql| SELECT connection_id, agent_conn_id, conn_level, via_contact, - conn_status, conn_type, contact_id, group_member_id, created_at + conn_status, conn_type, contact_id, group_member_id, snd_file_id, rcv_file_id, created_at FROM connections WHERE user_id = ? AND agent_conn_id = ? |] @@ -578,6 +648,22 @@ getConnectionChatDirection st User {userId, userContactId} agentConnId = let member = toGroupMember userContactId memberRow in Right (groupName, (member :: GroupMember) {activeConn = Just c}) toGroupAndMember _ _ = Left $ SEInternal "referenced group member not found" + getConnSndFileTransfer_ :: DB.Connection -> Int64 -> Connection -> ExceptT StoreError IO SndFileTransfer + getConnSndFileTransfer_ db fileId Connection {connId} = + ExceptT $ + sndFileTransfer_ fileId connId + <$> DB.query + db + [sql| + SELECT s.file_status, f.file_name, f.file_size, f.chunk_size, f.file_path + FROM snd_files s + JOIN files f USING (file_id) + WHERE f.user_id = ? AND f.file_id = ? AND s.connection_id = ? + |] + (userId, fileId, connId) + sndFileTransfer_ :: Int64 -> Int64 -> [(FileStatus, String, Integer, Integer, FilePath)] -> Either StoreError SndFileTransfer + sndFileTransfer_ fileId connId [(fileStatus, fileName, fileSize, chunkSize, filePath)] = Right SndFileTransfer {..} + sndFileTransfer_ fileId _ _ = Left $ SESndFileNotFound fileId updateConnectionStatus :: MonadUnliftIO m => SQLiteStore -> Connection -> ConnStatus -> m () updateConnectionStatus st Connection {connId} connStatus = @@ -655,7 +741,7 @@ getGroup_ db User {userId, userContactId} localDisplayName = do m.group_member_id, m.member_id, m.member_role, m.member_category, m.member_status, m.invited_by, m.local_display_name, m.contact_id, p.display_name, p.full_name, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, - c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.created_at + c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.created_at FROM group_members m JOIN contact_profiles p ON p.contact_profile_id = m.contact_profile_id LEFT JOIN connections c ON c.connection_id = ( @@ -1011,7 +1097,7 @@ getViaGroupMember st User {userId, userContactId} Contact {contactId} = m.group_member_id, m.member_id, m.member_role, m.member_category, m.member_status, m.invited_by, m.local_display_name, m.contact_id, p.display_name, p.full_name, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, - c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.created_at + c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.created_at FROM group_members m JOIN contacts ct ON ct.contact_id = m.contact_id JOIN contact_profiles p ON p.contact_profile_id = m.contact_profile_id @@ -1041,7 +1127,7 @@ getViaGroupContact st User {userId} GroupMember {groupMemberId} = SELECT ct.contact_id, ct.local_display_name, p.display_name, p.full_name, ct.via_group, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, - c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.created_at + c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.created_at FROM contacts ct JOIN contact_profiles p ON ct.contact_profile_id = p.contact_profile_id JOIN connections c ON c.connection_id = ( @@ -1062,6 +1148,225 @@ getViaGroupContact st User {userId} GroupMember {groupMemberId} = in Just Contact {contactId, localDisplayName, profile, activeConn, viaGroup} toContact _ = Nothing +createSndFileTransfer :: MonadUnliftIO m => SQLiteStore -> UserId -> Int64 -> FilePath -> FileInvitation -> ConnId -> Integer -> m SndFileTransfer +createSndFileTransfer st userId contactId filePath FileInvitation {fileName, fileSize} agentConnId chunkSize = + liftIO . withTransaction st $ \db -> do + DB.execute db "INSERT INTO files (user_id, contact_id, file_name, file_path, file_size, chunk_size) VALUES (?, ?, ?, ?, ?, ?)" (userId, contactId, fileName, filePath, fileSize, chunkSize) + fileId <- insertedRowId db + Connection {connId} <- createSndFileConnection_ db userId fileId agentConnId + let fileStatus = FSNew + DB.execute db "INSERT INTO snd_files (file_id, file_status, connection_id) VALUES (?, ?, ?)" (fileId, fileStatus, connId) + pure SndFileTransfer {..} + +createSndFileConnection_ :: DB.Connection -> UserId -> Int64 -> ConnId -> IO Connection +createSndFileConnection_ db userId fileId agentConnId = do + createdAt <- getCurrentTime + let connType = ConnSndFile + connStatus = ConnNew + DB.execute + db + [sql| + INSERT INTO connections + (user_id, snd_file_id, agent_conn_id, conn_status, conn_type, created_at) VALUES (?,?,?,?,?,?) + |] + (userId, fileId, agentConnId, connStatus, connType, createdAt) + connId <- insertedRowId db + pure Connection {connId, agentConnId, connType, entityId = Just fileId, viaContact = Nothing, connLevel = 0, connStatus, createdAt} + +updateSndFileStatus :: MonadUnliftIO m => SQLiteStore -> SndFileTransfer -> FileStatus -> m () +updateSndFileStatus st SndFileTransfer {fileId, connId} status = + liftIO . withTransaction st $ \db -> + DB.execute db "UPDATE snd_files SET file_status = ? WHERE file_id = ? AND connection_id = ?" (status, fileId, connId) + +createSndFileChunk :: MonadUnliftIO m => SQLiteStore -> SndFileTransfer -> m (Maybe Integer) +createSndFileChunk st SndFileTransfer {fileId, connId, fileSize, chunkSize} = + liftIO . withTransaction st $ \db -> do + chunkNo <- getLastChunkNo db + insertChunk db chunkNo + pure chunkNo + where + getLastChunkNo db = do + ns <- DB.query db "SELECT chunk_number FROM snd_file_chunks WHERE file_id = ? AND connection_id = ? AND chunk_sent = 1 ORDER BY chunk_number DESC LIMIT 1" (fileId, connId) + pure $ case map fromOnly ns of + [] -> Just 1 + n : _ -> if n * chunkSize >= fileSize then Nothing else Just (n + 1) + insertChunk db = \case + Just chunkNo -> DB.execute db "INSERT OR REPLACE INTO snd_file_chunks (file_id, connection_id, chunk_number) VALUES (?, ?, ?)" (fileId, connId, chunkNo) + Nothing -> pure () + +updateSndFileChunkMsg :: MonadUnliftIO m => SQLiteStore -> SndFileTransfer -> Integer -> AgentMsgId -> m () +updateSndFileChunkMsg st SndFileTransfer {fileId, connId} chunkNo msgId = + liftIO . withTransaction st $ \db -> + DB.execute + db + [sql| + UPDATE snd_file_chunks + SET chunk_agent_msg_id = ? + WHERE file_id = ? AND connection_id = ? AND chunk_number = ? + |] + (msgId, fileId, connId, chunkNo) + +updateSndFileChunkSent :: MonadUnliftIO m => SQLiteStore -> SndFileTransfer -> AgentMsgId -> m () +updateSndFileChunkSent st SndFileTransfer {fileId, connId} msgId = + liftIO . withTransaction st $ \db -> + DB.execute + db + [sql| + UPDATE snd_file_chunks + SET chunk_sent = 1 + WHERE file_id = ? AND connection_id = ? AND chunk_agent_msg_id = ? + |] + (fileId, connId, msgId) + +createRcvFileTransfer :: MonadUnliftIO m => SQLiteStore -> UserId -> Int64 -> FileInvitation -> Integer -> m RcvFileTransfer +createRcvFileTransfer st userId contactId f@FileInvitation {fileName, fileSize, fileQInfo} chunkSize = + liftIO . withTransaction st $ \db -> do + DB.execute db "INSERT INTO files (user_id, contact_id, file_name, file_size, chunk_size) VALUES (?, ?, ?, ?, ?)" (userId, contactId, fileName, fileSize, chunkSize) + fileId <- insertedRowId db + DB.execute db "INSERT INTO rcv_files (file_id, file_status, file_queue_info) VALUES (?, ?, ?)" (fileId, FSNew, fileQInfo) + pure RcvFileTransfer {fileId, fileInvitation = f, fileStatus = RFSNew, chunkSize} + +getRcvFileTransfer :: StoreMonad m => SQLiteStore -> UserId -> Int64 -> m RcvFileTransfer +getRcvFileTransfer st userId fileId = + liftIOEither . withTransaction st $ \db -> + getRcvFileTransfer_ db userId fileId + +getRcvFileTransfer_ :: DB.Connection -> UserId -> Int64 -> IO (Either StoreError RcvFileTransfer) +getRcvFileTransfer_ db userId fileId = + rcvFileTransfer + <$> DB.query + db + [sql| + SELECT r.file_status, r.file_queue_info, f.file_name, + f.file_size, f.chunk_size, f.file_path, 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 + WHERE f.user_id = ? AND f.file_id = ? + |] + (userId, fileId) + where + rcvFileTransfer :: + [(FileStatus, SMPQueueInfo, String, Integer, Integer, Maybe FilePath, Maybe Int64, Maybe ConnId)] -> + Either StoreError RcvFileTransfer + rcvFileTransfer [(fileStatus', fileQInfo, fileName, fileSize, chunkSize, filePath_, connId_, agentConnId_)] = + let fileInv = FileInvitation {fileName, fileSize, fileQInfo} + fileInfo = (filePath_, connId_, agentConnId_) + in case fileStatus' of + FSNew -> Right RcvFileTransfer {fileId, fileInvitation = fileInv, fileStatus = RFSNew, chunkSize} + FSAccepted -> ft fileInv RFSAccepted fileInfo + FSConnected -> ft fileInv RFSConnected fileInfo + FSComplete -> ft fileInv RFSComplete fileInfo + FSCancelled -> ft fileInv RFSCancelled fileInfo + where + ft fileInvitation rfs = \case + (Just filePath, Just connId, Just agentConnId) -> + let fileStatus = rfs RcvFileInfo {filePath, connId, agentConnId} + in Right RcvFileTransfer {..} + _ -> Left $ SERcvFileInvalid fileId + rcvFileTransfer _ = Left $ SERcvFileNotFound fileId + +acceptRcvFileTransfer :: StoreMonad m => SQLiteStore -> UserId -> Int64 -> ConnId -> FilePath -> m () +acceptRcvFileTransfer st userId fileId agentConnId filePath = + liftIO . withTransaction st $ \db -> do + DB.execute db "UPDATE files SET file_path = ? WHERE user_id = ? AND file_id = ?" (filePath, userId, fileId) + DB.execute db "UPDATE rcv_files SET file_status = ? WHERE file_id = ?" (FSAccepted, fileId) + + DB.execute db "INSERT INTO connections (agent_conn_id, conn_status, conn_type, rcv_file_id, user_id) VALUES (?, ?, ?, ?, ?)" (agentConnId, ConnJoined, ConnRcvFile, fileId, userId) + +updateRcvFileStatus :: MonadUnliftIO m => SQLiteStore -> RcvFileTransfer -> FileStatus -> m () +updateRcvFileStatus st RcvFileTransfer {fileId} status = + liftIO . withTransaction st $ \db -> + DB.execute db "UPDATE rcv_files SET file_status = ? WHERE file_id = ?" (status, fileId) + +createRcvFileChunk :: MonadUnliftIO m => SQLiteStore -> RcvFileTransfer -> Integer -> AgentMsgId -> m RcvChunkStatus +createRcvFileChunk st RcvFileTransfer {fileId, fileInvitation = FileInvitation {fileSize}, chunkSize} chunkNo msgId = + liftIO . withTransaction st $ \db -> do + status <- getLastChunkNo db + unless (status == RcvChunkError) $ + DB.execute db "INSERT OR REPLACE INTO rcv_file_chunks (file_id, chunk_number, chunk_agent_msg_id) VALUES (?, ?, ?)" (fileId, chunkNo, msgId) + pure status + where + getLastChunkNo db = do + ns <- DB.query db "SELECT chunk_number FROM rcv_file_chunks WHERE file_id = ? ORDER BY chunk_number DESC LIMIT 1" (Only fileId) + pure $ case map fromOnly ns of + [] -> if chunkNo == 1 then RcvChunkOk else RcvChunkError + n : _ + | chunkNo == n -> RcvChunkDuplicate + | chunkNo == n + 1 -> + let prevSize = n * chunkSize + in if prevSize >= fileSize + then RcvChunkError + else + if prevSize + chunkSize >= fileSize + then RcvChunkFinal + else RcvChunkOk + | otherwise -> RcvChunkError + +updatedRcvFileChunkStored :: MonadUnliftIO m => SQLiteStore -> RcvFileTransfer -> Integer -> m () +updatedRcvFileChunkStored st RcvFileTransfer {fileId} chunkNo = + liftIO . withTransaction st $ \db -> + DB.execute + db + [sql| + UPDATE rcv_file_chunks + SET chunk_stored = 1 + WHERE file_id = ? AND chunk_number = ? + |] + (fileId, chunkNo) + +getFileTransfer :: StoreMonad m => SQLiteStore -> UserId -> Int64 -> m FileTransfer +getFileTransfer st userId fileId = + liftIOEither . withTransaction st $ \db -> + getFileTransfer_ db userId fileId + +getFileTransferProgress :: StoreMonad m => SQLiteStore -> UserId -> Int64 -> m (FileTransfer, [Integer]) +getFileTransferProgress st userId fileId = + liftIOEither . withTransaction st $ \db -> runExceptT $ do + ft <- ExceptT $ getFileTransfer_ db userId fileId + liftIO $ + (ft,) . map fromOnly <$> case ft of + FTSnd _ -> DB.query db "SELECT COUNT(*) FROM snd_file_chunks WHERE file_id = ? and chunk_sent = 1 GROUP BY connection_id" (Only fileId) + FTRcv _ -> DB.query db "SELECT COUNT(*) FROM rcv_file_chunks WHERE file_id = ? AND chunk_stored = 1" (Only fileId) + +getFileTransfer_ :: DB.Connection -> UserId -> Int64 -> IO (Either StoreError FileTransfer) +getFileTransfer_ db userId fileId = + fileTransfer + =<< DB.query + db + [sql| + SELECT s.file_id, r.file_id + FROM files f + LEFT JOIN snd_files s ON s.file_id = f.file_id + LEFT JOIN rcv_files r ON r.file_id = f.file_id + WHERE user_id = ? AND f.file_id = ? + |] + (userId, fileId) + where + fileTransfer :: [(Maybe Int64, Maybe Int64)] -> IO (Either StoreError FileTransfer) + fileTransfer ((Just _, Nothing) : _) = FTSnd <$$> getSndFileTransfers_ db userId fileId + fileTransfer [(Nothing, Just _)] = FTRcv <$$> getRcvFileTransfer_ db userId fileId + fileTransfer _ = pure . Left $ SEFileNotFound fileId + +getSndFileTransfers_ :: DB.Connection -> UserId -> Int64 -> IO (Either StoreError [SndFileTransfer]) +getSndFileTransfers_ db userId fileId = + sndFileTransfers + <$> DB.query + db + [sql| + SELECT s.file_status, f.file_name, f.file_size, f.chunk_size, f.file_path, s.connection_id, c.agent_conn_id + FROM snd_files s + JOIN files f USING (file_id) + JOIN connections c USING (connection_id) + WHERE f.user_id = ? AND f.file_id = ? + |] + (userId, fileId) + where + sndFileTransfers :: [(FileStatus, String, Integer, Integer, FilePath, Int64, ConnId)] -> Either StoreError [SndFileTransfer] + sndFileTransfers [] = Left $ SESndFileNotFound fileId + sndFileTransfers fts = Right $ map sndFileTransfer fts + sndFileTransfer (fileStatus, fileName, fileSize, chunkSize, filePath, connId, agentConnId) = SndFileTransfer {..} + -- | Saves unique local display name based on passed displayName, suffixed with _N if required. -- This function should be called inside transaction. withLocalDisplayName :: forall a. DB.Connection -> UserId -> Text -> (Text -> IO a) -> IO (Either StoreError a) @@ -1126,6 +1431,10 @@ data StoreError | SEDuplicateGroupMember | SEGroupAlreadyJoined | SEGroupInvitationNotFound + | SESndFileNotFound Int64 + | SERcvFileNotFound Int64 + | SEFileNotFound Int64 + | SERcvFileInvalid Int64 | SEConnectionNotFound ConnId | SEIntroNotFound | SEUniqueID diff --git a/src/Simplex/Chat/Styled.hs b/src/Simplex/Chat/Styled.hs index 9bbb88d27..f7a3a80ac 100644 --- a/src/Simplex/Chat/Styled.hs +++ b/src/Simplex/Chat/Styled.hs @@ -7,6 +7,7 @@ module Simplex.Chat.Styled styleMarkdown, styleMarkdownText, sLength, + sShow, ) where @@ -54,6 +55,9 @@ instance StyledFormat Text where styled f = styled f . T.unpack plain = Styled [] . T.unpack +sShow :: Show a => a -> StyledString +sShow = plain . show + sgr :: Format -> [SGR] sgr = \case Bold -> [SetConsoleIntensity BoldIntensity] diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index 94708cdf9..b73deab15 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -299,6 +299,76 @@ serializeMemberStatus = \case GSMemComplete -> "complete" GSMemCreator -> "creator" +data SndFileTransfer = SndFileTransfer + { fileId :: Int64, + fileName :: String, + filePath :: String, + fileSize :: Integer, + chunkSize :: Integer, + connId :: Int64, + agentConnId :: ConnId, + fileStatus :: FileStatus + } + deriving (Eq, Show) + +data FileInvitation = FileInvitation + { fileName :: String, + fileSize :: Integer, + fileQInfo :: SMPQueueInfo + } + deriving (Eq, Show) + +data RcvFileTransfer = RcvFileTransfer + { fileId :: Int64, + fileInvitation :: FileInvitation, + fileStatus :: RcvFileStatus, + chunkSize :: Integer + } + deriving (Eq, Show) + +data RcvFileStatus + = RFSNew + | RFSAccepted RcvFileInfo + | RFSConnected RcvFileInfo + | RFSComplete RcvFileInfo + | RFSCancelled RcvFileInfo + deriving (Eq, Show) + +data RcvFileInfo = RcvFileInfo + { filePath :: FilePath, + connId :: Int64, + agentConnId :: ConnId + } + deriving (Eq, Show) + +data FileTransfer = FTSnd [SndFileTransfer] | FTRcv RcvFileTransfer + +data FileStatus = FSNew | FSAccepted | FSConnected | FSComplete | FSCancelled deriving (Eq, Show) + +instance FromField FileStatus where fromField = fromTextField_ fileStatusT + +instance ToField FileStatus where toField = toField . serializeFileStatus + +fileStatusT :: Text -> Maybe FileStatus +fileStatusT = \case + "new" -> Just FSNew + "accepted" -> Just FSAccepted + "connected" -> Just FSConnected + "complete" -> Just FSComplete + "cancelled" -> Just FSCancelled + _ -> Nothing + +serializeFileStatus :: FileStatus -> Text +serializeFileStatus = \case + FSNew -> "new" + FSAccepted -> "accepted" + FSConnected -> "connected" + FSComplete -> "complete" + FSCancelled -> "cancelled" + +data RcvChunkStatus = RcvChunkOk | RcvChunkFinal | RcvChunkDuplicate | RcvChunkError + deriving (Eq, Show) + data Connection = Connection { connId :: Int64, agentConnId :: ConnId, @@ -306,7 +376,7 @@ data Connection = Connection viaContact :: Maybe Int64, connType :: ConnType, connStatus :: ConnStatus, - entityId :: Maybe Int64, -- contact or group member ID + entityId :: Maybe Int64, -- contact, group member or file ID createdAt :: UTCTime } deriving (Eq, Show) @@ -353,7 +423,7 @@ serializeConnStatus = \case ConnReady -> "ready" ConnDeleted -> "deleted" -data ConnType = ConnContact | ConnMember +data ConnType = ConnContact | ConnMember | ConnSndFile | ConnRcvFile deriving (Eq, Show) instance FromField ConnType where fromField = fromTextField_ connTypeT @@ -364,12 +434,16 @@ connTypeT :: Text -> Maybe ConnType connTypeT = \case "contact" -> Just ConnContact "member" -> Just ConnMember + "snd_file" -> Just ConnSndFile + "rcv_file" -> Just ConnRcvFile _ -> Nothing serializeConnType :: ConnType -> Text serializeConnType = \case ConnContact -> "contact" ConnMember -> "member" + ConnSndFile -> "snd_file" + ConnRcvFile -> "rcv_file" data NewConnection = NewConnection { agentConnId :: ByteString, diff --git a/src/Simplex/Chat/Util.hs b/src/Simplex/Chat/Util.hs index d2fe0c3d4..05ea20cf8 100644 --- a/src/Simplex/Chat/Util.hs +++ b/src/Simplex/Chat/Util.hs @@ -8,3 +8,9 @@ safeDecodeUtf8 :: ByteString -> Text safeDecodeUtf8 = decodeUtf8With onError where onError _ _ = Just '?' + +ifM :: Monad m => m Bool -> m a -> m a -> m a +ifM ba t f = ba >>= \b -> if b then t else f + +unlessM :: Monad m => m Bool -> m () -> m () +unlessM b = ifM b $ pure () diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 608cac481..5139a94d9 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -24,6 +24,20 @@ module Simplex.Chat.View showReceivedGroupMessage, showSentMessage, showSentGroupMessage, + showSentFileInvitation, + showSndFileStart, + showSndFileComplete, + showSndFileCancelled, + showSndFileRcvCancelled, + showReceivedFileInvitation, + showRcvFileAccepted, + showRcvFileStart, + showRcvFileComplete, + showRcvFileCancelled, + showRcvFileSndCancelled, + showFileTransferStatus, + showSndFileSubError, + showRcvFileSubError, showGroupCreated, showGroupDeletedUser, showGroupDeleted, @@ -42,6 +56,7 @@ module Simplex.Chat.View showUserProfile, showUserProfileUpdated, showContactUpdated, + showMessageError, safeDecodeUtf8, ) where @@ -50,11 +65,13 @@ import Control.Monad.IO.Unlift import Control.Monad.Reader import Data.ByteString.Char8 (ByteString) import Data.Composition ((.:), (.:.)) +import Data.Int (Int64) import Data.Text (Text) import qualified Data.Text as T import Data.Time.Clock (DiffTime, UTCTime) import Data.Time.Format (defaultTimeLocale, formatTime) import Data.Time.LocalTime (TimeZone, ZonedTime, getCurrentTimeZone, getZonedTime, localDay, localTimeOfDay, timeOfDayToTime, utcToLocalTime, zonedTimeToLocalTime) +import Numeric (showFFloat) import Simplex.Chat.Controller import Simplex.Chat.Markdown import Simplex.Chat.Store (StoreError (..)) @@ -124,6 +141,48 @@ showSentGroupMessage = showSentMessage_ . ttyToGroup showSentMessage_ :: ChatReader m => StyledString -> ByteString -> m () showSentMessage_ to msg = printToView =<< liftIO (sentMessage to msg) +showSentFileInvitation :: ChatReader m => ContactName -> SndFileTransfer -> m () +showSentFileInvitation = printToView .: sentFileInvitation + +showSndFileStart :: ChatReader m => Int64 -> m () +showSndFileStart = printToView . sndFileStart + +showSndFileComplete :: ChatReader m => Int64 -> m () +showSndFileComplete = printToView . sndFileComplete + +showSndFileCancelled :: ChatReader m => Int64 -> m () +showSndFileCancelled = printToView . sndFileCancelled + +showSndFileRcvCancelled :: ChatReader m => Int64 -> m () +showSndFileRcvCancelled = printToView . sndFileRcvCancelled + +showReceivedFileInvitation :: ChatReader m => ContactName -> RcvFileTransfer -> m () +showReceivedFileInvitation = printToView .: receivedFileInvitation + +showRcvFileAccepted :: ChatReader m => Int64 -> FilePath -> m () +showRcvFileAccepted = printToView .: rcvFileAccepted + +showRcvFileStart :: ChatReader m => Int64 -> m () +showRcvFileStart = printToView . rcvFileStart + +showRcvFileComplete :: ChatReader m => Int64 -> m () +showRcvFileComplete = printToView . rcvFileComplete + +showRcvFileCancelled :: ChatReader m => Int64 -> m () +showRcvFileCancelled = printToView . rcvFileCancelled + +showRcvFileSndCancelled :: ChatReader m => Int64 -> m () +showRcvFileSndCancelled = printToView . rcvFileSndCancelled + +showFileTransferStatus :: ChatReader m => (FileTransfer, [Integer]) -> m () +showFileTransferStatus = printToView . fileTransferStatus + +showSndFileSubError :: ChatReader m => SndFileTransfer -> ChatError -> m () +showSndFileSubError = printToView .: sndFileSubError + +showRcvFileSubError :: ChatReader m => RcvFileTransfer -> ChatError -> m () +showRcvFileSubError = printToView .: rcvFileSubError + showGroupCreated :: ChatReader m => Group -> m () showGroupCreated = printToView . groupCreated @@ -178,6 +237,9 @@ showUserProfileUpdated = printToView .: userProfileUpdated showContactUpdated :: ChatReader m => Contact -> Contact -> m () showContactUpdated = printToView .: contactUpdated +showMessageError :: ChatReader m => Text -> Text -> m () +showMessageError = printToView .: messageError + invitation :: SMPQueueInfo -> [StyledString] invitation qInfo = [ "pass this invitation to your contact (via another channel): ", @@ -202,19 +264,19 @@ contactConnected :: Contact -> [StyledString] contactConnected ct = [ttyFullContact ct <> ": contact is connected"] contactDisconnected :: ContactName -> [StyledString] -contactDisconnected c = [ttyContact c <> ": contact is disconnected (messages will be queued)"] +contactDisconnected c = [ttyContact c <> ": disconnected from server (messages will be queued)"] contactAnotherClient :: ContactName -> [StyledString] contactAnotherClient c = [ttyContact c <> ": contact is connected to another client"] contactSubscribed :: ContactName -> [StyledString] -contactSubscribed c = [ttyContact c <> ": contact is active"] +contactSubscribed c = [ttyContact c <> ": connected to server"] contactSubError :: ContactName -> ChatError -> [StyledString] -contactSubError c e = [ttyContact c <> ": contact error " <> plain (show e)] +contactSubError c e = [ttyContact c <> ": contact error " <> sShow e] groupSubscribed :: GroupName -> [StyledString] -groupSubscribed g = [ttyGroup g <> ": group is active"] +groupSubscribed g = [ttyGroup g <> ": connected to server(s)"] groupEmpty :: GroupName -> [StyledString] groupEmpty g = [ttyGroup g <> ": group is empty"] @@ -223,7 +285,7 @@ groupRemoved :: GroupName -> [StyledString] groupRemoved g = [ttyGroup g <> ": you are no longer a member or group deleted"] memberSubError :: GroupName -> ContactName -> ChatError -> [StyledString] -memberSubError g c e = [ttyGroup g <> " member " <> ttyContact c <> " error: " <> plain (show e)] +memberSubError g c e = [ttyGroup g <> " member " <> ttyContact c <> " error: " <> sShow e] groupCreated :: Group -> [StyledString] groupCreated g@Group {localDisplayName} = @@ -317,7 +379,7 @@ contactsMerged _to@Contact {localDisplayName = c1} _from@Contact {localDisplayNa userProfile :: Profile -> [StyledString] userProfile Profile {displayName, fullName} = [ "user profile: " <> ttyFullName displayName fullName, - "use " <> highlight' "/p [ ]" <> " to change it", + "use " <> highlight' "/p []" <> " to change it", "(the updated profile will be sent to all your contacts)" ] @@ -344,6 +406,9 @@ contactUpdated where fullNameUpdate = if T.null fullName' || fullName' == n' then " removed full name" else " updated full name: " <> plain fullName' +messageError :: Text -> Text -> [StyledString] +messageError prefix err = [plain prefix <> ": " <> plain err] + receivedMessage :: StyledString -> UTCTime -> Text -> MsgIntegrity -> IO [StyledString] receivedMessage from utcTime msg mOk = do t <- formatUTCTime <$> getCurrentTimeZone <*> getZonedTime @@ -382,6 +447,90 @@ prependFirst s (s' : ss) = (s <> s') : ss msgPlain :: Text -> [StyledString] msgPlain = map styleMarkdownText . T.lines +sentFileInvitation :: ContactName -> SndFileTransfer -> [StyledString] +sentFileInvitation cName SndFileTransfer {fileId, fileName} = + [ "offered to send the file " <> plain fileName <> " to " <> ttyContact cName, + "use " <> highlight ("/fc " <> show fileId) <> " to cancel sending" + ] + +sndFileStart :: Int64 -> [StyledString] +sndFileStart fileId = ["started sending the file " <> sShow fileId] + +sndFileComplete :: Int64 -> [StyledString] +sndFileComplete fileId = ["completed sending the file " <> sShow fileId] + +sndFileCancelled :: Int64 -> [StyledString] +sndFileCancelled fileId = ["cancelled sending the file " <> sShow fileId] + +sndFileRcvCancelled :: Int64 -> [StyledString] +sndFileRcvCancelled fileId = ["recipient cancelled receiving the file " <> sShow fileId] + +receivedFileInvitation :: ContactName -> RcvFileTransfer -> [StyledString] +receivedFileInvitation c RcvFileTransfer {fileId, fileInvitation = FileInvitation {fileName, fileSize}} = + [ ttyContact c <> " wants to send you the file " <> plain fileName <> " (" <> humanReadableSize fileSize <> " / " <> sShow fileSize <> " bytes)", + "use " <> highlight ("/fr " <> show fileId <> " [/ | ]") <> " to receive it" + ] + +humanReadableSize :: Integer -> StyledString +humanReadableSize size + | size < kB = sShow size <> " bytes" + | size < mB = hrSize kB "KiB" + | size < gB = hrSize mB "MiB" + | otherwise = hrSize gB "GiB" + where + hrSize sB name = plain $ unwords [showFFloat (Just 1) (fromIntegral size / (fromIntegral sB :: Double)) "", name] + kB = 1024 + mB = kB * 1024 + gB = mB * 1024 + +rcvFileAccepted :: Int64 -> FilePath -> [StyledString] +rcvFileAccepted fileId filePath = ["saving file " <> sShow fileId <> " to " <> plain filePath] + +rcvFileStart :: Int64 -> [StyledString] +rcvFileStart fileId = ["started receiving the file " <> sShow fileId] + +rcvFileComplete :: Int64 -> [StyledString] +rcvFileComplete fileId = ["completed receiving the file " <> sShow fileId] + +rcvFileCancelled :: Int64 -> [StyledString] +rcvFileCancelled fileId = ["cancelled receiving the file " <> sShow fileId] + +rcvFileSndCancelled :: Int64 -> [StyledString] +rcvFileSndCancelled fileId = ["sender cancelled sending the file " <> sShow fileId] + +fileTransferStatus :: (FileTransfer, [Integer]) -> [StyledString] +fileTransferStatus (FTSnd [SndFileTransfer {fileStatus, fileSize, chunkSize}], chunksNum) = + ["sent file transfer " <> sndStatus] + where + sndStatus = case fileStatus of + FSNew -> "is not accepted yet" + FSAccepted -> "just started" + FSConnected -> "progress: " <> fileProgress chunksNum chunkSize fileSize + FSComplete -> "is complete" + FSCancelled -> "is cancelled" +fileTransferStatus (FTSnd _fts, _chunks) = [] -- TODO group transfer +fileTransferStatus (FTRcv RcvFileTransfer {fileId, fileInvitation = FileInvitation {fileSize}, fileStatus, chunkSize}, chunksNum) = + ["received file transfer " <> rcvStatus] + where + rcvStatus = case fileStatus of + RFSNew -> "is not accepted yet, use " <> highlight ("/fr " <> show fileId) <> " to receive file" + RFSAccepted _ -> "just started" + RFSConnected _ -> "progress: " <> fileProgress chunksNum chunkSize fileSize + RFSComplete RcvFileInfo {filePath} -> "is complete, path: " <> plain filePath + RFSCancelled RcvFileInfo {filePath} -> "is cancelled, received part path: " <> plain filePath + +fileProgress :: [Integer] -> Integer -> Integer -> StyledString +fileProgress chunksNum chunkSize fileSize = + sShow (sum chunksNum * chunkSize * 100 `div` fileSize) <> "% of " <> humanReadableSize fileSize + +sndFileSubError :: SndFileTransfer -> ChatError -> [StyledString] +sndFileSubError SndFileTransfer {fileId, fileName} e = + ["sent file " <> sShow fileId <> " (" <> plain fileName <> ") error: " <> sShow e] + +rcvFileSubError :: RcvFileTransfer -> ChatError -> [StyledString] +rcvFileSubError RcvFileTransfer {fileId, fileInvitation = FileInvitation {fileName}} e = + ["received file " <> sShow fileId <> " (" <> plain fileName <> ") error: " <> sShow e] + chatError :: ChatError -> [StyledString] chatError = \case ChatError err -> case err of @@ -394,16 +543,29 @@ chatError = \case CEGroupMemberUserRemoved -> ["you are no longer the member of the group"] CEGroupMemberNotFound c -> ["contact " <> ttyContact c <> " is not a group member"] CEGroupInternal s -> ["chat group bug: " <> plain s] - -- e -> ["chat error: " <> plain (show e)] + CEFileNotFound f -> ["file not found: " <> plain f] + CEFileAlreadyReceiving f -> ["file is already accepted: " <> plain f] + CEFileAlreadyExists f -> ["file already exists: " <> plain f] + CEFileRead f e -> ["cannot read file " <> plain f, sShow e] + CEFileWrite f e -> ["cannot write file " <> plain f, sShow e] + CEFileSend fileId e -> ["error sending file " <> sShow fileId <> ": " <> sShow e] + CEFileRcvChunk e -> ["error receiving file: " <> plain e] + CEFileInternal e -> ["file error: " <> plain e] + -- e -> ["chat error: " <> sShow e] ChatErrorStore err -> case err of SEDuplicateName -> ["this display name is already used by user, contact or group"] SEContactNotFound c -> ["no contact " <> ttyContact c] SEContactNotReady c -> ["contact " <> ttyContact c <> " is not active yet"] SEGroupNotFound g -> ["no group " <> ttyGroup g] SEGroupAlreadyJoined -> ["you already joined this group"] - e -> ["chat db error: " <> plain (show e)] - ChatErrorAgent e -> ["smp agent error: " <> plain (show e)] - ChatErrorMessage e -> ["chat message error: " <> plain (show e)] + SEFileNotFound fileId -> fileNotFound fileId + SESndFileNotFound fileId -> fileNotFound fileId + SERcvFileNotFound fileId -> fileNotFound fileId + e -> ["chat db error: " <> sShow e] + ChatErrorAgent e -> ["smp agent error: " <> sShow e] + ChatErrorMessage e -> ["chat message error: " <> sShow e] + where + fileNotFound fileId = ["file " <> sShow fileId <> " not found"] printToView :: (MonadUnliftIO m, MonadReader ChatController m) => [StyledString] -> m () printToView s = asks chatTerminal >>= liftIO . (`printToTerminal` s) diff --git a/src/Simplex/Chat/protocol.md b/src/Simplex/Chat/protocol.md index bd761ec46..111319c59 100644 --- a/src/Simplex/Chat/protocol.md +++ b/src/Simplex/Chat/protocol.md @@ -65,6 +65,7 @@ refMsgHash = 16*16(OCTET) ; SHA256 of agent message body ' x.grp.mem.inv 23456,234 x.text:NNN ' ' x.grp.mem.req 23456,123 x.json:NNN {...} ' ' x.grp.mem.direct.inv 23456,234 x.text:NNN ' +' x.file name,size x.text:NNN ' ``` ### Group protocol diff --git a/stack.yaml b/stack.yaml index 2d17c5d3a..9a58b8394 100644 --- a/stack.yaml +++ b/stack.yaml @@ -43,8 +43,7 @@ extra-deps: # - simplexmq-0.3.1@sha256:f247aaff3c16c5d3974a4ab4d5882ab50ac78073110997c0bceb05a74d10a325,6688 # - ../simplexmq - github: simplex-chat/simplexmq - commit: dd5137c336d5525c38b068d7212964b4ab196a33 - # this commit is in PR #164 + commit: 2ac903a2dd37c11a8612b19cd132cf43fe771fe4 # # extra-deps: [] diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs index 5b266e8c7..eff8ff6c1 100644 --- a/tests/ChatClient.hs +++ b/tests/ChatClient.hs @@ -15,7 +15,7 @@ import Control.Monad.Except import Data.List (dropWhileEnd) import Network.Socket import Simplex.Chat -import Simplex.Chat.Controller (ChatController (..)) +import Simplex.Chat.Controller (ChatConfig (..), ChatController (..)) import Simplex.Chat.Options import Simplex.Chat.Store import Simplex.Chat.Types (Profile) @@ -145,9 +145,11 @@ serverCfg = ServerConfig { transports = [(serverPort, transport @TCP)], tbqSize = 1, + msgQueueQuota = 4, queueIdBytes = 12, msgIdBytes = 6, storeLog = Nothing, + blockSize = 4096, serverPrivateKey = -- full RSA private key (only for tests) "MIIFIwIBAAKCAQEArZyrri/NAwt5buvYjwu+B/MQeJUszDBpRgVqNddlI9kNwDXu\ diff --git a/tests/ChatTests.hs b/tests/ChatTests.hs index ad5c4ff08..0afdf62ef 100644 --- a/tests/ChatTests.hs +++ b/tests/ChatTests.hs @@ -7,10 +7,13 @@ module ChatTests where import ChatClient import Control.Concurrent.Async (concurrently_) import Control.Concurrent.STM +import qualified Data.ByteString as B import Data.Char (isDigit) import qualified Data.Text as T import Simplex.Chat.Controller import Simplex.Chat.Types (Profile (..), User (..)) +import Simplex.Chat.Util (unlessM) +import System.Directory (doesFileExist) import System.Timeout (timeout) import Test.Hspec @@ -37,6 +40,10 @@ chatTests = do it "remove contact from group and add again" testGroupRemoveAdd describe "user profiles" $ it "update user profiles and notify contacts" testUpdateProfile + describe "sending and receiving files" $ do + it "send and receive file" testFileTransfer + it "sender cancelled file transfer" testFileSndCancel + it "recipient cancelled file transfer" testFileRcvCancel testAddContact :: IO () testAddContact = @@ -359,7 +366,7 @@ testUpdateProfile = createGroup3 "team" alice bob cath alice ##> "/p" alice <## "user profile: alice (Alice)" - alice <## "use /p [ ] to change it" + alice <## "use /p [] to change it" alice <## "(the updated profile will be sent to all your contacts)" alice ##> "/p alice" concurrentlyN_ @@ -394,6 +401,87 @@ testUpdateProfile = bob <## "use @cat to send messages" ] +testFileTransfer :: IO () +testFileTransfer = + testChat2 aliceProfile bobProfile $ + \alice bob -> do + connectUsers alice bob + startFileTransfer alice bob + concurrentlyN_ + [ do + bob #> "@alice receiving here..." + bob <## "completed receiving the file 1", + do + alice <# "bob> receiving here..." + alice <## "completed sending the file 1" + ] + src <- B.readFile "./tests/fixtures/test.jpg" + dest <- B.readFile "./tests/tmp/test.jpg" + dest `shouldBe` src + +testFileSndCancel :: IO () +testFileSndCancel = + testChat2 aliceProfile bobProfile $ + \alice bob -> do + connectUsers alice bob + startFileTransfer alice bob + alice ##> "/fc 1" + concurrentlyN_ + [ do + alice <## "cancelled sending the file 1" + alice ##> "/fs 1" + alice <## "sent file transfer is cancelled", + do + bob <## "sender cancelled sending the file 1" + bob ##> "/fs 1" + bob <## "received file transfer is cancelled, received part path: ./tests/tmp/test.jpg" + ] + checkPartialTransfer + +testFileRcvCancel :: IO () +testFileRcvCancel = + testChat2 aliceProfile bobProfile $ + \alice bob -> do + connectUsers alice bob + startFileTransfer alice bob + bob ##> "/fs 1" + getTermLine bob >>= (`shouldStartWith` "received file transfer progress:") + waitFileExists "./tests/tmp/test.jpg" + bob ##> "/fc 1" + concurrentlyN_ + [ do + bob <## "cancelled receiving the file 1" + bob ##> "/fs 1" + bob <## "received file transfer is cancelled, received part path: ./tests/tmp/test.jpg", + do + alice <## "recipient cancelled receiving the file 1" + alice ##> "/fs 1" + alice <## "sent file transfer is cancelled" + ] + checkPartialTransfer + where + waitFileExists f = unlessM (doesFileExist f) $ waitFileExists f + +startFileTransfer :: TestCC -> TestCC -> IO () +startFileTransfer alice bob = do + alice ##> "/f bob ./tests/fixtures/test.jpg" + alice <## "offered to send the file test.jpg to bob" + alice <## "use /fc 1 to cancel sending" + bob <## "alice wants to send you the file test.jpg (136.5 KiB / 139737 bytes)" + bob <## "use /fr 1 [/ | ] to receive it" + bob ##> "/fr 1 ./tests/tmp" + bob <## "saving file 1 to ./tests/tmp/test.jpg" + concurrently_ + (bob <## "started receiving the file 1") + (alice <## "started sending the file 1") + +checkPartialTransfer :: IO () +checkPartialTransfer = do + src <- B.readFile "./tests/fixtures/test.jpg" + dest <- B.readFile "./tests/tmp/test.jpg" + B.unpack src `shouldStartWith` B.unpack dest + B.length src > B.length dest `shouldBe` True + connectUsers :: TestCC -> TestCC -> IO () connectUsers cc1 cc2 = do name1 <- showName cc1 diff --git a/tests/fixtures/test.jpg b/tests/fixtures/test.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4a110305929671604423569157d7186833a66a21 GIT binary patch literal 139737 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%0ASxmtoW?Puw9xdMstbQzTX@oL= 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%0AWNS!-PIa*YQMXv6Ii0nm5&J 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