From fb03a119ea98ba3d1a5d6ebeb383f8f81ca5eb0f Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Fri, 14 Oct 2022 13:06:33 +0100 Subject: [PATCH] core: support inline file transfers (#1187) * core: support inline file transfers * parameterize ChatMessage * send files inline when accepted * accept inline file transfers (almost works) * db error SERcvFileInvalid * inline file transfer works (TODO fix test) * inline file transfer tests, change encodings * fixture * combine messages into x.file.acpt.inv, refactor * inline file mode * decide whether to receive file inline on the recipient side, not only via file invitation * test inline files "sent" mode * check that file was offered inline * update schema * enable encryption tests * test name Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com> * fix the list of rcv files to subscribe too Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com> --- simplex-chat.cabal | 1 + src/Simplex/Chat.hs | 709 ++++++++++-------- src/Simplex/Chat/Controller.hs | 17 + src/Simplex/Chat/Messages.hs | 10 +- .../Chat/Migrations/M20221012_inline_files.hs | 20 + src/Simplex/Chat/Migrations/chat_schema.sql | 17 +- src/Simplex/Chat/Protocol.hs | 418 +++++++---- src/Simplex/Chat/Store.hs | 343 +++++---- src/Simplex/Chat/Types.hs | 42 +- src/Simplex/Chat/View.hs | 47 +- tests/ChatClient.hs | 7 +- tests/ChatTests.hs | 495 +++++++----- tests/ProtocolTests.hs | 29 +- tests/fixtures/test.pdf | Bin 0 -> 272376 bytes 14 files changed, 1341 insertions(+), 814 deletions(-) create mode 100644 src/Simplex/Chat/Migrations/M20221012_inline_files.hs create mode 100644 tests/fixtures/test.pdf diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 63ab7ce9c8..7a29db609b 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -55,6 +55,7 @@ library Simplex.Chat.Migrations.M20221003_delete_broken_integrity_error_chat_items Simplex.Chat.Migrations.M20221004_idx_msg_deliveries_message_id Simplex.Chat.Migrations.M20221011_user_contact_links_group_id + Simplex.Chat.Migrations.M20221012_inline_files Simplex.Chat.Mobile Simplex.Chat.Options Simplex.Chat.ProfileGenerator diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index f7acce53c5..ad008f4189 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -3,6 +3,7 @@ {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE LambdaCase #-} +{-# LANGUAGE MultiWayIf #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-} @@ -22,7 +23,7 @@ import Crypto.Random (drgNew) import qualified Data.Aeson as J import Data.Attoparsec.ByteString.Char8 (Parser) import qualified Data.Attoparsec.ByteString.Char8 as A -import Data.Bifunctor (first) +import Data.Bifunctor (bimap, first) import qualified Data.ByteString.Base64 as B64 import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B @@ -43,7 +44,6 @@ import Data.Time (NominalDiffTime, addUTCTime) import Data.Time.Clock (UTCTime, diffUTCTime, getCurrentTime, nominalDiffTimeToSeconds) import Data.Time.Clock.System (SystemTime, systemToUTCTime) import Data.Time.LocalTime (getCurrentTimeZone, getZonedTime) -import Data.Word (Word32) import qualified Database.SQLite.Simple as DB import Simplex.Chat.Archive import Simplex.Chat.Call @@ -98,7 +98,8 @@ defaultChatConfig = netCfg = defaultNetworkConfig }, tbqSize = 64, - fileChunkSize = 15780, + fileChunkSize = 15780, -- do not change + inlineFiles = defaultInlineFilesConfig, subscriptionConcurrency = 16, subscriptionEvents = False, hostEvents = False, @@ -282,31 +283,33 @@ processChatCommand = \case APISendMessage (ChatRef cType chatId) (ComposedMessage file_ quotedItemId_ mc) -> withUser $ \user@User {userId} -> withChatLock $ case cType of CTDirect -> do ct@Contact {localDisplayName = c} <- withStore $ \db -> getContact db userId chatId - (fileInvitation_, ciFile_) <- unzipMaybe <$> setupSndFileTransfer ct + (fileInvitation_, ciFile_, ft_) <- unzipMaybe3 <$> setupSndFileTransfer ct (msgContainer, quotedItem_) <- prepareMsg fileInvitation_ - msg <- sendDirectContactMessage ct (XMsgNew msgContainer) + (msg@SndMessage {sharedMsgId}, _) <- sendDirectContactMessage ct (XMsgNew msgContainer) + case ft_ of + Just ft@FileTransferMeta {fileInline = Just IFMSent} -> + sendDirectFileInline ct ft sharedMsgId + _ -> pure () ci <- saveSndChatItem user (CDDirectSnd ct) msg (CISndMsgContent mc) ciFile_ quotedItem_ setActive $ ActiveC c pure . CRNewChatItem $ AChatItem SCTDirect SMDSnd (DirectChat ct) ci where - -- This method creates file invitation without connection request - it has to be accepted with x.acpt.file.inv message sent back to the contact - -- setupSndFileTransfer' :: Contact -> m (Maybe (FileInvitation, CIFile 'MDSnd)) - -- setupSndFileTransfer' ct = forM file_ $ \file -> do - -- (fileSize, chSize) <- checkSndFile file - -- let fileName = takeFileName file - -- fileInvitation = FileInvitation {fileName, fileSize, fileConnReq = Nothing} - -- fileId <- withStore' $ \db -> createSndDirectFileTransfer db userId ct file fileInvitation chSize - -- let ciFile = CIFile {fileId, fileName, fileSize, filePath = Just file, fileStatus = CIFSSndStored} - -- pure (fileInvitation, ciFile) - setupSndFileTransfer :: Contact -> m (Maybe (FileInvitation, CIFile 'MDSnd)) + setupSndFileTransfer :: Contact -> m (Maybe (FileInvitation, CIFile 'MDSnd, FileTransferMeta)) setupSndFileTransfer ct = forM file_ $ \file -> do - (fileSize, chSize) <- checkSndFile file - (agentConnId, fileConnReq) <- withAgent $ \a -> createConnection a True SCMInvitation + (fileSize, chSize, fileInline) <- checkSndFile file 1 + (agentConnId_, fileConnReq) <- + if isJust fileInline + then pure (Nothing, Nothing) + else bimap Just Just <$> withAgent (\a -> createConnection a True SCMInvitation) let fileName = takeFileName file - fileInvitation = FileInvitation {fileName, fileSize, fileConnReq = Just fileConnReq} - fileId <- withStore' $ \db -> createSndFileTransfer db userId ct file fileInvitation agentConnId chSize - let ciFile = CIFile {fileId, fileName, fileSize, filePath = Just file, fileStatus = CIFSSndStored} - pure (fileInvitation, ciFile) + fileInvitation = FileInvitation {fileName, fileSize, fileConnReq, fileInline} + withStore' $ \db -> do + ft@FileTransferMeta {fileId} <- createSndDirectFileTransfer db userId ct file fileInvitation agentConnId_ chSize + fileStatus <- case fileInline of + Just IFMSent -> createSndDirectInlineFT db ct ft $> CIFSSndTransfer + _ -> pure CIFSSndStored + let ciFile = CIFile {fileId, fileName, fileSize, filePath = Just file, fileStatus} + pure (fileInvitation, ciFile, ft) prepareMsg :: Maybe FileInvitation -> m (MsgContainer, Maybe (CIQuote 'CTDirect)) prepareMsg fileInvitation_ = case quotedItemId_ of Nothing -> pure (MCSimple (ExtMsgContent mc fileInvitation_), Nothing) @@ -326,21 +329,32 @@ processChatCommand = \case CTGroup -> do Group gInfo@GroupInfo {membership, localDisplayName = gName} ms <- withStore $ \db -> getGroup db user chatId unless (memberActive membership) $ throwChatError CEGroupMemberUserRemoved - (fileInvitation_, ciFile_) <- unzipMaybe <$> setupSndFileTransfer gInfo + (fileInvitation_, ciFile_, ft_) <- unzipMaybe3 <$> setupSndFileTransfer gInfo (length ms) (msgContainer, quotedItem_) <- prepareMsg fileInvitation_ membership - msg <- sendGroupMessage gInfo ms (XMsgNew msgContainer) + msg@SndMessage {sharedMsgId} <- sendGroupMessage gInfo ms (XMsgNew msgContainer) + mapM_ (sendGroupFileInline ms sharedMsgId) ft_ ci <- saveSndChatItem user (CDGroupSnd gInfo) msg (CISndMsgContent mc) ciFile_ quotedItem_ setActive $ ActiveG gName pure . CRNewChatItem $ AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci where - setupSndFileTransfer :: GroupInfo -> m (Maybe (FileInvitation, CIFile 'MDSnd)) - setupSndFileTransfer gInfo = forM file_ $ \file -> do - (fileSize, chSize) <- checkSndFile file + setupSndFileTransfer :: GroupInfo -> Int -> m (Maybe (FileInvitation, CIFile 'MDSnd, FileTransferMeta)) + setupSndFileTransfer gInfo n = forM file_ $ \file -> do + (fileSize, chSize, fileInline) <- checkSndFile file $ fromIntegral n let fileName = takeFileName file - fileInvitation = FileInvitation {fileName, fileSize, fileConnReq = Nothing} - fileId <- withStore' $ \db -> createSndGroupFileTransfer db userId gInfo file fileInvitation chSize - let ciFile = CIFile {fileId, fileName, fileSize, filePath = Just file, fileStatus = CIFSSndStored} - pure (fileInvitation, ciFile) + fileInvitation = FileInvitation {fileName, fileSize, fileConnReq = Nothing, fileInline} + fileStatus = if fileInline == Just IFMSent then CIFSSndTransfer else CIFSSndStored + withStore' $ \db -> do + ft@FileTransferMeta {fileId} <- createSndGroupFileTransfer db userId gInfo file fileInvitation chSize + let ciFile = CIFile {fileId, fileName, fileSize, filePath = Just file, fileStatus} + pure (fileInvitation, ciFile, ft) + sendGroupFileInline :: [GroupMember] -> SharedMsgId -> FileTransferMeta -> m () + sendGroupFileInline ms sharedMsgId ft@FileTransferMeta {fileInline} = + when (fileInline == Just IFMSent) . forM_ ms $ \case + m@GroupMember {activeConn = Just conn@Connection {connStatus}} -> + when (connStatus == ConnReady || connStatus == ConnSndReady) $ do + void . withStore' $ \db -> createSndGroupInlineFT db m conn ft + sendMemberFileInline m conn ft sharedMsgId + _ -> pure () prepareMsg :: Maybe FileInvitation -> GroupMember -> m (MsgContainer, Maybe (CIQuote 'CTGroup)) prepareMsg fileInvitation_ membership = case quotedItemId_ of Nothing -> pure (MCSimple (ExtMsgContent mc fileInvitation_), Nothing) @@ -364,9 +378,9 @@ processChatCommand = \case quoteContent qmc ciFile_ | replaceContent = MCText qTextOrFile | otherwise = case qmc of - MCImage _ image -> MCImage qTextOrFile image - MCFile _ -> MCFile qTextOrFile - _ -> qmc + MCImage _ image -> MCImage qTextOrFile image + MCFile _ -> MCFile qTextOrFile + _ -> qmc where -- if the message we're quoting with is one of the "large" MsgContents -- we replace the quote's content with MCText @@ -379,8 +393,9 @@ processChatCommand = \case qText = msgContentText qmc qFileName = maybe qText (T.pack . (fileName :: CIFile d -> String)) ciFile_ qTextOrFile = if T.null qText then qFileName else qText - unzipMaybe :: Maybe (a, b) -> (Maybe a, Maybe b) - unzipMaybe t = (fst <$> t, snd <$> t) + unzipMaybe3 :: Maybe (a, b, c) -> (Maybe a, Maybe b, Maybe c) + unzipMaybe3 (Just (a, b, c)) = (Just a, Just b, Just c) + unzipMaybe3 _ = (Nothing, Nothing, Nothing) APIUpdateChatItem (ChatRef cType chatId) itemId mc -> withUser $ \user@User {userId} -> withChatLock $ case cType of CTDirect -> do (ct@Contact {contactId, localDisplayName = c}, ci) <- withStore $ \db -> (,) <$> getContact db userId chatId <*> getDirectChatItem db userId chatId itemId @@ -388,7 +403,7 @@ processChatCommand = \case CChatItem SMDSnd ChatItem {meta = CIMeta {itemSharedMsgId}, content = ciContent} -> do case (ciContent, itemSharedMsgId) of (CISndMsgContent _, Just itemSharedMId) -> do - SndMessage {msgId} <- sendDirectContactMessage ct (XMsgUpdate itemSharedMId mc) + (SndMessage {msgId}, _) <- sendDirectContactMessage ct (XMsgUpdate itemSharedMId mc) updCi <- withStore $ \db -> updateDirectChatItem db userId contactId itemId (CISndMsgContent mc) $ Just msgId setActive $ ActiveC c pure . CRChatItemUpdated $ AChatItem SCTDirect SMDSnd (DirectChat ct) updCi @@ -544,7 +559,7 @@ processChatCommand = \case dhKeyPair <- if encryptedCall callType then Just <$> liftIO C.generateKeyPair' else pure Nothing let invitation = CallInvitation {callType, callDhPubKey = fst <$> dhKeyPair} callState = CallInvitationSent {localCallType = callType, localDhPrivKey = snd <$> dhKeyPair} - msg <- sendDirectContactMessage ct (XCallInv callId invitation) + (msg, _) <- sendDirectContactMessage ct (XCallInv callId invitation) ci <- saveSndChatItem user (CDDirectSnd ct) msg (CISndCall CISCallPending 0) Nothing Nothing let call' = Call {contactId, callId, chatItemId = chatItemId' ci, callState, callTs = chatItemTs' ci} call_ <- atomically $ TM.lookupInsert contactId call' calls @@ -570,7 +585,7 @@ processChatCommand = \case offer = CallOffer {callType, rtcSession, callDhPubKey} callState' = CallOfferSent {localCallType = callType, peerCallType, localCallSession = rtcSession, sharedKey} aciContent = ACIContent SMDRcv $ CIRcvCall CISCallAccepted 0 - SndMessage {msgId} <- sendDirectContactMessage ct (XCallOffer callId offer) + (SndMessage {msgId}, _) <- sendDirectContactMessage ct (XCallOffer callId offer) withStore' $ \db -> updateDirectChatItemsRead db contactId $ Just (chatItemId, chatItemId) updateDirectChatItemView userId ct chatItemId aciContent $ Just msgId pure $ Just call {callState = callState'} @@ -581,7 +596,7 @@ processChatCommand = \case CallOfferReceived {localCallType, peerCallType, peerCallSession, sharedKey} -> do let callState' = CallNegotiated {localCallType, peerCallType, localCallSession = rtcSession, peerCallSession, sharedKey} aciContent = ACIContent SMDSnd $ CISndCall CISCallNegotiated 0 - SndMessage {msgId} <- sendDirectContactMessage ct (XCallAnswer callId CallAnswer {rtcSession}) + (SndMessage {msgId}, _) <- sendDirectContactMessage ct (XCallAnswer callId CallAnswer {rtcSession}) updateDirectChatItemView userId ct chatItemId aciContent $ Just msgId pure $ Just call {callState = callState'} _ -> throwChatError . CECallState $ callStateTag callState @@ -590,19 +605,19 @@ processChatCommand = \case withCurrentCall contactId $ \_ ct call@Call {callId, callState} -> case callState of CallOfferSent {localCallType, peerCallType, localCallSession, sharedKey} -> do -- TODO update the list of ice servers in localCallSession - _ <- sendDirectContactMessage ct (XCallExtra callId CallExtraInfo {rtcExtraInfo}) + void . sendDirectContactMessage ct $ XCallExtra callId CallExtraInfo {rtcExtraInfo} let callState' = CallOfferSent {localCallType, peerCallType, localCallSession, sharedKey} pure $ Just call {callState = callState'} CallNegotiated {localCallType, peerCallType, localCallSession, peerCallSession, sharedKey} -> do -- TODO update the list of ice servers in localCallSession - _ <- sendDirectContactMessage ct (XCallExtra callId CallExtraInfo {rtcExtraInfo}) + void . sendDirectContactMessage ct $ XCallExtra callId CallExtraInfo {rtcExtraInfo} let callState' = CallNegotiated {localCallType, peerCallType, localCallSession, peerCallSession, sharedKey} pure $ Just call {callState = callState'} _ -> throwChatError . CECallState $ callStateTag callState APIEndCall contactId -> -- any call party withCurrentCall contactId $ \userId ct call@Call {callId} -> do - SndMessage {msgId} <- sendDirectContactMessage ct (XCallEnd callId) + (SndMessage {msgId}, _) <- sendDirectContactMessage ct (XCallEnd callId) updateCallItemStatus userId ct call WCSDisconnected $ Just msgId pure Nothing APIGetCallInvitations -> withUser $ \User {userId} -> do @@ -769,7 +784,7 @@ processChatCommand = \case forM_ cts $ \ct -> void ( do - sndMsg <- sendDirectContactMessage ct (XMsgNew $ MCSimple (ExtMsgContent mc Nothing)) + (sndMsg, _) <- sendDirectContactMessage ct (XMsgNew $ MCSimple (ExtMsgContent mc Nothing)) saveSndChatItem user (CDDirectSnd ct) sndMsg (CISndMsgContent mc) Nothing Nothing ) `catchError` (toView . CRChatError) @@ -815,9 +830,9 @@ processChatCommand = \case pure $ CRSentGroupInvitation gInfo contact member Just member@GroupMember {groupMemberId, memberStatus} | memberStatus == GSMemInvited -> - withStore' (\db -> getMemberInvitation db user groupMemberId) >>= \case - Just cReq -> sendInvitation member cReq $> CRSentGroupInvitation gInfo contact member - Nothing -> throwChatError $ CEGroupCantResendInvitation gInfo cName + withStore' (\db -> getMemberInvitation db user groupMemberId) >>= \case + Just cReq -> sendInvitation member cReq $> CRSentGroupInvitation gInfo contact member + Nothing -> throwChatError $ CEGroupCantResendInvitation gInfo cName | otherwise -> throwChatError $ CEGroupDuplicateMember cName APIJoinGroup groupId -> withUser $ \user@User {userId} -> do ReceivedGroupInvitation {fromMember, connRequest, groupInfo = g@GroupInfo {membership}} <- withStore $ \db -> getGroupInvitation db user groupId @@ -998,15 +1013,14 @@ processChatCommand = \case unless cancelled $ do cancelSndFile user ftm fts sharedMsgId <- withStore $ \db -> getSharedMsgIdByFileId db userId fileId - void $ - withStore (\db -> getChatRefByFileId db user fileId) >>= \case - ChatRef CTDirect contactId -> do - contact <- withStore $ \db -> getContact db userId contactId - sendDirectContactMessage contact $ XFileCancel sharedMsgId - ChatRef CTGroup groupId -> do - Group gInfo ms <- withStore $ \db -> getGroup db user groupId - sendGroupMessage gInfo ms $ XFileCancel sharedMsgId - _ -> throwChatError $ CEFileInternal "invalid chat ref for file transfer" + withStore (\db -> getChatRefByFileId db user fileId) >>= \case + ChatRef CTDirect contactId -> do + contact <- withStore $ \db -> getContact db userId contactId + void . sendDirectContactMessage contact $ XFileCancel sharedMsgId + ChatRef CTGroup groupId -> do + Group gInfo ms <- withStore $ \db -> getGroup db user groupId + void . sendGroupMessage gInfo ms $ XFileCancel sharedMsgId + _ -> throwChatError $ CEFileInternal "invalid chat ref for file transfer" ci <- withStore $ \db -> getChatItemByFileId db user fileId pure $ CRSndGroupFileCancelled ci ftm fts FTRcv ftr@RcvFileTransfer {cancelled} -> do @@ -1081,26 +1095,33 @@ processChatCommand = \case contactMember Contact {contactId} = find $ \GroupMember {memberContactId = cId, memberStatus = s} -> cId == Just contactId && s /= GSMemRemoved && s /= GSMemLeft - checkSndFile :: FilePath -> m (Integer, Integer) - checkSndFile f = do + checkSndFile :: FilePath -> Integer -> m (Integer, Integer, Maybe InlineFileMode) + checkSndFile f n = do fsFilePath <- toFSFilePath f unlessM (doesFileExist fsFilePath) . throwChatError $ CEFileNotFound f - (,) <$> getFileSize fsFilePath <*> asks (fileChunkSize . config) + ChatConfig {fileChunkSize, inlineFiles} <- asks config + fileSize <- getFileSize fsFilePath + let chunks = -((-fileSize) `div` fileChunkSize) + pure (fileSize, fileChunkSize, inlineFileMode inlineFiles chunks n) + inlineFileMode InlineFilesConfig {offerChunks, sendChunks, totalSendChunks} chunks n + | chunks > offerChunks = Nothing + | chunks > sendChunks || chunks * n > totalSendChunks = Just IFMOffer + | otherwise = Just IFMSent updateProfile :: User -> Profile -> m ChatResponse updateProfile user@User {profile = p@LocalProfile {profileId, localAlias}} p'@Profile {displayName} | p' == fromLocalProfile p = pure CRUserProfileNoChange | otherwise = do - withStore $ \db -> updateUserProfile db user p' - let user' = (user :: User) {localDisplayName = displayName, profile = toLocalProfile profileId p' localAlias} - asks currentUser >>= atomically . (`writeTVar` Just user') - -- [incognito] filter out contacts with whom user has incognito connections - contacts <- - filter (\ct -> isReady ct && not (contactConnIncognito ct)) - <$> withStore' (`getUserContacts` user) - withChatLock . procCmd $ do - forM_ contacts $ \ct -> - void (sendDirectContactMessage ct $ XInfo p') `catchError` (toView . CRChatError) - pure $ CRUserProfileUpdated (fromLocalProfile p) p' + withStore $ \db -> updateUserProfile db user p' + let user' = (user :: User) {localDisplayName = displayName, profile = toLocalProfile profileId p' localAlias} + asks currentUser >>= atomically . (`writeTVar` Just user') + -- [incognito] filter out contacts with whom user has incognito connections + contacts <- + filter (\ct -> isReady ct && not (contactConnIncognito ct)) + <$> withStore' (`getUserContacts` user) + withChatLock . procCmd $ do + forM_ contacts $ \ct -> + void (sendDirectContactMessage ct $ XInfo p') `catchError` (toView . CRChatError) + pure $ CRUserProfileUpdated (fromLocalProfile p) p' isReady :: Contact -> Bool isReady ct = let s = connStatus $ activeConn (ct :: Contact) @@ -1114,15 +1135,15 @@ processChatCommand = \case Nothing -> throwChatError CENoCurrentCall Just call@Call {contactId} | ctId == contactId -> do - call_ <- action userId ct call - case call_ of - Just call' -> do - unless (isRcvInvitation call') $ withStore' $ \db -> deleteCalls db user ctId - atomically $ TM.insert ctId call' calls - _ -> do - withStore' $ \db -> deleteCalls db user ctId - atomically $ TM.delete ctId calls - pure CRCmdOk + call_ <- action userId ct call + case call_ of + Just call' -> do + unless (isRcvInvitation call') $ withStore' $ \db -> deleteCalls db user ctId + atomically $ TM.insert ctId call' calls + _ -> do + withStore' $ \db -> deleteCalls db user ctId + atomically $ TM.delete ctId calls + pure CRCmdOk | otherwise -> throwChatError $ CECallContact contactId forwardFile :: ChatName -> FileTransferId -> (ChatName -> FilePath -> ChatCommand) -> m ChatResponse forwardFile chatName fileId sendCommand = withUser $ \user -> do @@ -1143,7 +1164,7 @@ sendGrpInvitation :: ChatMonad m => User -> Contact -> GroupInfo -> GroupMember sendGrpInvitation user ct@Contact {localDisplayName} GroupInfo {groupId, groupProfile, membership} GroupMember {groupMemberId, memberId, memberRole = memRole} cReq = do let GroupMember {memberRole = userRole, memberId = userMemberId} = membership groupInv = GroupInvitation (MemberIdRole userMemberId userRole) (MemberIdRole memberId memRole) cReq groupProfile - msg <- sendDirectContactMessage ct $ XGrpInv groupInv + (msg, _) <- sendDirectContactMessage ct $ XGrpInv groupInv let content = CISndGroupInvitation (CIGroupInvitation {groupId, groupMemberId, localDisplayName, groupProfile, status = CIGISPending}) memRole ci <- saveSndChatItem user (CDDirectSnd ct) msg content Nothing Nothing toView . CRNewChatItem $ AChatItem SCTDirect SMDSnd (DirectChat ct) ci @@ -1223,7 +1244,7 @@ toFSFilePath f = maybe f (<> "/" <> f) <$> (readTVarIO =<< asks filesFolder) acceptFileReceive :: forall m. ChatMonad m => User -> RcvFileTransfer -> Maybe FilePath -> m AChatItem -acceptFileReceive user@User {userId} RcvFileTransfer {fileId, fileInvitation = FileInvitation {fileName = fName, fileConnReq}, fileStatus, grpMemberId} filePath_ = do +acceptFileReceive user@User {userId} RcvFileTransfer {fileId, fileInvitation = FileInvitation {fileName = fName, fileConnReq, fileInline, fileSize}, fileStatus, grpMemberId} filePath_ = do unless (fileStatus == RFSNew) $ case fileStatus of RFSCancelled _ -> throwChatError $ CEFileCancelled fName _ -> throwChatError $ CEFileAlreadyReceiving fName @@ -1231,7 +1252,7 @@ acceptFileReceive user@User {userId} RcvFileTransfer {fileId, fileInvitation = F -- direct file protocol Just connReq -> do agentConnId <- withAgent $ \a -> joinConnection a True connReq . directMessage $ XFileAcpt fName - filePath <- getRcvFilePath filePath_ fName + filePath <- getRcvFilePath fileId filePath_ fName withStore $ \db -> acceptRcvFileTransfer db user fileId agentConnId ConnJoined filePath -- group & direct file protocol Nothing -> do @@ -1252,50 +1273,60 @@ acceptFileReceive user@User {userId} RcvFileTransfer {fileId, fileInvitation = F _ -> throwChatError $ CEFileInternal "member connection not active" _ -> throwChatError $ CEFileInternal "invalid chat ref for file transfer" where - acceptFile :: m (ChatMsgEvent, AChatItem) + acceptFile :: m (ChatMsgEvent 'Json, AChatItem) acceptFile = do sharedMsgId <- withStore $ \db -> getSharedMsgIdByFileId db userId fileId - (agentConnId, fileInvConnReq) <- withAgent $ \a -> createConnection a True SCMInvitation - filePath <- getRcvFilePath filePath_ fName - ci <- withStore (\db -> acceptRcvFileTransfer db user fileId agentConnId ConnNew filePath) - pure (XFileAcptInv sharedMsgId fileInvConnReq fName, ci) - getRcvFilePath :: Maybe FilePath -> String -> m FilePath - getRcvFilePath fPath_ fn = case fPath_ of - Nothing -> - asks filesFolder >>= readTVarIO >>= \case - Nothing -> do - dir <- (`combine` "Downloads") <$> getHomeDirectory - ifM (doesDirectoryExist dir) (pure dir) getTemporaryDirectory - >>= (`uniqueCombine` fn) - >>= createEmptyFile - Just filesFolder -> - filesFolder `uniqueCombine` fn - >>= createEmptyFile - >>= pure <$> takeFileName - Just fPath -> - ifM - (doesDirectoryExist fPath) - (fPath `uniqueCombine` fn >>= createEmptyFile) - $ ifM - (doesFileExist fPath) - (throwChatError $ CEFileAlreadyExists fPath) - (createEmptyFile fPath) + filePath <- getRcvFilePath fileId filePath_ fName + ChatConfig {fileChunkSize, inlineFiles} <- asks config + if + | fileInline == Just IFMOffer && fileSize <= fileChunkSize * receiveChunks inlineFiles -> do + -- accepting inline + ci <- withStore $ \db -> acceptRcvInlineFT db user fileId filePath + pure (XFileAcptInv sharedMsgId Nothing fName, ci) + | fileInline == Just IFMSent -> throwChatError $ CEFileAlreadyReceiving fName + | otherwise -> do + -- accepting via a new connection + (agentConnId, fileInvConnReq) <- withAgent $ \a -> createConnection a True SCMInvitation + ci <- withStore $ \db -> acceptRcvFileTransfer db user fileId agentConnId ConnNew filePath + pure (XFileAcptInv sharedMsgId (Just fileInvConnReq) fName, ci) + +getRcvFilePath :: forall m. ChatMonad m => FileTransferId -> Maybe FilePath -> String -> m FilePath +getRcvFilePath fileId fPath_ fn = case fPath_ of + Nothing -> + asks filesFolder >>= readTVarIO >>= \case + Nothing -> do + dir <- (`combine` "Downloads") <$> getHomeDirectory + ifM (doesDirectoryExist dir) (pure dir) getTemporaryDirectory + >>= (`uniqueCombine` fn) + >>= createEmptyFile + Just filesFolder -> + filesFolder `uniqueCombine` fn + >>= createEmptyFile + >>= pure <$> takeFileName + Just fPath -> + ifM + (doesDirectoryExist fPath) + (fPath `uniqueCombine` fn >>= createEmptyFile) + $ ifM + (doesFileExist fPath) + (throwChatError $ CEFileAlreadyExists fPath) + (createEmptyFile fPath) + where + createEmptyFile :: FilePath -> m FilePath + createEmptyFile fPath = emptyFile fPath `E.catch` (throwChatError . CEFileWrite fPath . (show :: E.SomeException -> String)) + emptyFile :: FilePath -> m FilePath + emptyFile fPath = do + h <- getFileHandle fileId fPath rcvFiles AppendMode + liftIO $ B.hPut h "" >> hFlush h + pure fPath + uniqueCombine :: FilePath -> String -> m FilePath + uniqueCombine filePath fileName = tryCombine (0 :: Int) where - createEmptyFile :: FilePath -> m FilePath - createEmptyFile fPath = emptyFile fPath `E.catch` (throwChatError . CEFileWrite fPath . (show :: E.SomeException -> String)) - emptyFile :: FilePath -> m FilePath - emptyFile fPath = do - h <- getFileHandle fileId fPath rcvFiles AppendMode - 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) + 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) acceptContactRequest :: ChatMonad m => User -> UserContactRequest -> Maybe IncognitoProfile -> m Contact acceptContactRequest user@User {userId} UserContactRequest {agentInvitationId = AgentInvId invId, localDisplayName = cName, profileId, profile = p, userContactLinkId, xContactId} incognitoProfile = do @@ -1411,9 +1442,9 @@ subscribeUserConnections agentBatchSubscribe user = do groupEvent | memberStatus membership == GSMemInvited = CRGroupInvitation g | all (\GroupMember {activeConn} -> isNothing activeConn) members = - if memberActive membership - then CRGroupEmpty g - else CRGroupRemoved g + if memberActive membership + then CRGroupEmpty g + else CRGroupRemoved g | otherwise = CRGroupSubscribed g sndFileSubsToView :: Map ConnId (Either AgentErrorType ()) -> Map ConnId SndFileTransfer -> m () sndFileSubsToView rs sfts = do @@ -1576,16 +1607,16 @@ processAgentMessage (Just user@User {userId, profile}) corrId agentConnId agentM CRContactUri _ -> throwChatError $ CECommandError "unexpected ConnectionRequestUri type" MSG msgMeta _msgFlags msgBody -> do cmdId <- createAckCmd conn - msg@RcvMessage {chatMsgEvent} <- saveRcvMSG conn (ConnectionId connId) msgMeta msgBody cmdId + msg@RcvMessage {chatMsgEvent = ACME _ event} <- saveRcvMSG conn (ConnectionId connId) msgMeta msgBody cmdId withAckMessage agentConnId cmdId msgMeta $ - case chatMsgEvent of + case event of XMsgNew mc -> newContentMessage ct mc msg msgMeta XMsgUpdate sharedMsgId mContent -> messageUpdate ct sharedMsgId mContent msg msgMeta XMsgDel sharedMsgId -> messageDelete ct sharedMsgId msg msgMeta -- TODO discontinue XFile XFile fInv -> processFileInvitation' ct fInv msg msgMeta XFileCancel sharedMsgId -> xFileCancel ct sharedMsgId msgMeta - XFileAcptInv sharedMsgId fileConnReq fName -> xFileAcptInv ct sharedMsgId fileConnReq fName msgMeta + XFileAcptInv sharedMsgId fileConnReq_ fName -> xFileAcptInv ct sharedMsgId fileConnReq_ fName msgMeta XInfo p -> xInfo ct p XGrpInv gInv -> processGroupInvitation ct gInv msg msgMeta XInfoProbe probe -> xInfoProbe ct probe @@ -1596,10 +1627,11 @@ processAgentMessage (Just user@User {userId, profile}) corrId agentConnId agentM XCallAnswer callId answer -> xCallAnswer ct callId answer msg msgMeta XCallExtra callId extraInfo -> xCallExtra ct callId extraInfo msg msgMeta XCallEnd callId -> xCallEnd ct callId msg msgMeta - _ -> pure () + BFileChunk sharedMsgId chunk -> bFileChunk ct sharedMsgId chunk msgMeta + _ -> messageError $ "unsupported message: " <> T.pack (show event) CONF confId _ connInfo -> do -- confirming direct connection with a member - ChatMessage {chatMsgEvent} <- liftEither $ parseChatMessage connInfo + ChatMessage {chatMsgEvent} <- parseChatMessage connInfo case chatMsgEvent of XGrpMemInfo _memId _memProfile -> do -- TODO check member ID @@ -1608,7 +1640,7 @@ processAgentMessage (Just user@User {userId, profile}) corrId agentConnId agentM allowAgentConnectionAsync user conn confId XOk _ -> messageError "CONF from member must have x.grp.mem.info" INFO connInfo -> do - ChatMessage {chatMsgEvent} <- liftEither $ parseChatMessage connInfo + ChatMessage {chatMsgEvent} <- parseChatMessage connInfo case chatMsgEvent of XGrpMemInfo _memId _memProfile -> do -- TODO check member ID @@ -1631,7 +1663,7 @@ processAgentMessage (Just user@User {userId, profile}) corrId agentConnId agentM withStore' (\db -> getUserContactLinkById db userId userContactLinkId) >>= \case Just (_, True, mc_, groupId_) -> do forM_ mc_ $ \mc -> do - msg <- sendDirectContactMessage ct (XMsgNew $ MCSimple (ExtMsgContent mc Nothing)) + (msg, _) <- sendDirectContactMessage ct (XMsgNew $ MCSimple (ExtMsgContent mc Nothing)) ci <- saveSndChatItem user (CDDirectSnd ct) msg (CISndMsgContent mc) Nothing Nothing toView . CRNewChatItem $ AChatItem SCTDirect SMDSnd (DirectChat ct) ci forM_ groupId_ $ \groupId -> do @@ -1650,6 +1682,7 @@ processAgentMessage (Just user@User {userId, profile}) corrId agentConnId agentM when (memberCategory m == GCPreMember) $ probeMatchingContacts ct connectedIncognito SENT msgId -> do sentMsgDeliveryEvent conn msgId + checkSndInlineFTComplete conn msgId withStore' (\db -> getDirectChatItemByAgentMsgId db userId contactId connId msgId) >>= \case Just (CChatItem SMDSnd ci) -> do chatItem <- withStore $ \db -> updateDirectChatItemStatus db userId contactId (chatItemId' ci) CISSndSent @@ -1690,33 +1723,33 @@ processAgentMessage (Just user@User {userId, profile}) corrId agentConnId agentM sendXGrpMemIntro hostConnId directConnReq XGrpMemIntroCont {groupId, groupMemberId, memberId, groupConnReq} CRContactUri _ -> throwChatError $ CECommandError "unexpected ConnectionRequestUri type" CONF confId _ connInfo -> do - ChatMessage {chatMsgEvent} <- liftEither $ parseChatMessage connInfo + ChatMessage {chatMsgEvent} <- parseChatMessage connInfo case memberCategory m of GCInviteeMember -> case chatMsgEvent of XGrpAcpt memId | sameMemberId memId m -> do - withStore $ \db -> liftIO $ updateGroupMemberStatus db userId m GSMemAccepted - -- [async agent commands] no continuation needed, but command should be asynchronous for stability - allowAgentConnectionAsync user conn confId XOk + withStore $ \db -> liftIO $ updateGroupMemberStatus db userId m GSMemAccepted + -- [async agent commands] no continuation needed, but command should be asynchronous for stability + allowAgentConnectionAsync user conn confId XOk | otherwise -> messageError "x.grp.acpt: memberId is different from expected" _ -> messageError "CONF from invited member must have x.grp.acpt" _ -> case chatMsgEvent of XGrpMemInfo memId _memProfile | sameMemberId memId m -> do - -- TODO update member profile - -- [async agent commands] no continuation needed, but command should be asynchronous for stability - allowAgentConnectionAsync user conn confId $ XGrpMemInfo (memberId (membership :: GroupMember)) (fromLocalProfile $ memberProfile membership) + -- TODO update member profile + -- [async agent commands] no continuation needed, but command should be asynchronous for stability + allowAgentConnectionAsync user conn confId $ XGrpMemInfo (memberId (membership :: GroupMember)) (fromLocalProfile $ memberProfile membership) | otherwise -> messageError "x.grp.mem.info: memberId is different from expected" _ -> messageError "CONF from member must have x.grp.mem.info" INFO connInfo -> do - ChatMessage {chatMsgEvent} <- liftEither $ parseChatMessage connInfo + ChatMessage {chatMsgEvent} <- parseChatMessage connInfo case chatMsgEvent of XGrpMemInfo memId _memProfile | sameMemberId memId m -> do - -- TODO update member profile - pure () + -- TODO update member profile + pure () | otherwise -> messageError "x.grp.mem.info: memberId is different from expected" XOk -> pure () _ -> messageError "INFO from member must have x.grp.mem.info" @@ -1759,16 +1792,16 @@ processAgentMessage (Just user@User {userId, profile}) corrId agentConnId agentM when (memberCategory m == GCPreMember) $ probeMatchingContacts ct connectedIncognito MSG msgMeta _msgFlags msgBody -> do cmdId <- createAckCmd conn - msg@RcvMessage {chatMsgEvent} <- saveRcvMSG conn (GroupId groupId) msgMeta msgBody cmdId + msg@RcvMessage {chatMsgEvent = ACME _ event} <- saveRcvMSG conn (GroupId groupId) msgMeta msgBody cmdId withAckMessage agentConnId cmdId msgMeta $ - case chatMsgEvent of + case event of XMsgNew mc -> newGroupContentMessage gInfo m mc msg msgMeta XMsgUpdate sharedMsgId mContent -> groupMessageUpdate gInfo m sharedMsgId mContent msg msgMeta XMsgDel sharedMsgId -> groupMessageDelete gInfo m sharedMsgId msg -- TODO discontinue XFile XFile fInv -> processGroupFileInvitation' gInfo m fInv msg msgMeta XFileCancel sharedMsgId -> xFileCancelGroup gInfo m sharedMsgId msgMeta - XFileAcptInv sharedMsgId fileConnReq fName -> xFileAcptInvGroup gInfo m sharedMsgId fileConnReq fName msgMeta + XFileAcptInv sharedMsgId fileConnReq_ fName -> xFileAcptInvGroup gInfo m sharedMsgId fileConnReq_ fName msgMeta XGrpMemNew memInfo -> xGrpMemNew gInfo m memInfo msg msgMeta XGrpMemIntro memInfo -> xGrpMemIntro gInfo m memInfo XGrpMemInv memId introInv -> xGrpMemInv gInfo m memId introInv @@ -1778,9 +1811,11 @@ processAgentMessage (Just user@User {userId, profile}) corrId agentConnId agentM XGrpLeave -> xGrpLeave gInfo m msg msgMeta XGrpDel -> xGrpDel gInfo m msg msgMeta XGrpInfo p' -> xGrpInfo gInfo m p' msg msgMeta - _ -> messageError $ "unsupported message: " <> T.pack (show chatMsgEvent) - SENT msgId -> + BFileChunk sharedMsgId chunk -> bFileChunkGroup gInfo sharedMsgId chunk msgMeta + _ -> messageError $ "unsupported message: " <> T.pack (show event) + SENT msgId -> do sentMsgDeliveryEvent conn msgId + checkSndInlineFTComplete conn msgId OK -> -- [async agent commands] continuation on receiving OK withCompletedCommand conn agentMsg $ \CommandData {cmdFunction, cmdId} -> @@ -1798,14 +1833,14 @@ processAgentMessage (Just user@User {userId, profile}) corrId agentConnId agentM -- SMP CONF for SndFileConnection happens for direct file protocol -- when recipient of the file "joins" connection created by the sender CONF confId _ connInfo -> do - ChatMessage {chatMsgEvent} <- liftEither $ parseChatMessage connInfo + ChatMessage {chatMsgEvent} <- parseChatMessage connInfo case chatMsgEvent of -- TODO save XFileAcpt message XFileAcpt name | name == fileName -> do - withStore' $ \db -> updateSndFileStatus db ft FSAccepted - -- [async agent commands] no continuation needed, but command should be asynchronous for stability - allowAgentConnectionAsync user conn confId XOk + withStore' $ \db -> updateSndFileStatus db ft FSAccepted + -- [async agent commands] no continuation needed, but command should be asynchronous for stability + allowAgentConnectionAsync user conn confId XOk | otherwise -> messageError "x.file.acpt: fileName is different from expected" _ -> messageError "CONF from file connection must have x.file.acpt" CON -> do @@ -1837,57 +1872,21 @@ processAgentMessage (Just user@User {userId, profile}) corrId agentConnId agentM _ -> pure () processRcvFileConn :: ACommand 'Agent -> Connection -> RcvFileTransfer -> m () - processRcvFileConn agentMsg conn ft@RcvFileTransfer {fileId, chunkSize, cancelled} = + processRcvFileConn agentMsg conn ft = case agentMsg of -- SMP CONF for RcvFileConnection happens for group file protocol -- when sender of the file "joins" connection created by the recipient -- (sender doesn't create connections for all group members) CONF confId _ connInfo -> do - ChatMessage {chatMsgEvent} <- liftEither $ parseChatMessage connInfo + ChatMessage {chatMsgEvent} <- parseChatMessage connInfo case chatMsgEvent of XOk -> allowAgentConnectionAsync user conn confId XOk -- [async agent commands] no continuation needed, but command should be asynchronous for stability _ -> pure () - CON -> do - ci <- withStore $ \db -> do - liftIO $ updateRcvFileStatus db ft FSConnected - liftIO $ updateCIFileStatus db user fileId CIFSRcvTransfer - getChatItemByFileId db user fileId - toView $ CRRcvFileStart ci - MSG meta@MsgMeta {recipient = (msgId, _), integrity} _ msgBody -> do + CON -> startReceivingFile ft + MSG meta _ msgBody -> do cmdId <- createAckCmd conn - withAckMessage agentConnId cmdId meta $ do - parseFileChunk msgBody >>= \case - FileChunkCancel -> - unless cancelled $ do - cancelRcvFileTransfer user ft - toView (CRRcvFileSndCancelled ft) - FileChunk {chunkNo, chunkBytes = 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' (\db -> createRcvFileChunk db 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 - ci <- withStore $ \db -> do - liftIO $ do - updateRcvFileStatus db ft FSComplete - updateCIFileStatus db user fileId CIFSRcvComplete - deleteRcvFileChunks db ft - getChatItemByFileId db user fileId - toView $ CRRcvFileComplete ci - closeFileHandle fileId rcvFiles - deleteAgentConnectionAsync user conn - RcvChunkDuplicate -> pure () - RcvChunkError -> badRcvFileChunk ft $ "incorrect chunk number " <> show chunkNo + withAckMessage agentConnId cmdId meta $ + parseFileChunk msgBody >>= receiveFileChunk ft (Just conn) meta OK -> -- [async agent commands] continuation on receiving OK withCompletedCommand conn agentMsg $ \_cmdData -> pure () @@ -1898,10 +1897,52 @@ processAgentMessage (Just user@User {userId, profile}) corrId agentConnId agentM -- TODO add debugging output _ -> pure () + startReceivingFile :: RcvFileTransfer -> m () + startReceivingFile ft@RcvFileTransfer {fileId} = do + ci <- withStore $ \db -> do + liftIO $ updateRcvFileStatus db ft FSConnected + liftIO $ updateCIFileStatus db user fileId CIFSRcvTransfer + getChatItemByFileId db user fileId + toView $ CRRcvFileStart ci + + receiveFileChunk :: RcvFileTransfer -> Maybe Connection -> MsgMeta -> FileChunk -> m () + receiveFileChunk ft@RcvFileTransfer {fileId, chunkSize, cancelled} conn_ MsgMeta {recipient = (msgId, _), integrity} = \case + FileChunkCancel -> + unless cancelled $ do + cancelRcvFileTransfer user ft + toView (CRRcvFileSndCancelled ft) + FileChunk {chunkNo, chunkBytes = 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' (\db -> createRcvFileChunk db 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 + ci <- withStore $ \db -> do + liftIO $ do + updateRcvFileStatus db ft FSComplete + updateCIFileStatus db user fileId CIFSRcvComplete + deleteRcvFileChunks db ft + getChatItemByFileId db user fileId + toView $ CRRcvFileComplete ci + closeFileHandle fileId rcvFiles + mapM_ (deleteAgentConnectionAsync user) conn_ + RcvChunkDuplicate -> pure () + RcvChunkError -> badRcvFileChunk ft $ "incorrect chunk number " <> show chunkNo + processUserContactRequest :: ACommand 'Agent -> Connection -> UserContact -> m () processUserContactRequest agentMsg conn UserContact {userContactLinkId} = case agentMsg of REQ invId _ connInfo -> do - ChatMessage {chatMsgEvent} <- liftEither $ parseChatMessage connInfo + ChatMessage {chatMsgEvent} <- parseChatMessage connInfo case chatMsgEvent of XContact p xContactId_ -> profileContactRequest invId p xContactId_ XInfo p -> profileContactRequest invId p Nothing @@ -1947,8 +1988,8 @@ processAgentMessage (Just user@User {userId, profile}) corrId agentConnId agentM case cmdData_ of Just cmdData@CommandData {cmdId, cmdConnId = Just cmdConnId', cmdFunction} | connId == cmdConnId' && (agentMsgTag == commandExpectedResponse cmdFunction || agentMsgTag == ERR_) -> do - withStore' $ \db -> deleteCommand db user cmdId - action cmdData + withStore' $ \db -> deleteCommand db user cmdId + action cmdData | otherwise -> err cmdId $ "not matching connection id or unexpected response, corrId = " <> show corrId Just CommandData {cmdId, cmdConnId = Nothing} -> err cmdId $ "no command connection id, corrId = " <> show corrId Nothing -> throwChatError . CEAgentCommandError $ "command not found, corrId = " <> show corrId @@ -2029,20 +2070,24 @@ processAgentMessage (Just user@User {userId, profile}) corrId agentConnId agentM newContentMessage ct@Contact {localDisplayName = c, chatSettings} mc msg msgMeta = do checkIntegrityCreateItem (CDDirectRcv ct) msgMeta let (ExtMsgContent content fileInvitation_) = mcExtMsgContent mc - ciFile_ <- processFileInvitation fileInvitation_ $ - \fi chSize -> withStore' $ \db -> createRcvFileTransfer db userId ct fi chSize + ciFile_ <- processFileInvitation fileInvitation_ $ \db -> createRcvFileTransfer db userId ct ci@ChatItem {formattedText} <- saveRcvChatItem user (CDDirectRcv ct) msg msgMeta (CIRcvMsgContent content) ciFile_ toView . CRNewChatItem $ AChatItem SCTDirect SMDRcv (DirectChat ct) ci when (enableNtfs chatSettings) $ showMsgToast (c <> "> ") content formattedText setActive $ ActiveC c - processFileInvitation :: Maybe FileInvitation -> (FileInvitation -> Integer -> m RcvFileTransfer) -> m (Maybe (CIFile 'MDRcv)) - processFileInvitation fileInvitation_ createRcvFileTransferF = - forM fileInvitation_ $ \fileInvitation@FileInvitation {fileName, fileSize} -> do - chSize <- asks $ fileChunkSize . config - RcvFileTransfer {fileId} <- createRcvFileTransferF fileInvitation chSize - let ciFile = CIFile {fileId, fileName, fileSize, filePath = Nothing, fileStatus = CIFSRcvInvitation} - pure ciFile + processFileInvitation :: Maybe FileInvitation -> (DB.Connection -> FileInvitation -> Maybe InlineFileMode -> Integer -> IO RcvFileTransfer) -> m (Maybe (CIFile 'MDRcv)) + processFileInvitation fInv_ createRcvFT = forM fInv_ $ \fInv@FileInvitation {fileName, fileSize} -> do + chSize <- asks $ fileChunkSize . config + inline <- receiveInlineMode fInv chSize + ft@RcvFileTransfer {fileId} <- withStore' $ \db -> createRcvFT db fInv inline chSize + (filePath, fileStatus) <- case inline of + Just IFMSent -> do + fPath <- getRcvFilePath fileId Nothing fileName + withStore' $ \db -> startRcvInlineFT db user ft fPath + pure (Just fPath, CIFSRcvAccepted) + _ -> pure (Nothing, CIFSRcvInvitation) + pure CIFile {fileId, fileName, fileSize, filePath, fileStatus} messageUpdate :: Contact -> SharedMsgId -> MsgContent -> RcvMessage -> MsgMeta -> m () messageUpdate ct@Contact {contactId, localDisplayName = c} sharedMsgId mc msg@RcvMessage {msgId} msgMeta = do @@ -2082,9 +2127,8 @@ processAgentMessage (Just user@User {userId, profile}) corrId agentConnId agentM newGroupContentMessage :: GroupInfo -> GroupMember -> MsgContainer -> RcvMessage -> MsgMeta -> m () newGroupContentMessage gInfo@GroupInfo {chatSettings} m@GroupMember {localDisplayName = c} mc msg msgMeta = do - let (ExtMsgContent content fileInvitation_) = mcExtMsgContent mc - ciFile_ <- processFileInvitation fileInvitation_ $ - \fi chSize -> withStore' $ \db -> createRcvGroupFileTransfer db userId m fi chSize + let (ExtMsgContent content fInv_) = mcExtMsgContent mc + ciFile_ <- processFileInvitation fInv_ $ \db -> createRcvGroupFileTransfer db userId m ci@ChatItem {formattedText} <- saveRcvChatItem user (CDGroupRcv gInfo m) msg msgMeta (CIRcvMsgContent content) ciFile_ groupMsgToView gInfo m ci msgMeta let g = groupName' gInfo @@ -2133,7 +2177,8 @@ processAgentMessage (Just user@User {userId, profile}) corrId agentConnId agentM processFileInvitation' ct@Contact {localDisplayName = c} fInv@FileInvitation {fileName, fileSize} msg msgMeta = do checkIntegrityCreateItem (CDDirectRcv ct) msgMeta chSize <- asks $ fileChunkSize . config - RcvFileTransfer {fileId} <- withStore' $ \db -> createRcvFileTransfer db userId ct fInv chSize + inline <- receiveInlineMode fInv chSize + RcvFileTransfer {fileId} <- withStore' $ \db -> createRcvFileTransfer db userId ct fInv inline chSize let ciFile = Just $ CIFile {fileId, fileName, fileSize, filePath = Nothing, fileStatus = CIFSRcvInvitation} ci <- saveRcvChatItem user (CDDirectRcv ct) msg msgMeta (CIRcvMsgContent $ MCFile "") ciFile toView . CRNewChatItem $ AChatItem SCTDirect SMDRcv (DirectChat ct) ci @@ -2144,7 +2189,8 @@ processAgentMessage (Just user@User {userId, profile}) corrId agentConnId agentM processGroupFileInvitation' :: GroupInfo -> GroupMember -> FileInvitation -> RcvMessage -> MsgMeta -> m () processGroupFileInvitation' gInfo m@GroupMember {localDisplayName = c} fInv@FileInvitation {fileName, fileSize} msg msgMeta = do chSize <- asks $ fileChunkSize . config - RcvFileTransfer {fileId} <- withStore' $ \db -> createRcvGroupFileTransfer db userId m fInv chSize + inline <- receiveInlineMode fInv chSize + RcvFileTransfer {fileId} <- withStore' $ \db -> createRcvGroupFileTransfer db userId m fInv inline chSize let ciFile = Just $ CIFile {fileId, fileName, fileSize, filePath = Nothing, fileStatus = CIFSRcvInvitation} ci <- saveRcvChatItem user (CDGroupRcv gInfo m) msg msgMeta (CIRcvMsgContent $ MCFile "") ciFile groupMsgToView gInfo m ci msgMeta @@ -2152,6 +2198,13 @@ processAgentMessage (Just user@User {userId, profile}) corrId agentConnId agentM showToast ("#" <> g <> " " <> c <> "> ") "wants to send a file" setActive $ ActiveG g + receiveInlineMode :: FileInvitation -> Integer -> m (Maybe InlineFileMode) + receiveInlineMode FileInvitation {fileSize, fileInline} chSize = case fileInline of + inline@(Just _) -> do + rcvChunks <- asks $ receiveChunks . inlineFiles . config + pure $ if fileSize <= rcvChunks * chSize then inline else Nothing + _ -> pure Nothing + xFileCancel :: Contact -> SharedMsgId -> MsgMeta -> m () xFileCancel ct@Contact {contactId} sharedMsgId msgMeta = do checkIntegrityCreateItem (CDDirectRcv ct) msgMeta @@ -2161,18 +2214,65 @@ processAgentMessage (Just user@User {userId, profile}) corrId agentConnId agentM cancelRcvFileTransfer user ft toView $ CRRcvFileSndCancelled ft - xFileAcptInv :: Contact -> SharedMsgId -> ConnReqInvitation -> String -> MsgMeta -> m () - xFileAcptInv ct sharedMsgId fileConnReq fName msgMeta = do + xFileAcptInv :: Contact -> SharedMsgId -> Maybe ConnReqInvitation -> String -> MsgMeta -> m () + xFileAcptInv ct sharedMsgId fileConnReq_ fName msgMeta = do checkIntegrityCreateItem (CDDirectRcv ct) msgMeta fileId <- withStore $ \db -> getDirectFileIdBySharedMsgId db user ct sharedMsgId - (FileTransferMeta {fileName, cancelled}, _) <- withStore (\db -> getSndFileTransfer db user fileId) + ft@FileTransferMeta {fileName, fileSize, fileInline, cancelled} <- withStore (\db -> getFileTransferMeta db user fileId) -- [async agent commands] no continuation needed, but command should be asynchronous for stability if fName == fileName - then unless cancelled $ do - connIds <- joinAgentConnectionAsync user True fileConnReq $ directMessage XOk - withStore' $ \db -> createSndDirectFTConnection db user fileId connIds + then unless cancelled $ case fileConnReq_ of + -- receiving via a separate connection + Just fileConnReq -> do + connIds <- joinAgentConnectionAsync user True fileConnReq $ directMessage XOk + withStore' $ \db -> createSndDirectFTConnection db user fileId connIds + -- receiving inline + _ -> do + event <- withStore $ \db -> do + ci <- updateDirectCIFileStatus db user fileId CIFSSndTransfer + sft <- liftIO $ createSndDirectInlineFT db ct ft + pure $ CRSndFileStart ci sft + toView event + ifM + (allowSendInline fileSize fileInline) + (sendDirectFileInline ct ft sharedMsgId) + (messageError "x.file.acpt.inv: fileSize is bigger than allowed to send inline") else messageError "x.file.acpt.inv: fileName is different from expected" + checkSndInlineFTComplete :: Connection -> AgentMsgId -> m () + checkSndInlineFTComplete conn agentMsgId = do + ft_ <- withStore' $ \db -> getSndInlineFTViaMsgDelivery db user conn agentMsgId + forM_ ft_ $ \ft@SndFileTransfer {fileId} -> do + ci <- withStore $ \db -> do + liftIO $ updateSndFileStatus db ft FSComplete + liftIO $ deleteSndFileChunks db ft + updateDirectCIFileStatus db user fileId CIFSSndComplete + toView $ CRSndFileComplete ci ft + + allowSendInline :: Integer -> Maybe InlineFileMode -> m Bool + allowSendInline fileSize = \case + Just IFMOffer -> do + ChatConfig {fileChunkSize, inlineFiles} <- asks config + pure $ fileSize <= fileChunkSize * offerChunks inlineFiles + _ -> pure False + + bFileChunk :: Contact -> SharedMsgId -> FileChunk -> MsgMeta -> m () + bFileChunk ct sharedMsgId chunk meta = do + ft <- withStore $ \db -> getDirectFileIdBySharedMsgId db user ct sharedMsgId >>= getRcvFileTransfer db user + receiveInlineChunk ft chunk meta + + bFileChunkGroup :: GroupInfo -> SharedMsgId -> FileChunk -> MsgMeta -> m () + bFileChunkGroup GroupInfo {groupId} sharedMsgId chunk meta = do + ft <- withStore $ \db -> getGroupFileIdBySharedMsgId db userId groupId sharedMsgId >>= getRcvFileTransfer db user + receiveInlineChunk ft chunk meta + + receiveInlineChunk :: RcvFileTransfer -> FileChunk -> MsgMeta -> m () + receiveInlineChunk ft chunk meta = do + case chunk of + FileChunk {chunkNo} -> when (chunkNo == 1) $ startReceivingFile ft + _ -> pure () + receiveFileChunk ft Nothing meta chunk + xFileCancelGroup :: GroupInfo -> GroupMember -> SharedMsgId -> MsgMeta -> m () xFileCancelGroup g@GroupInfo {groupId} mem@GroupMember {groupMemberId, memberId} sharedMsgId msgMeta = do checkIntegrityCreateItem (CDGroupRcv g mem) msgMeta @@ -2189,16 +2289,31 @@ processAgentMessage (Just user@User {userId, profile}) corrId agentConnId agentM else messageError "x.file.cancel: group member attempted to cancel file of another member" -- shouldn't happen now that query includes group member id (SMDSnd, _) -> messageError "x.file.cancel: group member attempted invalid file cancel" - xFileAcptInvGroup :: GroupInfo -> GroupMember -> SharedMsgId -> ConnReqInvitation -> String -> MsgMeta -> m () - xFileAcptInvGroup g@GroupInfo {groupId} m sharedMsgId fileConnReq fName msgMeta = do + xFileAcptInvGroup :: GroupInfo -> GroupMember -> SharedMsgId -> Maybe ConnReqInvitation -> String -> MsgMeta -> m () + xFileAcptInvGroup g@GroupInfo {groupId} m@GroupMember {activeConn} sharedMsgId fileConnReq_ fName msgMeta = do checkIntegrityCreateItem (CDGroupRcv g m) msgMeta fileId <- withStore $ \db -> getGroupFileIdBySharedMsgId db userId groupId sharedMsgId - (FileTransferMeta {fileName, cancelled}, _) <- withStore (\db -> getSndFileTransfer db user fileId) + -- TODO check that it's not already accpeted + ft@FileTransferMeta {fileName, fileSize, fileInline, cancelled} <- withStore (\db -> getFileTransferMeta db user fileId) if fName == fileName - then unless cancelled $ do - -- [async agent commands] no continuation needed, but command should be asynchronous for stability - connIds <- joinAgentConnectionAsync user True fileConnReq $ directMessage XOk - withStore' $ \db -> createSndGroupFileTransferConnection db user fileId connIds m + then unless cancelled $ case (fileConnReq_, activeConn) of + (Just fileConnReq, _) -> do + -- receiving via a separate connection + -- [async agent commands] no continuation needed, but command should be asynchronous for stability + connIds <- joinAgentConnectionAsync user True fileConnReq $ directMessage XOk + withStore' $ \db -> createSndGroupFileTransferConnection db user fileId connIds m + (_, Just conn) -> do + -- receiving inline + event <- withStore $ \db -> do + ci <- updateDirectCIFileStatus db user fileId CIFSSndTransfer + sft <- liftIO $ createSndGroupInlineFT db m conn ft + pure $ CRSndFileStart ci sft + toView event + ifM + (allowSendInline fileSize fileInline) + (sendMemberFileInline m conn ft sharedMsgId) + (messageError "x.file.acpt.inv: fileSize is bigger than allowed to send inline") + _ -> messageError "x.file.acpt.inv: member connection is not active" else messageError "x.file.acpt.inv: fileName is different from expected" groupMsgToView :: GroupInfo -> GroupMember -> ChatItem 'CTGroup 'MDRcv -> MsgMeta -> m () @@ -2348,16 +2463,16 @@ processAgentMessage (Just user@User {userId, profile}) corrId agentConnId agentM Just call@Call {contactId, callId, chatItemId} | contactId /= ctId' || callId /= callId' -> messageError $ eventName <> ": wrong contact or callId" | otherwise -> do - (call_, aciContent_) <- action call - case call_ of - Just call' -> do - unless (isRcvInvitation call') $ withStore' $ \db -> deleteCalls db user ctId' - atomically $ TM.insert ctId' call' calls - _ -> do - withStore' $ \db -> deleteCalls db user ctId' - atomically $ TM.delete ctId' calls - forM_ aciContent_ $ \aciContent -> - updateDirectChatItemView userId ct chatItemId aciContent $ Just msgId + (call_, aciContent_) <- action call + case call_ of + Just call' -> do + unless (isRcvInvitation call') $ withStore' $ \db -> deleteCalls db user ctId' + atomically $ TM.insert ctId' call' calls + _ -> do + withStore' $ \db -> deleteCalls db user ctId' + atomically $ TM.delete ctId' calls + forM_ aciContent_ $ \aciContent -> + updateDirectChatItemView userId ct chatItemId aciContent $ Just msgId msgCallStateError :: Text -> Call -> m () msgCallStateError eventName Call {callState} = @@ -2370,7 +2485,7 @@ processAgentMessage (Just user@User {userId, profile}) corrId agentConnId agentM saveConnInfo :: Connection -> ConnInfo -> m () saveConnInfo activeConn connInfo = do - ChatMessage {chatMsgEvent} <- liftEither $ parseChatMessage connInfo + ChatMessage {chatMsgEvent} <- parseChatMessage connInfo case chatMsgEvent of XInfo p -> do ct <- withStore $ \db -> createDirectContact db userId activeConn p @@ -2450,21 +2565,21 @@ processAgentMessage (Just user@User {userId, profile}) corrId agentConnId agentM xGrpMemRole :: GroupInfo -> GroupMember -> MemberId -> GroupMemberRole -> RcvMessage -> MsgMeta -> m () xGrpMemRole gInfo@GroupInfo {membership} m@GroupMember {memberRole = senderRole} memId memRole msg msgMeta | memberId (membership :: GroupMember) == memId = - let gInfo' = gInfo {membership = membership {memberRole = memRole}} - in changeMemberRole gInfo' membership $ RGEUserRole memRole + let gInfo' = gInfo {membership = membership {memberRole = memRole}} + in changeMemberRole gInfo' membership $ RGEUserRole memRole | otherwise = do - members <- withStore' $ \db -> getGroupMembers db user gInfo - case find (sameMemberId memId) members of - Just member -> changeMemberRole gInfo member $ RGEMemberRole (groupMemberId' member) (fromLocalProfile $ memberProfile member) memRole - _ -> messageError "x.grp.mem.role with unknown member ID" + members <- withStore' $ \db -> getGroupMembers db user gInfo + case find (sameMemberId memId) members of + Just member -> changeMemberRole gInfo member $ RGEMemberRole (groupMemberId' member) (fromLocalProfile $ memberProfile member) memRole + _ -> messageError "x.grp.mem.role with unknown member ID" where changeMemberRole gInfo' member@GroupMember {memberRole = fromRole} gEvent | senderRole < GRAdmin || senderRole < fromRole = messageError "x.grp.mem.role with insufficient member permissions" | otherwise = do - withStore' $ \db -> updateGroupMemberRole db user member memRole - ci <- saveRcvChatItem user (CDGroupRcv gInfo m) msg msgMeta (CIRcvGroupEvent gEvent) Nothing - groupMsgToView gInfo m ci msgMeta - toView CRMemberRole {groupInfo = gInfo', byMember = m, member = member {memberRole = memRole}, fromRole, toRole = memRole} + withStore' $ \db -> updateGroupMemberRole db user member memRole + ci <- saveRcvChatItem user (CDGroupRcv gInfo m) msg msgMeta (CIRcvGroupEvent gEvent) Nothing + groupMsgToView gInfo m ci msgMeta + toView CRMemberRole {groupInfo = gInfo', byMember = m, member = member {memberRole = memRole}, fromRole, toRole = memRole} checkHostRole :: GroupMember -> GroupMemberRole -> m () checkHostRole GroupMember {memberRole, localDisplayName} memRole = @@ -2489,7 +2604,7 @@ processAgentMessage (Just user@User {userId, profile}) corrId agentConnId agentM where checkRole GroupMember {memberRole} a | senderRole < GRAdmin || senderRole < memberRole = - messageError "x.grp.mem.del with insufficient member permissions" + messageError "x.grp.mem.del with insufficient member permissions" | otherwise = a deleteMember member gEvent = do withStore' $ \db -> updateGroupMemberStatus db userId member GSMemRemoved @@ -2523,13 +2638,38 @@ processAgentMessage (Just user@User {userId, profile}) corrId agentConnId agentM xGrpInfo g m@GroupMember {memberRole} p' msg msgMeta | memberRole < GROwner = messageError "x.grp.info with insufficient member permissions" | otherwise = do - g' <- withStore $ \db -> updateGroupProfile db user g p' - ci <- saveRcvChatItem user (CDGroupRcv g' m) msg msgMeta (CIRcvGroupEvent $ RGEGroupUpdated p') Nothing - groupMsgToView g' m ci msgMeta - toView . CRGroupUpdated g g' $ Just m + g' <- withStore $ \db -> updateGroupProfile db user g p' + ci <- saveRcvChatItem user (CDGroupRcv g' m) msg msgMeta (CIRcvGroupEvent $ RGEGroupUpdated p') Nothing + groupMsgToView g' m ci msgMeta + toView . CRGroupUpdated g g' $ Just m -parseChatMessage :: ByteString -> Either ChatError ChatMessage -parseChatMessage = first (ChatError . CEInvalidChatMessage) . strDecode +sendDirectFileInline :: ChatMonad m => Contact -> FileTransferMeta -> SharedMsgId -> m () +sendDirectFileInline ct ft sharedMsgId = do + msgDeliveryId <- sendFileInline_ ft sharedMsgId $ sendDirectContactMessage ct + withStore' $ \db -> updateSndDirectFTDelivery db ct ft msgDeliveryId + +sendMemberFileInline :: ChatMonad m => GroupMember -> Connection -> FileTransferMeta -> SharedMsgId -> m () +sendMemberFileInline m@GroupMember {groupId} conn ft sharedMsgId = do + msgDeliveryId <- sendFileInline_ ft sharedMsgId $ \msg -> sendDirectMessage conn msg $ GroupId groupId + withStore' $ \db -> updateSndGroupFTDelivery db m conn ft msgDeliveryId + +sendFileInline_ :: ChatMonad m => FileTransferMeta -> SharedMsgId -> (ChatMsgEvent 'Binary -> m (SndMessage, Int64)) -> m Int64 +sendFileInline_ FileTransferMeta {filePath, chunkSize} sharedMsgId sendMsg = + sendChunks 1 =<< liftIO . B.readFile =<< toFSFilePath filePath + where + sendChunks chunkNo bytes = do + let (chunk, rest) = B.splitAt chSize bytes + (_, msgDeliveryId) <- sendMsg $ BFileChunk sharedMsgId $ FileChunk chunkNo chunk + if B.null rest + then pure msgDeliveryId + else sendChunks (chunkNo + 1) rest + chSize = fromIntegral chunkSize + +parseChatMessage :: ChatMonad m => ByteString -> m (ChatMessage 'Json) +parseChatMessage = liftEither . first (ChatError . CEInvalidChatMessage) . strDecode + +parseAChatMessage :: ChatMonad m => ByteString -> m AChatMessage +parseAChatMessage = liftEither . first (ChatError . CEInvalidChatMessage) . strDecode sendFileChunk :: ChatMonad m => User -> SndFileTransfer -> m () sendFileChunk user ft@SndFileTransfer {fileId, fileStatus, connId, agentConnId} = @@ -2563,24 +2703,8 @@ readFileChunk SndFileTransfer {fileId, filePath, chunkSize} chunkNo = do when (pos /= pos') $ hSeek h AbsoluteSeek pos' liftIO . B.hGet h $ fromInteger chunkSize -data FileChunk = FileChunk {chunkNo :: Integer, chunkBytes :: ByteString} | FileChunkCancel - -instance Encoding FileChunk where - smpEncode = \case - FileChunk {chunkNo, chunkBytes} -> smpEncode ('F', fromIntegral chunkNo :: Word32, Tail chunkBytes) - FileChunkCancel -> smpEncode 'C' - smpP = - smpP >>= \case - 'F' -> do - chunkNo <- fromIntegral <$> smpP @Word32 - Tail chunkBytes <- smpP - pure FileChunk {chunkNo, chunkBytes} - 'C' -> pure FileChunkCancel - _ -> fail "bad FileChunk" - parseFileChunk :: ChatMonad m => ByteString -> m FileChunk -parseFileChunk msg = - liftEither . first (ChatError . CEFileRcvChunk) $ parseAll smpP msg +parseFileChunk = liftEither . first (ChatError . CEFileRcvChunk) . smpDecode appendFileChunk :: ChatMonad m => RcvFileTransfer -> Integer -> ByteString -> m () appendFileChunk ft@RcvFileTransfer {fileId, fileStatus} chunkNo chunk = @@ -2617,13 +2741,13 @@ isFileActive fileId files = do isJust . M.lookup fileId <$> readTVarIO fs cancelRcvFileTransfer :: ChatMonad m => User -> RcvFileTransfer -> m () -cancelRcvFileTransfer user ft@RcvFileTransfer {fileId, fileStatus} = do +cancelRcvFileTransfer user ft@RcvFileTransfer {fileId, fileStatus, rcvFileInline} = do closeFileHandle fileId rcvFiles withStore' $ \db -> do updateFileCancelled db user fileId CIFSRcvCancelled updateRcvFileStatus db ft FSCancelled deleteRcvFileChunks db ft - case fileStatus of + when (isNothing rcvFileInline) $ case fileStatus of RFSAccepted RcvFileInfo {connId, agentConnId} -> deleteAgentConnectionAsync' user connId agentConnId RFSConnected RcvFileInfo {connId, agentConnId} -> @@ -2661,45 +2785,44 @@ deleteMemberConnection user GroupMember {activeConn} = do -- withStore $ \db -> deleteGroupMemberConnection db userId m -sendDirectContactMessage :: ChatMonad m => Contact -> ChatMsgEvent -> m SndMessage +sendDirectContactMessage :: (MsgEncodingI e, ChatMonad m) => Contact -> ChatMsgEvent e -> m (SndMessage, Int64) sendDirectContactMessage ct@Contact {activeConn = conn@Connection {connId, connStatus}} chatMsgEvent = do if connStatus == ConnReady || connStatus == ConnSndReady then sendDirectMessage conn chatMsgEvent (ConnectionId connId) else throwChatError $ CEContactNotReady ct -sendDirectMessage :: ChatMonad m => Connection -> ChatMsgEvent -> ConnOrGroupId -> m SndMessage +sendDirectMessage :: (MsgEncodingI e, ChatMonad m) => Connection -> ChatMsgEvent e -> ConnOrGroupId -> m (SndMessage, Int64) sendDirectMessage conn chatMsgEvent connOrGroupId = do msg@SndMessage {msgId, msgBody} <- createSndMessage chatMsgEvent connOrGroupId - deliverMessage conn (toCMEventTag chatMsgEvent) msgBody msgId - pure msg + (msg,) <$> deliverMessage conn (toCMEventTag chatMsgEvent) msgBody msgId -createSndMessage :: ChatMonad m => ChatMsgEvent -> ConnOrGroupId -> m SndMessage +createSndMessage :: (MsgEncodingI e, ChatMonad m) => ChatMsgEvent e -> ConnOrGroupId -> m SndMessage createSndMessage chatMsgEvent connOrGroupId = do gVar <- asks idsDrg withStore $ \db -> createNewSndMessage db gVar connOrGroupId $ \sharedMsgId -> let msgBody = strEncode ChatMessage {msgId = Just sharedMsgId, chatMsgEvent} in NewMessage {chatMsgEvent, msgBody} -directMessage :: ChatMsgEvent -> ByteString +directMessage :: MsgEncodingI e => ChatMsgEvent e -> ByteString directMessage chatMsgEvent = strEncode ChatMessage {msgId = Nothing, chatMsgEvent} -deliverMessage :: ChatMonad m => Connection -> CMEventTag -> MsgBody -> MessageId -> m () +deliverMessage :: ChatMonad m => Connection -> CMEventTag e -> MsgBody -> MessageId -> m Int64 deliverMessage conn@Connection {connId} cmEventTag msgBody msgId = do let msgFlags = MsgFlags {notification = hasNotification cmEventTag} agentMsgId <- withAgent $ \a -> sendMessage a (aConnId conn) msgFlags msgBody let sndMsgDelivery = SndMsgDelivery {connId, agentMsgId} withStore' $ \db -> createSndMsgDelivery db sndMsgDelivery msgId -sendGroupMessage :: ChatMonad m => GroupInfo -> [GroupMember] -> ChatMsgEvent -> m SndMessage +sendGroupMessage :: (MsgEncodingI e, ChatMonad m) => GroupInfo -> [GroupMember] -> ChatMsgEvent e -> m SndMessage sendGroupMessage GroupInfo {groupId} members chatMsgEvent = sendGroupMessage' members chatMsgEvent groupId Nothing $ pure () -sendXGrpMemInv :: ChatMonad m => GroupInfo -> GroupMember -> ChatMsgEvent -> Int64 -> m SndMessage +sendXGrpMemInv :: (MsgEncodingI e, ChatMonad m) => GroupInfo -> GroupMember -> ChatMsgEvent e -> Int64 -> m SndMessage sendXGrpMemInv GroupInfo {groupId} reMember chatMsgEvent introId = sendGroupMessage' [reMember] chatMsgEvent groupId (Just introId) $ withStore' $ \db -> updateIntroStatus db introId GMIntroInvForwarded -sendGroupMessage' :: ChatMonad m => [GroupMember] -> ChatMsgEvent -> Int64 -> Maybe Int64 -> m () -> m SndMessage +sendGroupMessage' :: (MsgEncodingI e, ChatMonad m) => [GroupMember] -> ChatMsgEvent e -> Int64 -> Maybe Int64 -> m () -> m SndMessage sendGroupMessage' members chatMsgEvent groupId introId_ postDeliver = do msg@SndMessage {msgId, msgBody} <- createSndMessage chatMsgEvent (GroupId groupId) -- TODO collect failed deliveries into a single error @@ -2708,8 +2831,8 @@ sendGroupMessage' members chatMsgEvent groupId introId_ postDeliver = do Nothing -> withStore' $ \db -> createPendingGroupMessage db groupMemberId msgId introId_ Just conn@Connection {connStatus} | connStatus == ConnSndReady || connStatus == ConnReady -> do - let tag = toCMEventTag chatMsgEvent - (deliverMessage conn tag msgBody msgId >> postDeliver) `catchError` const (pure ()) + let tag = toCMEventTag chatMsgEvent + (deliverMessage conn tag msgBody msgId >> postDeliver) `catchError` const (pure ()) | connStatus == ConnDeleted -> pure () | otherwise -> withStore' $ \db -> createPendingGroupMessage db groupMemberId msgId introId_ pure msg @@ -2718,16 +2841,18 @@ sendPendingGroupMessages :: ChatMonad m => GroupMember -> Connection -> m () sendPendingGroupMessages GroupMember {groupMemberId, localDisplayName} conn = do pendingMessages <- withStore' $ \db -> getPendingGroupMessages db groupMemberId -- TODO ensure order - pending messages interleave with user input messages - forM_ pendingMessages $ \PendingGroupMessage {msgId, cmEventTag, msgBody, introId_} -> do - deliverMessage conn cmEventTag msgBody msgId + forM_ pendingMessages $ \PendingGroupMessage {msgId, cmEventTag = ACMEventTag _ tag, msgBody, introId_} -> do + void $ deliverMessage conn tag msgBody msgId withStore' $ \db -> deletePendingGroupMessage db groupMemberId msgId - when (cmEventTag == XGrpMemFwd_) $ case introId_ of - Nothing -> throwChatError $ CEGroupMemberIntroNotFound localDisplayName - Just introId -> withStore' $ \db -> updateIntroStatus db introId GMIntroInvForwarded + case tag of + XGrpMemFwd_ -> case introId_ of + Just introId -> withStore' $ \db -> updateIntroStatus db introId GMIntroInvForwarded + _ -> throwChatError $ CEGroupMemberIntroNotFound localDisplayName + _ -> pure () saveRcvMSG :: ChatMonad m => Connection -> ConnOrGroupId -> MsgMeta -> MsgBody -> CommandId -> m RcvMessage saveRcvMSG Connection {connId} connOrGroupId agentMsgMeta msgBody agentAckCmdId = do - ChatMessage {msgId = sharedMsgId_, chatMsgEvent} <- liftEither $ parseChatMessage msgBody + ACMsg _ ChatMessage {msgId = sharedMsgId_, chatMsgEvent} <- parseAChatMessage msgBody let agentMsgId = fst $ recipient agentMsgMeta newMsg = NewMessage {chatMsgEvent, msgBody} rcvMsgDelivery = RcvMsgDelivery {connId, agentMsgId, agentMsgMeta, agentAckCmdId} @@ -2769,7 +2894,7 @@ joinAgentConnectionAsync user enableNtfs cReqUri cInfo = do connId <- withAgent $ \a -> joinConnectionAsync a (aCorrId cmdId) enableNtfs cReqUri cInfo pure (cmdId, connId) -allowAgentConnectionAsync :: ChatMonad m => User -> Connection -> ConfirmationId -> ChatMsgEvent -> m () +allowAgentConnectionAsync :: (MsgEncodingI e, ChatMonad m) => User -> Connection -> ConfirmationId -> ChatMsgEvent e -> m () allowAgentConnectionAsync user conn@Connection {connId} confId msg = do cmdId <- withStore' $ \db -> createCommand db user (Just connId) CFAllowConn withAgent $ \a -> allowConnectionAsync a (aCorrId cmdId) (aConnId conn) confId $ directMessage msg @@ -2833,9 +2958,9 @@ getCreateActiveUser st = do Just n | n <= 0 || n > length users -> putStrLn "invalid user number" >> loop | otherwise -> do - let user = users !! (n - 1) - withTransaction st (`setActiveUser` userId user) - pure user + let user = users !! (n - 1) + withTransaction st (`setActiveUser` userId user) + pure user userStr :: User -> String userStr User {localDisplayName, profile = LocalProfile {fullName}} = T.unpack $ localDisplayName <> if T.null fullName || localDisplayName == fullName then "" else " (" <> fullName <> ")" diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index bb96523705..8cc9d5879e 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -67,12 +67,29 @@ data ChatConfig = ChatConfig defaultServers :: InitialAgentServers, tbqSize :: Natural, fileChunkSize :: Integer, + inlineFiles :: InlineFilesConfig, subscriptionConcurrency :: Int, subscriptionEvents :: Bool, hostEvents :: Bool, testView :: Bool } +data InlineFilesConfig = InlineFilesConfig + { offerChunks :: Integer, + sendChunks :: Integer, + totalSendChunks :: Integer, + receiveChunks :: Integer + } + +defaultInlineFilesConfig :: InlineFilesConfig +defaultInlineFilesConfig = + InlineFilesConfig + { offerChunks = 15, -- max when chunks are offered - limited to 255 on the encoding level + sendChunks = 0, -- max per file when chunks will be sent inline without acceptance + totalSendChunks = 30, -- max per conversation when chunks will be sent inline without acceptance + receiveChunks = 5 -- max when chunks are accepted + } + data ActiveTo = ActiveNone | ActiveC ContactName | ActiveG GroupName deriving (Eq) diff --git a/src/Simplex/Chat/Messages.hs b/src/Simplex/Chat/Messages.hs index 665e23f25f..10315a163b 100644 --- a/src/Simplex/Chat/Messages.hs +++ b/src/Simplex/Chat/Messages.hs @@ -338,6 +338,8 @@ data CIFileStatus (d :: MsgDirection) where CIFSRcvComplete :: CIFileStatus 'MDRcv CIFSRcvCancelled :: CIFileStatus 'MDRcv +deriving instance Eq (CIFileStatus d) + deriving instance Show (CIFileStatus d) ciFileEnded :: CIFileStatus d -> Bool @@ -836,8 +838,8 @@ instance ChatTypeI 'CTDirect where chatTypeI = SCTDirect instance ChatTypeI 'CTGroup where chatTypeI = SCTGroup -data NewMessage = NewMessage - { chatMsgEvent :: ChatMsgEvent, +data NewMessage e = NewMessage + { chatMsgEvent :: ChatMsgEvent e, msgBody :: MsgBody } deriving (Show) @@ -850,14 +852,14 @@ data SndMessage = SndMessage data RcvMessage = RcvMessage { msgId :: MessageId, - chatMsgEvent :: ChatMsgEvent, + chatMsgEvent :: AChatMsgEvent, sharedMsgId_ :: Maybe SharedMsgId, msgBody :: MsgBody } data PendingGroupMessage = PendingGroupMessage { msgId :: MessageId, - cmEventTag :: CMEventTag, + cmEventTag :: ACMEventTag, msgBody :: MsgBody, introId_ :: Maybe Int64 } diff --git a/src/Simplex/Chat/Migrations/M20221012_inline_files.hs b/src/Simplex/Chat/Migrations/M20221012_inline_files.hs new file mode 100644 index 0000000000..51133d6b63 --- /dev/null +++ b/src/Simplex/Chat/Migrations/M20221012_inline_files.hs @@ -0,0 +1,20 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Migrations.M20221012_inline_files where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20221012_inline_files :: Query +m20221012_inline_files = + [sql| +DROP INDEX idx_messages_direct_shared_msg_id; + +ALTER TABLE files ADD COLUMN file_inline TEXT; +ALTER TABLE rcv_files ADD COLUMN rcv_file_inline TEXT; +ALTER TABLE rcv_files ADD COLUMN file_inline TEXT; +ALTER TABLE snd_files ADD COLUMN file_inline TEXT; +ALTER TABLE snd_files ADD COLUMN last_inline_msg_delivery_id INTEGER; + +CREATE UNIQUE INDEX idx_snd_files_last_inline_msg_delivery_id ON snd_files(last_inline_msg_delivery_id); +|] diff --git a/src/Simplex/Chat/Migrations/chat_schema.sql b/src/Simplex/Chat/Migrations/chat_schema.sql index 8145c20183..c5fcc446e9 100644 --- a/src/Simplex/Chat/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Migrations/chat_schema.sql @@ -182,7 +182,8 @@ CREATE TABLE files( chat_item_id INTEGER DEFAULT NULL REFERENCES chat_items ON DELETE CASCADE, updated_at TEXT CHECK(updated_at NOT NULL), cancelled INTEGER, - ci_file_status TEXT + ci_file_status TEXT, + file_inline TEXT ); CREATE TABLE snd_files( file_id INTEGER NOT NULL REFERENCES files ON DELETE CASCADE, @@ -191,6 +192,8 @@ CREATE TABLE snd_files( group_member_id INTEGER REFERENCES group_members ON DELETE CASCADE, created_at TEXT CHECK(created_at NOT NULL), updated_at TEXT CHECK(updated_at NOT NULL), + file_inline TEXT, + last_inline_msg_delivery_id INTEGER, PRIMARY KEY(file_id, connection_id) ) WITHOUT ROWID; CREATE TABLE rcv_files( @@ -200,7 +203,9 @@ CREATE TABLE rcv_files( file_queue_info BLOB , created_at TEXT CHECK(created_at NOT NULL), - updated_at TEXT CHECK(updated_at NOT NULL) + updated_at TEXT CHECK(updated_at NOT NULL), + rcv_file_inline TEXT, + file_inline TEXT ); CREATE TABLE snd_file_chunks( file_id INTEGER NOT NULL, @@ -370,11 +375,6 @@ CREATE TABLE smp_servers( UNIQUE(host, port) ); CREATE INDEX idx_messages_shared_msg_id ON messages(shared_msg_id); -CREATE UNIQUE INDEX idx_messages_direct_shared_msg_id ON messages( - connection_id, - shared_msg_id_user, - shared_msg_id -); CREATE INDEX idx_chat_items_shared_msg_id ON chat_items(shared_msg_id); CREATE TABLE calls( -- stores call invitations state for communicating state between NSE and app when call notification comes @@ -431,3 +431,6 @@ CREATE INDEX idx_msg_deliveries_message_id ON msg_deliveries(message_id); CREATE UNIQUE INDEX idx_user_contact_links_group_id ON user_contact_links( group_id ); +CREATE UNIQUE INDEX idx_snd_files_last_inline_msg_delivery_id ON snd_files( + last_inline_msg_delivery_id +); diff --git a/src/Simplex/Chat/Protocol.hs b/src/Simplex/Chat/Protocol.hs index 6a3d044925..6874d18bd3 100644 --- a/src/Simplex/Chat/Protocol.hs +++ b/src/Simplex/Chat/Protocol.hs @@ -9,7 +9,9 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE StrictData #-} +{-# LANGUAGE TypeApplications #-} module Simplex.Chat.Protocol where @@ -22,19 +24,25 @@ import qualified Data.Aeson.KeyMap as JM import qualified Data.Aeson.Types as JT import qualified Data.Attoparsec.ByteString.Char8 as A import Data.ByteString.Char8 (ByteString) +import qualified Data.ByteString.Char8 as B +import Data.ByteString.Internal (c2w, w2c) import qualified Data.ByteString.Lazy.Char8 as LB import Data.Maybe (fromMaybe) import Data.Text (Text) import Data.Text.Encoding (decodeLatin1, encodeUtf8) import Data.Time.Clock (UTCTime) +import Data.Type.Equality +import Data.Typeable (Typeable) +import Data.Word (Word32) import Database.SQLite.Simple.FromField (FromField (..)) import Database.SQLite.Simple.ToField (ToField (..)) import GHC.Generics (Generic) import Simplex.Chat.Call import Simplex.Chat.Types import Simplex.Chat.Util (safeDecodeUtf8) +import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String -import Simplex.Messaging.Parsers (fromTextField_, fstToLower, sumTypeJSON) +import Simplex.Messaging.Parsers (fromTextField_, fstToLower, parseAll, sumTypeJSON) import Simplex.Messaging.Util (eitherToMaybe, (<$?>)) data ConnectionEntity @@ -59,18 +67,64 @@ updateEntityConnStatus connEntity connStatus = case connEntity of where st c = c {connStatus} +data MsgEncoding = Binary | Json + +data SMsgEncoding (e :: MsgEncoding) where + SBinary :: SMsgEncoding 'Binary + SJson :: SMsgEncoding 'Json + +deriving instance Show (SMsgEncoding e) + +class MsgEncodingI (e :: MsgEncoding) where + encoding :: SMsgEncoding e + +instance MsgEncodingI 'Binary where encoding = SBinary + +instance MsgEncodingI 'Json where encoding = SJson + +data ACMEventTag = forall e. MsgEncodingI e => ACMEventTag (SMsgEncoding e) (CMEventTag e) + +instance TestEquality SMsgEncoding where + testEquality SBinary SBinary = Just Refl + testEquality SJson SJson = Just Refl + testEquality _ _ = Nothing + +checkEncoding :: forall t e e'. (MsgEncodingI e, MsgEncodingI e') => t e' -> Either String (t e) +checkEncoding x = case testEquality (encoding @e) (encoding @e') of + Just Refl -> Right x + Nothing -> Left "bad encoding" + +data AppMessage (e :: MsgEncoding) where + AMJson :: AppMessageJson -> AppMessage 'Json + AMBinary :: AppMessageBinary -> AppMessage 'Binary + -- chat message is sent as JSON with these properties -data AppMessage = AppMessage +data AppMessageJson = AppMessageJson { msgId :: Maybe SharedMsgId, event :: Text, params :: J.Object } deriving (Generic, FromJSON) -instance ToJSON AppMessage where +data AppMessageBinary = AppMessageBinary + { msgId :: Maybe SharedMsgId, + tag :: Char, + body :: ByteString + } + +instance ToJSON AppMessageJson where toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True} toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True} +instance StrEncoding AppMessageBinary where + strEncode AppMessageBinary {tag, msgId, body} = smpEncode (tag, msgId', Tail body) + where + msgId' = maybe B.empty (\(SharedMsgId mId') -> mId') msgId + strP = do + (tag, msgId', Tail body) <- smpP + let msgId = if B.null msgId' then Nothing else Just (SharedMsgId msgId') + pure AppMessageBinary {tag, msgId, body} + newtype SharedMsgId = SharedMsgId ByteString deriving (Eq, Show) @@ -105,51 +159,99 @@ instance ToJSON MsgRef where toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True} toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True} -data ChatMessage = ChatMessage {msgId :: Maybe SharedMsgId, chatMsgEvent :: ChatMsgEvent} +data ChatMessage e = ChatMessage {msgId :: Maybe SharedMsgId, chatMsgEvent :: ChatMsgEvent e} deriving (Eq, Show) -instance StrEncoding ChatMessage where - strEncode = LB.toStrict . J.encode . chatToAppMessage - strDecode = appToChatMessage <=< J.eitherDecodeStrict' - strP = strDecode <$?> A.takeByteString +data AChatMessage = forall e. MsgEncodingI e => ACMsg (SMsgEncoding e) (ChatMessage e) -data ChatMsgEvent - = XMsgNew MsgContainer - | XMsgUpdate SharedMsgId MsgContent - | XMsgDel SharedMsgId - | XMsgDeleted - | XFile FileInvitation -- TODO discontinue - | XFileAcpt String -- direct file protocol - | XFileAcptInv SharedMsgId ConnReqInvitation String -- group file protocol - | XFileCancel SharedMsgId - | XInfo Profile - | XContact Profile (Maybe XContactId) - | XGrpInv GroupInvitation - | XGrpAcpt MemberId - | XGrpMemNew MemberInfo - | XGrpMemIntro MemberInfo - | XGrpMemInv MemberId IntroInvitation - | XGrpMemFwd MemberInfo IntroInvitation - | XGrpMemInfo MemberId Profile - | XGrpMemRole MemberId GroupMemberRole - | XGrpMemCon MemberId -- TODO not implemented - | XGrpMemConAll MemberId -- TODO not implemented - | XGrpMemDel MemberId - | XGrpLeave - | XGrpDel - | XGrpInfo GroupProfile - | XInfoProbe Probe - | XInfoProbeCheck ProbeHash - | XInfoProbeOk Probe - | XCallInv CallId CallInvitation - | XCallOffer CallId CallOffer - | XCallAnswer CallId CallAnswer - | XCallExtra CallId CallExtraInfo - | XCallEnd CallId - | XOk - | XUnknown {event :: Text, params :: J.Object} +instance MsgEncodingI e => StrEncoding (ChatMessage e) where + strEncode msg = case chatToAppMessage msg of + AMJson m -> LB.toStrict $ J.encode m + AMBinary m -> strEncode m + strP = (\(ACMsg _ m) -> checkEncoding m) <$?> strP + +instance StrEncoding AChatMessage where + strEncode (ACMsg _ m) = strEncode m + strP = + A.peekChar' >>= \case + '{' -> ACMsg SJson <$> ((appJsonToCM <=< J.eitherDecodeStrict') <$?> A.takeByteString) + _ -> ACMsg SBinary <$> (appBinaryToCM <$?> strP) + +data ChatMsgEvent (e :: MsgEncoding) where + XMsgNew :: MsgContainer -> ChatMsgEvent 'Json + XMsgUpdate :: SharedMsgId -> MsgContent -> ChatMsgEvent 'Json + XMsgDel :: SharedMsgId -> ChatMsgEvent 'Json + XMsgDeleted :: ChatMsgEvent 'Json + XFile :: FileInvitation -> ChatMsgEvent 'Json -- TODO discontinue + XFileAcpt :: String -> ChatMsgEvent 'Json -- direct file protocol + XFileAcptInv :: SharedMsgId -> Maybe ConnReqInvitation -> String -> ChatMsgEvent 'Json + XFileCancel :: SharedMsgId -> ChatMsgEvent 'Json + XInfo :: Profile -> ChatMsgEvent 'Json + XContact :: Profile -> Maybe XContactId -> ChatMsgEvent 'Json + XGrpInv :: GroupInvitation -> ChatMsgEvent 'Json + XGrpAcpt :: MemberId -> ChatMsgEvent 'Json + XGrpMemNew :: MemberInfo -> ChatMsgEvent 'Json + XGrpMemIntro :: MemberInfo -> ChatMsgEvent 'Json + XGrpMemInv :: MemberId -> IntroInvitation -> ChatMsgEvent 'Json + XGrpMemFwd :: MemberInfo -> IntroInvitation -> ChatMsgEvent 'Json + XGrpMemInfo :: MemberId -> Profile -> ChatMsgEvent 'Json + XGrpMemRole :: MemberId -> GroupMemberRole -> ChatMsgEvent 'Json + XGrpMemCon :: MemberId -> ChatMsgEvent 'Json -- TODO not implemented + XGrpMemConAll :: MemberId -> ChatMsgEvent 'Json -- TODO not implemented + XGrpMemDel :: MemberId -> ChatMsgEvent 'Json + XGrpLeave :: ChatMsgEvent 'Json + XGrpDel :: ChatMsgEvent 'Json + XGrpInfo :: GroupProfile -> ChatMsgEvent 'Json + XInfoProbe :: Probe -> ChatMsgEvent 'Json + XInfoProbeCheck :: ProbeHash -> ChatMsgEvent 'Json + XInfoProbeOk :: Probe -> ChatMsgEvent 'Json + XCallInv :: CallId -> CallInvitation -> ChatMsgEvent 'Json + XCallOffer :: CallId -> CallOffer -> ChatMsgEvent 'Json + XCallAnswer :: CallId -> CallAnswer -> ChatMsgEvent 'Json + XCallExtra :: CallId -> CallExtraInfo -> ChatMsgEvent 'Json + XCallEnd :: CallId -> ChatMsgEvent 'Json + XOk :: ChatMsgEvent 'Json + XUnknown :: {event :: Text, params :: J.Object} -> ChatMsgEvent 'Json + BFileChunk :: SharedMsgId -> FileChunk -> ChatMsgEvent 'Binary + +deriving instance Eq (ChatMsgEvent e) + +deriving instance Show (ChatMsgEvent e) + +data AChatMsgEvent = forall e. MsgEncodingI e => ACME (SMsgEncoding e) (ChatMsgEvent e) + +deriving instance Show AChatMsgEvent + +data FileChunk = FileChunk {chunkNo :: Integer, chunkBytes :: ByteString} | FileChunkCancel deriving (Eq, Show) +instance Encoding FileChunk where + smpEncode = \case + FileChunk {chunkNo, chunkBytes} -> smpEncode ('F', fromIntegral chunkNo :: Word32, Tail chunkBytes) + FileChunkCancel -> smpEncode 'C' + smpP = + smpP >>= \case + 'F' -> do + chunkNo <- fromIntegral <$> smpP @Word32 + Tail chunkBytes <- smpP + pure FileChunk {chunkNo, chunkBytes} + 'C' -> pure FileChunkCancel + _ -> fail "bad FileChunk" + +newtype InlineFileChunk = IFC {unIFC :: FileChunk} + +instance Encoding InlineFileChunk where + smpEncode (IFC chunk) = case chunk of + FileChunk {chunkNo, chunkBytes} -> smpEncode (w2c $ fromIntegral chunkNo, Tail chunkBytes) + FileChunkCancel -> smpEncode '\NUL' + smpP = do + c <- A.anyChar + IFC <$> case c of + '\NUL' -> pure FileChunkCancel + _ -> do + Tail chunkBytes <- smpP + pure FileChunk {chunkNo = fromIntegral $ c2w c, chunkBytes} + data QuotedMsg = QuotedMsg {msgRef :: MsgRef, content :: MsgContent} deriving (Eq, Show, Generic, FromJSON) @@ -157,9 +259,9 @@ instance ToJSON QuotedMsg where toEncoding = J.genericToEncoding J.defaultOptions toJSON = J.genericToJSON J.defaultOptions -cmToQuotedMsg :: ChatMsgEvent -> Maybe QuotedMsg +cmToQuotedMsg :: AChatMsgEvent -> Maybe QuotedMsg cmToQuotedMsg = \case - XMsgNew (MCQuote quotedMsg _) -> Just quotedMsg + ACME _ (XMsgNew (MCQuote quotedMsg _)) -> Just quotedMsg _ -> Nothing data MsgContentTag = MCText_ | MCLink_ | MCImage_ | MCFile_ | MCUnknown_ Text @@ -273,7 +375,7 @@ msgContainerJSON = \case where withFile l = \case Nothing -> l - Just f -> l <> ["file" .= fileInvitationJSON f] + Just f -> l <> ["file" .= f] instance ToJSON MsgContent where toJSON = \case @@ -295,44 +397,48 @@ instance ToField MsgContent where instance FromField MsgContent where fromField = fromTextField_ $ J.decode . LB.fromStrict . encodeUtf8 -data CMEventTag - = XMsgNew_ - | XMsgUpdate_ - | XMsgDel_ - | XMsgDeleted_ - | XFile_ - | XFileAcpt_ - | XFileAcptInv_ - | XFileCancel_ - | XInfo_ - | XContact_ - | XGrpInv_ - | XGrpAcpt_ - | XGrpMemNew_ - | XGrpMemIntro_ - | XGrpMemInv_ - | XGrpMemFwd_ - | XGrpMemInfo_ - | XGrpMemRole_ - | XGrpMemCon_ - | XGrpMemConAll_ - | XGrpMemDel_ - | XGrpLeave_ - | XGrpDel_ - | XGrpInfo_ - | XInfoProbe_ - | XInfoProbeCheck_ - | XInfoProbeOk_ - | XCallInv_ - | XCallOffer_ - | XCallAnswer_ - | XCallExtra_ - | XCallEnd_ - | XOk_ - | XUnknown_ Text - deriving (Eq, Show) +data CMEventTag (e :: MsgEncoding) where + XMsgNew_ :: CMEventTag 'Json + XMsgUpdate_ :: CMEventTag 'Json + XMsgDel_ :: CMEventTag 'Json + XMsgDeleted_ :: CMEventTag 'Json + XFile_ :: CMEventTag 'Json + XFileAcpt_ :: CMEventTag 'Json + XFileAcptInv_ :: CMEventTag 'Json + XFileCancel_ :: CMEventTag 'Json + XInfo_ :: CMEventTag 'Json + XContact_ :: CMEventTag 'Json + XGrpInv_ :: CMEventTag 'Json + XGrpAcpt_ :: CMEventTag 'Json + XGrpMemNew_ :: CMEventTag 'Json + XGrpMemIntro_ :: CMEventTag 'Json + XGrpMemInv_ :: CMEventTag 'Json + XGrpMemFwd_ :: CMEventTag 'Json + XGrpMemInfo_ :: CMEventTag 'Json + XGrpMemRole_ :: CMEventTag 'Json + XGrpMemCon_ :: CMEventTag 'Json + XGrpMemConAll_ :: CMEventTag 'Json + XGrpMemDel_ :: CMEventTag 'Json + XGrpLeave_ :: CMEventTag 'Json + XGrpDel_ :: CMEventTag 'Json + XGrpInfo_ :: CMEventTag 'Json + XInfoProbe_ :: CMEventTag 'Json + XInfoProbeCheck_ :: CMEventTag 'Json + XInfoProbeOk_ :: CMEventTag 'Json + XCallInv_ :: CMEventTag 'Json + XCallOffer_ :: CMEventTag 'Json + XCallAnswer_ :: CMEventTag 'Json + XCallExtra_ :: CMEventTag 'Json + XCallEnd_ :: CMEventTag 'Json + XOk_ :: CMEventTag 'Json + XUnknown_ :: Text -> CMEventTag 'Json + BFileChunk_ :: CMEventTag 'Binary -instance StrEncoding CMEventTag where +deriving instance Show (CMEventTag e) + +deriving instance Eq (CMEventTag e) + +instance MsgEncodingI e => StrEncoding (CMEventTag e) where strEncode = \case XMsgNew_ -> "x.msg.new" XMsgUpdate_ -> "x.msg.update" @@ -368,45 +474,54 @@ instance StrEncoding CMEventTag where XCallEnd_ -> "x.call.end" XOk_ -> "x.ok" XUnknown_ t -> encodeUtf8 t - strDecode = \case - "x.msg.new" -> Right XMsgNew_ - "x.msg.update" -> Right XMsgUpdate_ - "x.msg.del" -> Right XMsgDel_ - "x.msg.deleted" -> Right XMsgDeleted_ - "x.file" -> Right XFile_ - "x.file.acpt" -> Right XFileAcpt_ - "x.file.acpt.inv" -> Right XFileAcptInv_ - "x.file.cancel" -> Right XFileCancel_ - "x.info" -> Right XInfo_ - "x.contact" -> Right XContact_ - "x.grp.inv" -> Right XGrpInv_ - "x.grp.acpt" -> Right XGrpAcpt_ - "x.grp.mem.new" -> Right XGrpMemNew_ - "x.grp.mem.intro" -> Right XGrpMemIntro_ - "x.grp.mem.inv" -> Right XGrpMemInv_ - "x.grp.mem.fwd" -> Right XGrpMemFwd_ - "x.grp.mem.info" -> Right XGrpMemInfo_ - "x.grp.mem.role" -> Right XGrpMemRole_ - "x.grp.mem.con" -> Right XGrpMemCon_ - "x.grp.mem.con.all" -> Right XGrpMemConAll_ - "x.grp.mem.del" -> Right XGrpMemDel_ - "x.grp.leave" -> Right XGrpLeave_ - "x.grp.del" -> Right XGrpDel_ - "x.grp.info" -> Right XGrpInfo_ - "x.info.probe" -> Right XInfoProbe_ - "x.info.probe.check" -> Right XInfoProbeCheck_ - "x.info.probe.ok" -> Right XInfoProbeOk_ - "x.call.inv" -> Right XCallInv_ - "x.call.offer" -> Right XCallOffer_ - "x.call.answer" -> Right XCallAnswer_ - "x.call.extra" -> Right XCallExtra_ - "x.call.end" -> Right XCallEnd_ - "x.ok" -> Right XOk_ - t -> Right . XUnknown_ $ safeDecodeUtf8 t + BFileChunk_ -> "F" + strDecode = (\(ACMEventTag _ t) -> checkEncoding t) <=< strDecode strP = strDecode <$?> A.takeTill (== ' ') -toCMEventTag :: ChatMsgEvent -> CMEventTag -toCMEventTag = \case +instance StrEncoding ACMEventTag where + strEncode (ACMEventTag _ t) = strEncode t + strP = + ((,) <$> A.peekChar' <*> A.takeTill (== ' ')) >>= \case + ('x', t) -> pure . ACMEventTag SJson $ case t of + "x.msg.new" -> XMsgNew_ + "x.msg.update" -> XMsgUpdate_ + "x.msg.del" -> XMsgDel_ + "x.msg.deleted" -> XMsgDeleted_ + "x.file" -> XFile_ + "x.file.acpt" -> XFileAcpt_ + "x.file.acpt.inv" -> XFileAcptInv_ + "x.file.cancel" -> XFileCancel_ + "x.info" -> XInfo_ + "x.contact" -> XContact_ + "x.grp.inv" -> XGrpInv_ + "x.grp.acpt" -> XGrpAcpt_ + "x.grp.mem.new" -> XGrpMemNew_ + "x.grp.mem.intro" -> XGrpMemIntro_ + "x.grp.mem.inv" -> XGrpMemInv_ + "x.grp.mem.fwd" -> XGrpMemFwd_ + "x.grp.mem.info" -> XGrpMemInfo_ + "x.grp.mem.role" -> XGrpMemRole_ + "x.grp.mem.con" -> XGrpMemCon_ + "x.grp.mem.con.all" -> XGrpMemConAll_ + "x.grp.mem.del" -> XGrpMemDel_ + "x.grp.leave" -> XGrpLeave_ + "x.grp.del" -> XGrpDel_ + "x.grp.info" -> XGrpInfo_ + "x.info.probe" -> XInfoProbe_ + "x.info.probe.check" -> XInfoProbeCheck_ + "x.info.probe.ok" -> XInfoProbeOk_ + "x.call.inv" -> XCallInv_ + "x.call.offer" -> XCallOffer_ + "x.call.answer" -> XCallAnswer_ + "x.call.extra" -> XCallExtra_ + "x.call.end" -> XCallEnd_ + "x.ok" -> XOk_ + _ -> XUnknown_ $ safeDecodeUtf8 t + (_, "F") -> pure $ ACMEventTag SBinary BFileChunk_ + _ -> fail "bad ACMEventTag" + +toCMEventTag :: ChatMsgEvent e -> CMEventTag e +toCMEventTag msg = case msg of XMsgNew _ -> XMsgNew_ XMsgUpdate _ _ -> XMsgUpdate_ XMsgDel _ -> XMsgDel_ @@ -441,18 +556,25 @@ toCMEventTag = \case XCallEnd _ -> XCallEnd_ XOk -> XOk_ XUnknown t _ -> XUnknown_ t + BFileChunk _ _ -> BFileChunk_ -cmEventTagT :: Text -> Maybe CMEventTag -cmEventTagT = eitherToMaybe . strDecode . encodeUtf8 +instance MsgEncodingI e => TextEncoding (CMEventTag e) where + textEncode = decodeLatin1 . strEncode + textDecode = eitherToMaybe . strDecode . encodeUtf8 -serializeCMEventTag :: CMEventTag -> Text -serializeCMEventTag = decodeLatin1 . strEncode +instance TextEncoding ACMEventTag where + textEncode (ACMEventTag _ t) = textEncode t + textDecode = eitherToMaybe . strDecode . encodeUtf8 -instance FromField CMEventTag where fromField = fromTextField_ cmEventTagT +instance (MsgEncodingI e, Typeable e) => FromField (CMEventTag e) where fromField = fromTextField_ textDecode -instance ToField CMEventTag where toField = toField . serializeCMEventTag +instance MsgEncodingI e => ToField (CMEventTag e) where toField = toField . textEncode -hasNotification :: CMEventTag -> Bool +instance FromField ACMEventTag where fromField = fromTextField_ textDecode + +instance ToField ACMEventTag where toField = toField . textEncode + +hasNotification :: CMEventTag e -> Bool hasNotification = \case XMsgNew_ -> True XFile_ -> True @@ -463,8 +585,18 @@ hasNotification = \case XCallInv_ -> True _ -> False -appToChatMessage :: AppMessage -> Either String ChatMessage -appToChatMessage AppMessage {msgId, event, params} = do +appBinaryToCM :: AppMessageBinary -> Either String (ChatMessage 'Binary) +appBinaryToCM AppMessageBinary {msgId, tag, body} = do + eventTag <- strDecode $ B.singleton tag + chatMsgEvent <- parseAll (msg eventTag) body + pure ChatMessage {msgId, chatMsgEvent} + where + msg :: CMEventTag 'Binary -> A.Parser (ChatMsgEvent 'Binary) + msg = \case + BFileChunk_ -> BFileChunk <$> (SharedMsgId <$> smpP) <*> (unIFC <$> smpP) + +appJsonToCM :: AppMessageJson -> Either String (ChatMessage 'Json) +appJsonToCM AppMessageJson {msgId, event, params} = do eventTag <- strDecode $ encodeUtf8 event chatMsgEvent <- msg eventTag pure ChatMessage {msgId, chatMsgEvent} @@ -473,6 +605,7 @@ appToChatMessage AppMessage {msgId, event, params} = do p key = JT.parseEither (.: key) params opt :: FromJSON a => J.Key -> Either String (Maybe a) opt key = JT.parseEither (.:? key) params + msg :: CMEventTag 'Json -> Either String (ChatMsgEvent 'Json) msg = \case XMsgNew_ -> XMsgNew <$> JT.parseEither parseMsgContainer params XMsgUpdate_ -> XMsgUpdate <$> p "msgId" <*> p "content" @@ -480,7 +613,7 @@ appToChatMessage AppMessage {msgId, event, params} = do XMsgDeleted_ -> pure XMsgDeleted XFile_ -> XFile <$> p "file" XFileAcpt_ -> XFileAcpt <$> p "fileName" - XFileAcptInv_ -> XFileAcptInv <$> p "msgId" <*> p "fileConnReq" <*> p "fileName" + XFileAcptInv_ -> XFileAcptInv <$> p "msgId" <*> opt "fileConnReq" <*> p "fileName" XFileCancel_ -> XFileCancel <$> p "msgId" XInfo_ -> XInfo <$> p "profile" XContact_ -> XContact <$> p "profile" <*> opt "contactReqId" @@ -509,21 +642,29 @@ appToChatMessage AppMessage {msgId, event, params} = do XOk_ -> pure XOk XUnknown_ t -> pure $ XUnknown t params -chatToAppMessage :: ChatMessage -> AppMessage -chatToAppMessage ChatMessage {msgId, chatMsgEvent} = AppMessage {msgId, event, params} +chatToAppMessage :: forall e. MsgEncodingI e => ChatMessage e -> AppMessage e +chatToAppMessage ChatMessage {msgId, chatMsgEvent} = case encoding @e of + SBinary -> + let (binaryMsgId, body) = toBody chatMsgEvent + in AMBinary AppMessageBinary {msgId = binaryMsgId, tag = B.head $ strEncode tag, body} + SJson -> AMJson AppMessageJson {msgId, event = textEncode tag, params = params chatMsgEvent} where - event = serializeCMEventTag . toCMEventTag $ chatMsgEvent + tag = toCMEventTag chatMsgEvent o :: [(J.Key, J.Value)] -> J.Object o = JM.fromList key .=? value = maybe id ((:) . (key .=)) value - params = case chatMsgEvent of + toBody :: ChatMsgEvent 'Binary -> (Maybe SharedMsgId, ByteString) + toBody = \case + BFileChunk (SharedMsgId msgId') chunk -> (Nothing, smpEncode (msgId', IFC chunk)) + params :: ChatMsgEvent 'Json -> J.Object + params = \case XMsgNew container -> msgContainerJSON container XMsgUpdate msgId' content -> o ["msgId" .= msgId', "content" .= content] XMsgDel msgId' -> o ["msgId" .= msgId'] XMsgDeleted -> JM.empty - XFile fileInv -> o ["file" .= fileInvitationJSON fileInv] + XFile fileInv -> o ["file" .= fileInv] XFileAcpt fileName -> o ["fileName" .= fileName] - XFileAcptInv sharedMsgId fileConnReq fileName -> o ["msgId" .= sharedMsgId, "fileConnReq" .= fileConnReq, "fileName" .= fileName] + XFileAcptInv sharedMsgId fileConnReq fileName -> o $ ("fileConnReq" .=? fileConnReq) ["msgId" .= sharedMsgId, "fileName" .= fileName] XFileCancel sharedMsgId -> o ["msgId" .= sharedMsgId] XInfo profile -> o ["profile" .= profile] XContact profile xContactId -> o $ ("contactReqId" .=? xContactId) ["profile" .= profile] @@ -551,8 +692,3 @@ chatToAppMessage ChatMessage {msgId, chatMsgEvent} = AppMessage {msgId, event, p XCallEnd callId -> o ["callId" .= callId] XOk -> JM.empty XUnknown _ ps -> ps - -fileInvitationJSON :: FileInvitation -> J.Object -fileInvitationJSON FileInvitation {fileName, fileSize, fileConnReq} = case fileConnReq of - Nothing -> JM.fromList ["fileName" .= fileName, "fileSize" .= fileSize] - Just fConnReq -> JM.fromList ["fileName" .= fileName, "fileSize" .= fileSize, "fileConnReq" .= fConnReq] diff --git a/src/Simplex/Chat/Store.hs b/src/Simplex/Chat/Store.hs index 042c840018..d2f7563307 100644 --- a/src/Simplex/Chat/Store.hs +++ b/src/Simplex/Chat/Store.hs @@ -111,11 +111,15 @@ module Simplex.Chat.Store matchReceivedProbeHash, matchSentProbe, mergeContactRecords, - createSndFileTransfer, createSndDirectFileTransfer, createSndDirectFTConnection, createSndGroupFileTransfer, createSndGroupFileTransferConnection, + createSndDirectInlineFT, + createSndGroupInlineFT, + updateSndDirectFTDelivery, + updateSndGroupFTDelivery, + getSndInlineFTViaMsgDelivery, updateFileCancelled, updateCIFileStatus, getSharedMsgIdByFileId, @@ -132,6 +136,8 @@ module Simplex.Chat.Store createRcvGroupFileTransfer, getRcvFileTransfer, acceptRcvFileTransfer, + acceptRcvInlineFT, + startRcvInlineFT, updateRcvFileStatus, createRcvFileChunk, updatedRcvFileChunkStored, @@ -139,6 +145,7 @@ module Simplex.Chat.Store updateFileTransferChatItemId, getFileTransfer, getFileTransferProgress, + getFileTransferMeta, getSndFileTransfer, getContactFileInfo, getContactMaxItemTs, @@ -270,6 +277,7 @@ import Simplex.Chat.Migrations.M20221001_shared_msg_id_indices import Simplex.Chat.Migrations.M20221003_delete_broken_integrity_error_chat_items import Simplex.Chat.Migrations.M20221004_idx_msg_deliveries_message_id import Simplex.Chat.Migrations.M20221011_user_contact_links_group_id +import Simplex.Chat.Migrations.M20221012_inline_files import Simplex.Chat.Protocol import Simplex.Chat.Types import Simplex.Messaging.Agent.Protocol (ACorrId, AgentMsgId, ConnId, InvitationId, MsgMeta (..)) @@ -310,7 +318,8 @@ schemaMigrations = ("20221001_shared_msg_id_indices", m20221001_shared_msg_id_indices), ("20221003_delete_broken_integrity_error_chat_items", m20221003_delete_broken_integrity_error_chat_items), ("20221004_idx_msg_deliveries_message_id", m20221004_idx_msg_deliveries_message_id), - ("20221011_user_contact_links_group_id", m20221011_user_contact_links_group_id) + ("20221011_user_contact_links_group_id", m20221011_user_contact_links_group_id), + ("20221012_inline_files", m20221012_inline_files) ] -- | The list of migrations in ascending order by date @@ -570,28 +579,28 @@ deleteContactProfile_ db userId contactId = updateUserProfile :: DB.Connection -> User -> Profile -> ExceptT StoreError IO () updateUserProfile db User {userId, userContactId, localDisplayName, profile = LocalProfile {profileId, displayName}} p'@Profile {displayName = newName} | displayName == newName = - liftIO $ updateContactProfile_ db userId profileId p' + liftIO $ updateContactProfile_ db userId profileId p' | otherwise = - checkConstraint SEDuplicateName . liftIO $ do - currentTs <- getCurrentTime - DB.execute db "UPDATE users SET local_display_name = ?, updated_at = ? WHERE user_id = ?" (newName, currentTs, userId) - DB.execute - db - "INSERT INTO display_names (local_display_name, ldn_base, user_id, created_at, updated_at) VALUES (?,?,?,?,?)" - (newName, newName, userId, currentTs, currentTs) - updateContactProfile_' db userId profileId p' currentTs - updateContact_ db userId userContactId localDisplayName newName currentTs + checkConstraint SEDuplicateName . liftIO $ do + currentTs <- getCurrentTime + DB.execute db "UPDATE users SET local_display_name = ?, updated_at = ? WHERE user_id = ?" (newName, currentTs, userId) + DB.execute + db + "INSERT INTO display_names (local_display_name, ldn_base, user_id, created_at, updated_at) VALUES (?,?,?,?,?)" + (newName, newName, userId, currentTs, currentTs) + updateContactProfile_' db userId profileId p' currentTs + updateContact_ db userId userContactId localDisplayName newName currentTs updateContactProfile :: DB.Connection -> UserId -> Contact -> Profile -> ExceptT StoreError IO Contact updateContactProfile db userId c@Contact {contactId, localDisplayName, profile = LocalProfile {profileId, displayName, localAlias}} p'@Profile {displayName = newName} | displayName == newName = - liftIO $ updateContactProfile_ db userId profileId p' $> (c :: Contact) {profile = toLocalProfile profileId p' localAlias} + liftIO $ updateContactProfile_ db userId profileId p' $> (c :: Contact) {profile = toLocalProfile profileId p' localAlias} | otherwise = - ExceptT . withLocalDisplayName db userId newName $ \ldn -> do - currentTs <- getCurrentTime - updateContactProfile_' db userId profileId p' currentTs - updateContact_ db userId contactId localDisplayName ldn currentTs - pure . Right $ (c :: Contact) {localDisplayName = ldn, profile = toLocalProfile profileId p' localAlias} + ExceptT . withLocalDisplayName db userId newName $ \ldn -> do + currentTs <- getCurrentTime + updateContactProfile_' db userId profileId p' currentTs + updateContact_ db userId contactId localDisplayName ldn currentTs + pure . Right $ (c :: Contact) {localDisplayName = ldn, profile = toLocalProfile profileId p' localAlias} updateContactAlias :: DB.Connection -> UserId -> Contact -> LocalAlias -> IO Contact updateContactAlias db userId c@Contact {profile = lp@LocalProfile {profileId}} localAlias = do @@ -1057,7 +1066,7 @@ getLiveSndFileTransfers db User {userId} = do SELECT DISTINCT f.file_id FROM files f JOIN snd_files s - WHERE f.user_id = ? AND s.file_status IN (?, ?, ?) + WHERE f.user_id = ? AND s.file_status IN (?, ?, ?) AND s.file_inline IS NULL |] (userId, FSNew, FSAccepted, FSConnected) concatMap (filter liveTransfer) . rights <$> mapM (getSndFileTransfers_ db userId) fileIds @@ -1075,7 +1084,7 @@ getLiveRcvFileTransfers db user@User {userId} = do SELECT f.file_id FROM files f JOIN rcv_files r - WHERE f.user_id = ? AND r.file_status IN (?, ?) + WHERE f.user_id = ? AND r.file_status IN (?, ?) AND r.rcv_file_inline IS NULL |] (userId, FSAccepted, FSConnected) rights <$> mapM (runExceptT . getRcvFileTransfer db user) fileIds @@ -1373,7 +1382,7 @@ getConnectionEntity db user@User {userId, userContactId} agentConnId = do DB.query db [sql| - SELECT s.file_status, f.file_name, f.file_size, f.chunk_size, f.file_path, cs.local_display_name, m.local_display_name + SELECT s.file_status, f.file_name, f.file_size, f.chunk_size, f.file_path, s.file_inline, cs.local_display_name, m.local_display_name FROM snd_files s JOIN files f USING (file_id) LEFT JOIN contacts cs USING (contact_id) @@ -1381,10 +1390,10 @@ getConnectionEntity db user@User {userId, userContactId} agentConnId = do WHERE f.user_id = ? AND f.file_id = ? AND s.connection_id = ? |] (userId, fileId, connId) - sndFileTransfer_ :: Int64 -> Int64 -> (FileStatus, String, Integer, Integer, FilePath, Maybe ContactName, Maybe ContactName) -> Either StoreError SndFileTransfer - sndFileTransfer_ fileId connId (fileStatus, fileName, fileSize, chunkSize, filePath, contactName_, memberName_) = + sndFileTransfer_ :: Int64 -> Int64 -> (FileStatus, String, Integer, Integer, FilePath, Maybe InlineFileMode, Maybe ContactName, Maybe ContactName) -> Either StoreError SndFileTransfer + sndFileTransfer_ fileId connId (fileStatus, fileName, fileSize, chunkSize, filePath, fileInline, contactName_, memberName_) = case contactName_ <|> memberName_ of - Just recipientDisplayName -> Right SndFileTransfer {fileId, fileStatus, fileName, fileSize, chunkSize, filePath, recipientDisplayName, connId, agentConnId} + Just recipientDisplayName -> Right SndFileTransfer {fileId, fileStatus, fileName, fileSize, chunkSize, filePath, fileInline, recipientDisplayName, connId, agentConnId} Nothing -> Left $ SESndFileInvalid fileId getUserContact_ :: Int64 -> ExceptT StoreError IO UserContact getUserContact_ userContactLinkId = ExceptT $ do @@ -2118,30 +2127,22 @@ getViaGroupContact db User {userId} GroupMember {groupMemberId} = activeConn = toConnection connRow in Contact {contactId, localDisplayName, profile, activeConn, viaGroup, chatSettings, createdAt, updatedAt} -createSndFileTransfer :: DB.Connection -> UserId -> Contact -> FilePath -> FileInvitation -> ConnId -> Integer -> IO Int64 -createSndFileTransfer db userId Contact {contactId} filePath FileInvitation {fileName, fileSize} acId chunkSize = do +createSndDirectFileTransfer :: DB.Connection -> UserId -> Contact -> FilePath -> FileInvitation -> Maybe ConnId -> Integer -> IO FileTransferMeta +createSndDirectFileTransfer db userId Contact {contactId} filePath FileInvitation {fileName, fileSize, fileInline} acId_ chunkSize = do currentTs <- getCurrentTime DB.execute db - "INSERT INTO files (user_id, contact_id, file_name, file_path, file_size, chunk_size, ci_file_status, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)" - (userId, contactId, fileName, filePath, fileSize, chunkSize, CIFSSndStored, currentTs, currentTs) + "INSERT INTO files (user_id, contact_id, file_name, file_path, file_size, chunk_size, file_inline, ci_file_status, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?)" + (userId, contactId, fileName, filePath, fileSize, chunkSize, fileInline, CIFSSndStored, currentTs, currentTs) fileId <- insertedRowId db - Connection {connId} <- createSndFileConnection_ db userId fileId acId - let fileStatus = FSNew - DB.execute - db - "INSERT INTO snd_files (file_id, file_status, connection_id, created_at, updated_at) VALUES (?,?,?,?,?)" - (fileId, fileStatus, connId, currentTs, currentTs) - pure fileId - -createSndDirectFileTransfer :: DB.Connection -> UserId -> Contact -> FilePath -> FileInvitation -> Integer -> IO Int64 -createSndDirectFileTransfer db userId Contact {contactId} filePath FileInvitation {fileName, fileSize} chunkSize = do - currentTs <- getCurrentTime - DB.execute - db - "INSERT INTO files (user_id, contact_id, file_name, file_path, file_size, chunk_size, ci_file_status, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)" - (userId, contactId, fileName, filePath, fileSize, chunkSize, CIFSSndStored, currentTs, currentTs) - insertedRowId db + forM_ acId_ $ \acId -> do + Connection {connId} <- createSndFileConnection_ db userId fileId acId + let fileStatus = FSNew + DB.execute + db + "INSERT INTO snd_files (file_id, file_status, file_inline, connection_id, created_at, updated_at) VALUES (?,?,?,?,?,?)" + (fileId, fileStatus, fileInline, connId, currentTs, currentTs) + pure FileTransferMeta {fileId, fileName, filePath, fileSize, fileInline, chunkSize, cancelled = False} createSndDirectFTConnection :: DB.Connection -> User -> Int64 -> (CommandId, ConnId) -> IO () createSndDirectFTConnection db user@User {userId} fileId (cmdId, acId) = do @@ -2153,14 +2154,15 @@ createSndDirectFTConnection db user@User {userId} fileId (cmdId, acId) = do "INSERT INTO snd_files (file_id, file_status, connection_id, created_at, updated_at) VALUES (?,?,?,?,?)" (fileId, FSAccepted, connId, currentTs, currentTs) -createSndGroupFileTransfer :: DB.Connection -> UserId -> GroupInfo -> FilePath -> FileInvitation -> Integer -> IO Int64 -createSndGroupFileTransfer db userId GroupInfo {groupId} filePath FileInvitation {fileName, fileSize} chunkSize = do +createSndGroupFileTransfer :: DB.Connection -> UserId -> GroupInfo -> FilePath -> FileInvitation -> Integer -> IO FileTransferMeta +createSndGroupFileTransfer db userId GroupInfo {groupId} filePath FileInvitation {fileName, fileSize, fileInline} chunkSize = do currentTs <- getCurrentTime DB.execute db - "INSERT INTO files (user_id, group_id, file_name, file_path, file_size, chunk_size, ci_file_status, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)" - (userId, groupId, fileName, filePath, fileSize, chunkSize, CIFSSndStored, currentTs, currentTs) - insertedRowId db + "INSERT INTO files (user_id, group_id, file_name, file_path, file_size, chunk_size, file_inline, ci_file_status, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?)" + (userId, groupId, fileName, filePath, fileSize, chunkSize, fileInline, CIFSSndStored, currentTs, currentTs) + fileId <- insertedRowId db + pure FileTransferMeta {fileId, fileName, filePath, fileSize, fileInline, chunkSize, cancelled = False} createSndGroupFileTransferConnection :: DB.Connection -> User -> Int64 -> (CommandId, ConnId) -> GroupMember -> IO () createSndGroupFileTransferConnection db user@User {userId} fileId (cmdId, acId) GroupMember {groupMemberId} = do @@ -2172,6 +2174,63 @@ createSndGroupFileTransferConnection db user@User {userId} fileId (cmdId, acId) "INSERT INTO snd_files (file_id, file_status, connection_id, group_member_id, created_at, updated_at) VALUES (?,?,?,?,?,?)" (fileId, FSAccepted, connId, groupMemberId, currentTs, currentTs) +createSndDirectInlineFT :: DB.Connection -> Contact -> FileTransferMeta -> IO SndFileTransfer +createSndDirectInlineFT db Contact {localDisplayName = n, activeConn = Connection {connId, agentConnId}} FileTransferMeta {fileId, fileName, filePath, fileSize, chunkSize, fileInline} = do + currentTs <- getCurrentTime + let fileStatus = FSConnected + fileInline' = Just $ fromMaybe (IFMOffer) fileInline + DB.execute + db + "INSERT INTO snd_files (file_id, file_status, file_inline, connection_id, created_at, updated_at) VALUES (?,?,?,?,?,?)" + (fileId, fileStatus, fileInline', connId, currentTs, currentTs) + pure SndFileTransfer {fileId, fileName, filePath, fileSize, chunkSize, recipientDisplayName = n, connId, agentConnId, fileStatus, fileInline = fileInline'} + +createSndGroupInlineFT :: DB.Connection -> GroupMember -> Connection -> FileTransferMeta -> IO SndFileTransfer +createSndGroupInlineFT db GroupMember {groupMemberId, localDisplayName = n} Connection {connId, agentConnId} FileTransferMeta {fileId, fileName, filePath, fileSize, chunkSize, fileInline} = do + currentTs <- getCurrentTime + let fileStatus = FSConnected + fileInline' = Just $ fromMaybe (IFMOffer) fileInline + DB.execute + db + "INSERT INTO snd_files (file_id, file_status, file_inline, connection_id, group_member_id, created_at, updated_at) VALUES (?,?,?,?,?,?,?)" + (fileId, fileStatus, fileInline', connId, groupMemberId, currentTs, currentTs) + pure SndFileTransfer {fileId, fileName, filePath, fileSize, chunkSize, recipientDisplayName = n, connId, agentConnId, fileStatus, fileInline = fileInline'} + +updateSndDirectFTDelivery :: DB.Connection -> Contact -> FileTransferMeta -> Int64 -> IO () +updateSndDirectFTDelivery db Contact {activeConn = Connection {connId}} FileTransferMeta {fileId} msgDeliveryId = + DB.execute + db + "UPDATE snd_files SET last_inline_msg_delivery_id = ? WHERE connection_id = ? AND file_id = ? AND file_inline IS NOT NULL" + (msgDeliveryId, connId, fileId) + +updateSndGroupFTDelivery :: DB.Connection -> GroupMember -> Connection -> FileTransferMeta -> Int64 -> IO () +updateSndGroupFTDelivery db GroupMember {groupMemberId} Connection {connId} FileTransferMeta {fileId} msgDeliveryId = + DB.execute + db + "UPDATE snd_files SET last_inline_msg_delivery_id = ? WHERE group_member_id = ? AND connection_id = ? AND file_id = ? AND file_inline IS NOT NULL" + (msgDeliveryId, groupMemberId, connId, fileId) + +getSndInlineFTViaMsgDelivery :: DB.Connection -> User -> Connection -> AgentMsgId -> IO (Maybe SndFileTransfer) +getSndInlineFTViaMsgDelivery db User {userId} Connection {connId, agentConnId} agentMsgId = do + (sndFileTransfer_ <=< listToMaybe) + <$> DB.query + db + [sql| + SELECT s.file_id, s.file_status, f.file_name, f.file_size, f.chunk_size, f.file_path, s.file_inline, c.local_display_name, m.local_display_name + FROM msg_deliveries d + JOIN snd_files s ON s.connection_id = d.connection_id AND s.last_inline_msg_delivery_id = d.msg_delivery_id + JOIN files f ON f.file_id = s.file_id + LEFT JOIN contacts c USING (contact_id) + LEFT JOIN group_members m USING (group_member_id) + WHERE d.connection_id = ? AND d.agent_msg_id = ? AND f.user_id = ? AND s.file_inline IS NOT NULL + |] + (connId, agentMsgId, userId) + where + sndFileTransfer_ :: (Int64, FileStatus, String, Integer, Integer, FilePath, Maybe InlineFileMode, Maybe ContactName, Maybe ContactName) -> Maybe SndFileTransfer + sndFileTransfer_ (fileId, fileStatus, fileName, fileSize, chunkSize, filePath, fileInline, contactName_, memberName_) = + (\n -> SndFileTransfer {fileId, fileStatus, fileName, fileSize, chunkSize, filePath, fileInline, recipientDisplayName = n, connId, agentConnId}) + <$> (contactName_ <|> memberName_) + updateFileCancelled :: MsgDirectionI d => DB.Connection -> User -> Int64 -> CIFileStatus d -> IO () updateFileCancelled db User {userId} fileId ciFileStatus = do currentTs <- getCurrentTime @@ -2308,43 +2367,44 @@ deleteSndFileChunks :: DB.Connection -> SndFileTransfer -> IO () deleteSndFileChunks db SndFileTransfer {fileId, connId} = DB.execute db "DELETE FROM snd_file_chunks WHERE file_id = ? AND connection_id = ?" (fileId, connId) -createRcvFileTransfer :: DB.Connection -> UserId -> Contact -> FileInvitation -> Integer -> IO RcvFileTransfer -createRcvFileTransfer db userId Contact {contactId, localDisplayName = c} f@FileInvitation {fileName, fileSize, fileConnReq} chunkSize = do +createRcvFileTransfer :: DB.Connection -> UserId -> Contact -> FileInvitation -> Maybe InlineFileMode -> Integer -> IO RcvFileTransfer +createRcvFileTransfer db userId Contact {contactId, localDisplayName = c} f@FileInvitation {fileName, fileSize, fileConnReq, fileInline} rcvFileInline chunkSize = do currentTs <- getCurrentTime DB.execute db - "INSERT INTO files (user_id, contact_id, file_name, file_size, chunk_size, ci_file_status, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)" - (userId, contactId, fileName, fileSize, chunkSize, CIFSRcvInvitation, currentTs, currentTs) + "INSERT INTO files (user_id, contact_id, file_name, file_size, chunk_size, file_inline, ci_file_status, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)" + (userId, contactId, fileName, fileSize, chunkSize, fileInline, CIFSRcvInvitation, currentTs, currentTs) fileId <- insertedRowId db DB.execute db - "INSERT INTO rcv_files (file_id, file_status, file_queue_info, created_at, updated_at) VALUES (?,?,?,?,?)" - (fileId, FSNew, fileConnReq, currentTs, currentTs) - pure RcvFileTransfer {fileId, fileInvitation = f, fileStatus = RFSNew, senderDisplayName = c, chunkSize, cancelled = False, grpMemberId = Nothing} + "INSERT INTO rcv_files (file_id, file_status, file_queue_info, file_inline, rcv_file_inline, created_at, updated_at) VALUES (?,?,?,?,?,?,?)" + (fileId, FSNew, fileConnReq, fileInline, rcvFileInline, currentTs, currentTs) + pure RcvFileTransfer {fileId, fileInvitation = f, fileStatus = RFSNew, rcvFileInline, senderDisplayName = c, chunkSize, cancelled = False, grpMemberId = Nothing} -createRcvGroupFileTransfer :: DB.Connection -> UserId -> GroupMember -> FileInvitation -> Integer -> IO RcvFileTransfer -createRcvGroupFileTransfer db userId GroupMember {groupId, groupMemberId, localDisplayName = c} f@FileInvitation {fileName, fileSize, fileConnReq} chunkSize = do +createRcvGroupFileTransfer :: DB.Connection -> UserId -> GroupMember -> FileInvitation -> Maybe InlineFileMode -> Integer -> IO RcvFileTransfer +createRcvGroupFileTransfer db userId GroupMember {groupId, groupMemberId, localDisplayName = c} f@FileInvitation {fileName, fileSize, fileConnReq, fileInline} rcvFileInline chunkSize = do currentTs <- getCurrentTime DB.execute db - "INSERT INTO files (user_id, group_id, file_name, file_size, chunk_size, ci_file_status, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)" - (userId, groupId, fileName, fileSize, chunkSize, CIFSRcvInvitation, currentTs, currentTs) + "INSERT INTO files (user_id, group_id, file_name, file_size, chunk_size, file_inline, ci_file_status, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)" + (userId, groupId, fileName, fileSize, chunkSize, fileInline, CIFSRcvInvitation, currentTs, currentTs) fileId <- insertedRowId db DB.execute db - "INSERT INTO rcv_files (file_id, file_status, file_queue_info, group_member_id, created_at, updated_at) VALUES (?,?,?,?,?,?)" - (fileId, FSNew, fileConnReq, groupMemberId, currentTs, currentTs) - pure RcvFileTransfer {fileId, fileInvitation = f, fileStatus = RFSNew, senderDisplayName = c, chunkSize, cancelled = False, grpMemberId = Just groupMemberId} + "INSERT INTO rcv_files (file_id, file_status, file_queue_info, file_inline, rcv_file_inline, group_member_id, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)" + (fileId, FSNew, fileConnReq, fileInline, rcvFileInline, groupMemberId, currentTs, currentTs) + pure RcvFileTransfer {fileId, fileInvitation = f, fileStatus = RFSNew, rcvFileInline, senderDisplayName = c, chunkSize, cancelled = False, grpMemberId = Just groupMemberId} getRcvFileTransfer :: DB.Connection -> User -> Int64 -> ExceptT StoreError IO RcvFileTransfer -getRcvFileTransfer db User {userId} fileId = - ExceptT . firstRow' rcvFileTransfer (SERcvFileNotFound fileId) $ - DB.query - db - [sql| +getRcvFileTransfer db user@User {userId} fileId = do + rftRow <- + ExceptT . firstRow id (SERcvFileNotFound fileId) $ + DB.query + db + [sql| SELECT r.file_status, r.file_queue_info, r.group_member_id, f.file_name, - f.file_size, f.chunk_size, f.cancelled, cs.local_display_name, m.local_display_name, - f.file_path, c.connection_id, c.agent_conn_id + f.file_size, f.chunk_size, f.cancelled, cs.contact_id, cs.local_display_name, m.group_id, m.group_member_id, m.local_display_name, + f.file_path, r.file_inline, r.rcv_file_inline, 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 @@ -2352,35 +2412,62 @@ getRcvFileTransfer db User {userId} fileId = LEFT JOIN group_members m USING (group_member_id) WHERE f.user_id = ? AND f.file_id = ? |] - (userId, fileId) + (userId, fileId) + rcvFileTransfer rftRow where rcvFileTransfer :: - (FileStatus, Maybe ConnReqInvitation, Maybe Int64, String, Integer, Integer, Maybe Bool, Maybe ContactName, Maybe ContactName, Maybe FilePath, Maybe Int64, Maybe AgentConnId) -> - Either StoreError RcvFileTransfer - rcvFileTransfer (fileStatus', fileConnReq, grpMemberId, fileName, fileSize, chunkSize, cancelled_, contactName_, memberName_, filePath_, connId_, agentConnId_) = - let fileInv = FileInvitation {fileName, fileSize, fileConnReq} - fileInfo = (filePath_, connId_, agentConnId_) - in case contactName_ <|> memberName_ of - Nothing -> Left $ SERcvFileInvalid fileId - Just name -> - case fileStatus' of - FSNew -> ft name fileInv RFSNew - FSAccepted -> ft name fileInv . RFSAccepted =<< rfi fileInfo - FSConnected -> ft name fileInv . RFSConnected =<< rfi fileInfo - FSComplete -> ft name fileInv . RFSComplete =<< rfi fileInfo - FSCancelled -> ft name fileInv . RFSCancelled $ rfi_ fileInfo + (FileStatus, Maybe ConnReqInvitation, Maybe Int64, String, Integer, Integer, Maybe Bool) :. (Maybe Int64, Maybe ContactName, Maybe Int64, Maybe Int64, Maybe ContactName, Maybe FilePath, Maybe InlineFileMode, Maybe InlineFileMode) :. (Maybe Int64, Maybe AgentConnId) -> + ExceptT StoreError IO RcvFileTransfer + rcvFileTransfer ((fileStatus', fileConnReq, grpMemberId, fileName, fileSize, chunkSize, cancelled_) :. (contactId_, contactName_, groupId_, groupMemberId_, memberName_, filePath_, fileInline, rcvFileInline) :. (connId_, agentConnId_)) = do + let fileInv = FileInvitation {fileName, fileSize, fileConnReq, fileInline} + fileInfo = (filePath_, connId_, agentConnId_, contactId_, groupId_, groupMemberId_, isJust fileInline) + case contactName_ <|> memberName_ of + Nothing -> throwError $ SERcvFileInvalid fileId + Just name -> do + case fileStatus' of + FSNew -> pure $ ft name fileInv RFSNew + FSAccepted -> ft name fileInv . RFSAccepted <$> rfi fileInfo + FSConnected -> ft name fileInv . RFSConnected <$> rfi fileInfo + FSComplete -> ft name fileInv . RFSComplete <$> rfi fileInfo + FSCancelled -> ft name fileInv . RFSCancelled <$> rfi_ fileInfo where ft senderDisplayName fileInvitation fileStatus = - Right RcvFileTransfer {fileId, fileInvitation, fileStatus, senderDisplayName, chunkSize, cancelled, grpMemberId} - rfi fileInfo = maybe (Left $ SERcvFileInvalid fileId) Right $ rfi_ fileInfo + RcvFileTransfer {fileId, fileInvitation, fileStatus, rcvFileInline, senderDisplayName, chunkSize, cancelled, grpMemberId} + rfi fileInfo = maybe (throwError $ SERcvFileInvalid fileId) pure =<< rfi_ fileInfo rfi_ = \case - (Just filePath, Just connId, Just agentConnId) -> Just RcvFileInfo {filePath, connId, agentConnId} - _ -> Nothing + (Just filePath, Just connId, Just agentConnId, _, _, _, _) -> pure $ Just RcvFileInfo {filePath, connId, agentConnId} + (Just filePath, Nothing, Nothing, Just contactId, _, _, True) -> do + Contact {activeConn = Connection {connId, agentConnId}} <- getContact db userId contactId + pure $ Just RcvFileInfo {filePath, connId, agentConnId} + (Just filePath, Nothing, Nothing, _, Just groupId, Just groupMemberId, True) -> do + getGroupMember db user groupId groupMemberId >>= \case + GroupMember {activeConn = Just Connection {connId, agentConnId}} -> + pure $ Just RcvFileInfo {filePath, connId, agentConnId} + _ -> pure Nothing + _ -> pure Nothing cancelled = fromMaybe False cancelled_ acceptRcvFileTransfer :: DB.Connection -> User -> Int64 -> ConnId -> ConnStatus -> FilePath -> ExceptT StoreError IO AChatItem acceptRcvFileTransfer db user@User {userId} fileId agentConnId connStatus filePath = ExceptT $ do currentTs <- getCurrentTime + acceptRcvFT_ db user fileId filePath currentTs + DB.execute + db + "INSERT INTO connections (agent_conn_id, conn_status, conn_type, rcv_file_id, user_id, created_at, updated_at) VALUES (?,?,?,?,?,?,?)" + (agentConnId, connStatus, ConnRcvFile, fileId, userId, currentTs, currentTs) + runExceptT $ getChatItemByFileId db user fileId + +acceptRcvInlineFT :: DB.Connection -> User -> Int64 -> FilePath -> ExceptT StoreError IO AChatItem +acceptRcvInlineFT db user fileId filePath = do + liftIO $ acceptRcvFT_ db user fileId filePath =<< getCurrentTime + getChatItemByFileId db user fileId + +startRcvInlineFT :: DB.Connection -> User -> RcvFileTransfer -> FilePath -> IO () +startRcvInlineFT db user RcvFileTransfer {fileId} filePath = + acceptRcvFT_ db user fileId filePath =<< getCurrentTime + +acceptRcvFT_ :: DB.Connection -> User -> Int64 -> FilePath -> UTCTime -> IO () +acceptRcvFT_ db User {userId} fileId filePath currentTs = do DB.execute db "UPDATE files SET file_path = ?, ci_file_status = ?, updated_at = ? WHERE user_id = ? AND file_id = ?" @@ -2389,11 +2476,6 @@ acceptRcvFileTransfer db user@User {userId} fileId agentConnId connStatus filePa db "UPDATE rcv_files SET file_status = ?, updated_at = ? WHERE file_id = ?" (FSAccepted, currentTs, fileId) - DB.execute - db - "INSERT INTO connections (agent_conn_id, conn_status, conn_type, rcv_file_id, user_id, created_at, updated_at) VALUES (?,?,?,?,?,?,?)" - (agentConnId, connStatus, ConnRcvFile, fileId, userId, currentTs, currentTs) - runExceptT $ getChatItemByFileId db user fileId updateRcvFileStatus :: DB.Connection -> RcvFileTransfer -> FileStatus -> IO () updateRcvFileStatus db RcvFileTransfer {fileId} status = do @@ -2416,20 +2498,20 @@ createRcvFileChunk db RcvFileTransfer {fileId, fileInvitation = FileInvitation { pure $ case map fromOnly ns of [] | chunkNo == 1 -> - if chunkSize >= fileSize - then RcvChunkFinal - else RcvChunkOk + if chunkSize >= fileSize + then RcvChunkFinal + else RcvChunkOk | otherwise -> 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 + let prevSize = n * chunkSize + in if prevSize >= fileSize + then RcvChunkError + else + if prevSize + chunkSize >= fileSize + then RcvChunkFinal + else RcvChunkOk | otherwise -> RcvChunkError updatedRcvFileChunkStored :: DB.Connection -> RcvFileTransfer -> Integer -> IO () @@ -2485,18 +2567,18 @@ getFileTransfer db user@User {userId} fileId = (userId, fileId) getSndFileTransfer :: DB.Connection -> User -> Int64 -> ExceptT StoreError IO (FileTransferMeta, [SndFileTransfer]) -getSndFileTransfer db User {userId} fileId = do - fileTransferMeta <- ExceptT $ getFileTransferMeta_ db userId fileId +getSndFileTransfer db user@User {userId} fileId = do + fileTransferMeta <- getFileTransferMeta db user fileId sndFileTransfers <- ExceptT $ getSndFileTransfers_ db userId fileId pure (fileTransferMeta, sndFileTransfers) getSndFileTransfers_ :: DB.Connection -> UserId -> Int64 -> IO (Either StoreError [SndFileTransfer]) getSndFileTransfers_ db userId fileId = - sndFileTransfers + mapM sndFileTransfer <$> 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, + SELECT s.file_status, f.file_name, f.file_size, f.chunk_size, f.file_path, s.file_inline, s.connection_id, c.agent_conn_id, cs.local_display_name, m.local_display_name FROM snd_files s JOIN files f USING (file_id) @@ -2507,29 +2589,27 @@ getSndFileTransfers_ db userId fileId = |] (userId, fileId) where - sndFileTransfers :: [(FileStatus, String, Integer, Integer, FilePath, Int64, AgentConnId, Maybe ContactName, Maybe ContactName)] -> Either StoreError [SndFileTransfer] - sndFileTransfers [] = Right [] - sndFileTransfers fts = mapM sndFileTransfer fts - sndFileTransfer (fileStatus, fileName, fileSize, chunkSize, filePath, connId, agentConnId, contactName_, memberName_) = + sndFileTransfer :: (FileStatus, String, Integer, Integer, FilePath, Maybe InlineFileMode, Int64, AgentConnId, Maybe ContactName, Maybe ContactName) -> Either StoreError SndFileTransfer + sndFileTransfer (fileStatus, fileName, fileSize, chunkSize, filePath, fileInline, connId, agentConnId, contactName_, memberName_) = case contactName_ <|> memberName_ of - Just recipientDisplayName -> Right SndFileTransfer {fileId, fileStatus, fileName, fileSize, chunkSize, filePath, recipientDisplayName, connId, agentConnId} + Just recipientDisplayName -> Right SndFileTransfer {fileId, fileStatus, fileName, fileSize, chunkSize, filePath, fileInline, recipientDisplayName, connId, agentConnId} Nothing -> Left $ SESndFileInvalid fileId -getFileTransferMeta_ :: DB.Connection -> UserId -> Int64 -> IO (Either StoreError FileTransferMeta) -getFileTransferMeta_ db userId fileId = - firstRow fileTransferMeta (SEFileNotFound fileId) $ +getFileTransferMeta :: DB.Connection -> User -> Int64 -> ExceptT StoreError IO FileTransferMeta +getFileTransferMeta db User {userId} fileId = + ExceptT . firstRow fileTransferMeta (SEFileNotFound fileId) $ DB.query db [sql| - SELECT f.file_name, f.file_size, f.chunk_size, f.file_path, f.cancelled + SELECT f.file_name, f.file_size, f.chunk_size, f.file_path, f.file_inline, f.cancelled FROM files f WHERE f.user_id = ? AND f.file_id = ? |] (userId, fileId) where - fileTransferMeta :: (String, Integer, Integer, FilePath, Maybe Bool) -> FileTransferMeta - fileTransferMeta (fileName, fileSize, chunkSize, filePath, cancelled_) = - FileTransferMeta {fileId, fileName, filePath, fileSize, chunkSize, cancelled = fromMaybe False cancelled_} + fileTransferMeta :: (String, Integer, Integer, FilePath, Maybe InlineFileMode, Maybe Bool) -> FileTransferMeta + fileTransferMeta (fileName, fileSize, chunkSize, filePath, fileInline, cancelled_) = + FileTransferMeta {fileId, fileName, fileSize, chunkSize, filePath, fileInline, cancelled = fromMaybe False cancelled_} getContactFileInfo :: DB.Connection -> User -> Contact -> IO [CIFileInfo] getContactFileInfo db User {userId} Contact {contactId} = @@ -2601,7 +2681,7 @@ updateGroupTs db User {userId} GroupInfo {groupId} updatedAt = "UPDATE groups SET updated_at = ? WHERE user_id = ? AND group_id = ?" (updatedAt, userId, groupId) -createNewSndMessage :: DB.Connection -> TVar ChaChaDRG -> ConnOrGroupId -> (SharedMsgId -> NewMessage) -> ExceptT StoreError IO SndMessage +createNewSndMessage :: MsgEncodingI e => DB.Connection -> TVar ChaChaDRG -> ConnOrGroupId -> (SharedMsgId -> NewMessage e) -> ExceptT StoreError IO SndMessage createNewSndMessage db gVar connOrGroupId mkMessage = createWithRandomId gVar $ \sharedMsgId -> do let NewMessage {chatMsgEvent, msgBody} = mkMessage $ SharedMsgId sharedMsgId @@ -2622,13 +2702,14 @@ createNewSndMessage db gVar connOrGroupId mkMessage = ConnectionId connId -> (Just connId, Nothing) GroupId groupId -> (Nothing, Just groupId) -createSndMsgDelivery :: DB.Connection -> SndMsgDelivery -> MessageId -> IO () +createSndMsgDelivery :: DB.Connection -> SndMsgDelivery -> MessageId -> IO Int64 createSndMsgDelivery db sndMsgDelivery messageId = do currentTs <- getCurrentTime msgDeliveryId <- createSndMsgDelivery_ db sndMsgDelivery messageId currentTs createMsgDeliveryEvent_ db msgDeliveryId MDSSndAgent currentTs + pure msgDeliveryId -createNewMessageAndRcvMsgDelivery :: DB.Connection -> ConnOrGroupId -> NewMessage -> Maybe SharedMsgId -> RcvMsgDelivery -> IO RcvMessage +createNewMessageAndRcvMsgDelivery :: forall e. MsgEncodingI e => DB.Connection -> ConnOrGroupId -> NewMessage e -> Maybe SharedMsgId -> RcvMsgDelivery -> IO RcvMessage createNewMessageAndRcvMsgDelivery db connOrGroupId NewMessage {chatMsgEvent, msgBody} sharedMsgId_ RcvMsgDelivery {connId, agentMsgId, agentMsgMeta, agentAckCmdId} = do currentTs <- getCurrentTime DB.execute @@ -2642,7 +2723,7 @@ createNewMessageAndRcvMsgDelivery db connOrGroupId NewMessage {chatMsgEvent, msg (msgId, connId, agentMsgId, msgMetaJson agentMsgMeta, agentAckCmdId, snd $ broker agentMsgMeta, currentTs, currentTs) msgDeliveryId <- insertedRowId db createMsgDeliveryEvent_ db msgDeliveryId MDSRcvAgent currentTs - pure RcvMessage {msgId, chatMsgEvent, sharedMsgId_, msgBody} + pure RcvMessage {msgId, chatMsgEvent = ACME (encoding @e) chatMsgEvent, sharedMsgId_, msgBody} where (connId_, groupId_) = case connOrGroupId of ConnectionId connId' -> (Just connId', Nothing) @@ -3382,14 +3463,14 @@ getGroupInfo db User {userId, userContactId} groupId = updateGroupProfile :: DB.Connection -> User -> GroupInfo -> GroupProfile -> ExceptT StoreError IO GroupInfo updateGroupProfile db User {userId} g@GroupInfo {groupId, localDisplayName, groupProfile = GroupProfile {displayName}} p'@GroupProfile {displayName = newName, fullName, image} | displayName == newName = liftIO $ do - currentTs <- getCurrentTime - updateGroupProfile_ currentTs $> (g :: GroupInfo) {groupProfile = p'} - | otherwise = - ExceptT . withLocalDisplayName db userId newName $ \ldn -> do currentTs <- getCurrentTime - updateGroupProfile_ currentTs - updateGroup_ ldn currentTs - pure . Right $ (g :: GroupInfo) {localDisplayName = ldn, groupProfile = p'} + updateGroupProfile_ currentTs $> (g :: GroupInfo) {groupProfile = p'} + | otherwise = + ExceptT . withLocalDisplayName db userId newName $ \ldn -> do + currentTs <- getCurrentTime + updateGroupProfile_ currentTs + updateGroup_ ldn currentTs + pure . Right $ (g :: GroupInfo) {localDisplayName = ldn, groupProfile = p'} where updateGroupProfile_ currentTs = DB.execute diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index 3b2351e2f4..e6a6f74e15 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -613,7 +613,8 @@ data SndFileTransfer = SndFileTransfer recipientDisplayName :: ContactName, connId :: Int64, agentConnId :: AgentConnId, - fileStatus :: FileStatus + fileStatus :: FileStatus, + fileInline :: Maybe InlineFileMode } deriving (Eq, Show, Generic) @@ -627,16 +628,48 @@ type FileTransferId = Int64 data FileInvitation = FileInvitation { fileName :: String, fileSize :: Integer, - fileConnReq :: Maybe ConnReqInvitation + fileConnReq :: Maybe ConnReqInvitation, + fileInline :: Maybe InlineFileMode } - deriving (Eq, Show, Generic, FromJSON) + deriving (Eq, Show, Generic) -instance ToJSON FileInvitation where toEncoding = J.genericToEncoding J.defaultOptions +instance ToJSON FileInvitation where + toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True} + toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True} + +instance FromJSON FileInvitation where + parseJSON = J.genericParseJSON J.defaultOptions {J.omitNothingFields = True} + +data InlineFileMode + = IFMOffer -- file will be sent inline once accepted + | IFMSent -- file is sent inline without acceptance + deriving (Eq, Show, Generic) + +instance TextEncoding InlineFileMode where + textEncode = \case + IFMOffer -> "offer" + IFMSent -> "sent" + textDecode = \case + "offer" -> Just IFMOffer + "sent" -> Just IFMSent + _ -> Nothing + +instance FromField InlineFileMode where fromField = fromTextField_ textDecode + +instance ToField InlineFileMode where toField = toField . textEncode + +instance FromJSON InlineFileMode where + parseJSON = J.withText "InlineFileMode" $ maybe (fail "bad InlineFileMode") pure . textDecode + +instance ToJSON InlineFileMode where + toJSON = J.String . textEncode + toEncoding = JE.text . textEncode data RcvFileTransfer = RcvFileTransfer { fileId :: FileTransferId, fileInvitation :: FileInvitation, fileStatus :: RcvFileStatus, + rcvFileInline :: Maybe InlineFileMode, senderDisplayName :: ContactName, chunkSize :: Integer, cancelled :: Bool, @@ -724,6 +757,7 @@ data FileTransferMeta = FileTransferMeta fileName :: String, filePath :: String, fileSize :: Integer, + fileInline :: Maybe InlineFileMode, chunkSize :: Integer, cancelled :: Bool } diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index bf1099f771..35657d5db2 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -241,7 +241,7 @@ showSMPServer = B.unpack . strEncode . host viewHostEvent :: AProtocolType -> TransportHost -> String viewHostEvent p h = map toUpper (B.unpack $ strEncode p) <> " host " <> B.unpack (strEncode h) -viewChatItem :: MsgDirectionI d => ChatInfo c -> ChatItem c d -> Bool -> [StyledString] +viewChatItem :: forall c d. MsgDirectionI d => ChatInfo c -> ChatItem c d -> Bool -> [StyledString] viewChatItem chat ChatItem {chatDir, meta, content, quotedItem, file} doShow = case chat of DirectChat c -> case chatDir of CIDirectSnd -> case content of @@ -714,9 +714,9 @@ viewContactUpdated | n == n' && fullName == fullName' = [] | n == n' = ["contact " <> ttyContact n <> fullNameUpdate] | otherwise = - [ "contact " <> ttyContact n <> " changed to " <> ttyFullName n' fullName', - "use " <> ttyToContact n' <> highlight' "" <> " to send messages" - ] + [ "contact " <> ttyContact n <> " changed to " <> ttyFullName n' fullName', + "use " <> ttyToContact n' <> highlight' "" <> " to send messages" + ] where fullNameUpdate = if T.null fullName' || fullName' == n' then " removed full name" else " updated full name: " <> plain fullName' @@ -748,9 +748,14 @@ viewSentBroadcast :: MsgContent -> Int -> ZonedTime -> [StyledString] viewSentBroadcast mc n ts = prependFirst (highlight' "/feed" <> " (" <> sShow n <> ") " <> ttyMsgTime ts <> " ") (ttyMsgContent mc) viewSentFileInvitation :: StyledString -> CIFile d -> CIMeta d -> [StyledString] -viewSentFileInvitation to CIFile {fileId, filePath} = case filePath of - Just fPath -> sentWithTime_ $ ttySentFile to fileId fPath +viewSentFileInvitation to CIFile {fileId, filePath, fileStatus} = case filePath of + Just fPath -> sentWithTime_ $ ttySentFile fPath _ -> const [] + where + ttySentFile fPath = ["/f " <> to <> ttyFilePath fPath] <> cancelSending + cancelSending = case fileStatus of + CIFSSndTransfer -> [] + _ -> ["use " <> highlight ("/fc " <> show fileId) <> " to cancel sending"] sentWithTime_ :: [StyledString] -> CIMeta d -> [StyledString] sentWithTime_ styledMsg CIMeta {localItemTs} = @@ -762,9 +767,6 @@ ttyMsgTime = styleTime . formatTime defaultTimeLocale "%H:%M" ttyMsgContent :: MsgContent -> [StyledString] ttyMsgContent = msgPlain . msgContentText -ttySentFile :: StyledString -> FileTransferId -> FilePath -> [StyledString] -ttySentFile to fId fPath = ["/f " <> to <> ttyFilePath fPath, "use " <> highlight ("/fc " <> show fId) <> " to cancel sending"] - prependFirst :: StyledString -> [StyledString] -> [StyledString] prependFirst s [] = [s] prependFirst s (s' : ss) = (s <> s') : ss @@ -793,21 +795,11 @@ viewReceivedFileInvitation :: StyledString -> CIFile d -> CIMeta d -> [StyledStr viewReceivedFileInvitation from file meta = receivedWithTime_ from [] meta (receivedFileInvitation_ file) receivedFileInvitation_ :: CIFile d -> [StyledString] -receivedFileInvitation_ CIFile {fileId, fileName, fileSize} = - [ "sends file " <> ttyFilePath fileName <> " (" <> humanReadableSize fileSize <> " / " <> sShow fileSize <> " bytes)", - -- below is printed for auto-accepted files as well; auto-accept is disabled in terminal though so in reality it never happens - "use " <> highlight ("/fr " <> show fileId <> " [/ | ]") <> " to receive it" - ] - --- TODO remove -viewReceivedFileInvitation' :: StyledString -> RcvFileTransfer -> CIMeta d -> [StyledString] -viewReceivedFileInvitation' from RcvFileTransfer {fileId, fileInvitation = FileInvitation {fileName, fileSize}} meta = receivedWithTime_ from [] meta (receivedFileInvitation_' fileId fileName fileSize) - -receivedFileInvitation_' :: Int64 -> String -> Integer -> [StyledString] -receivedFileInvitation_' fileId fileName fileSize = - [ "sends file " <> ttyFilePath fileName <> " (" <> humanReadableSize fileSize <> " / " <> sShow fileSize <> " bytes)", - "use " <> highlight ("/fr " <> show fileId <> " [/ | ]") <> " to receive it" - ] +receivedFileInvitation_ CIFile {fileId, fileName, fileSize, fileStatus} = + ["sends file " <> ttyFilePath fileName <> " (" <> humanReadableSize fileSize <> " / " <> sShow fileSize <> " bytes)"] + <> case fileStatus of + CIFSRcvAccepted -> [] + _ -> ["use " <> highlight ("/fr " <> show fileId <> " [/ | ]") <> " to receive it"] humanReadableSize :: Integer -> StyledString humanReadableSize size @@ -849,9 +841,8 @@ fileTransferStr fileId fileName = "file " <> sShow fileId <> " (" <> ttyFilePath viewFileTransferStatus :: (FileTransfer, [Integer]) -> [StyledString] viewFileTransferStatus (FTSnd FileTransferMeta {fileId, fileName, cancelled} [], _) = - [ "sending " <> fileTransferStr fileId fileName <> ": no file transfers" - <> if cancelled then ", file transfer cancelled" else "" - ] + ["sending " <> fileTransferStr fileId fileName <> ": no file transfers"] + <> ["file transfer cancelled" | cancelled] viewFileTransferStatus (FTSnd FileTransferMeta {cancelled} fts@(ft : _), chunksNum) = recipientStatuses <> ["file transfer cancelled" | cancelled] where @@ -978,7 +969,7 @@ viewChatError = \case CEGroupCantResendInvitation g c -> viewCannotResendInvitation g c CEGroupInternal s -> ["chat group bug: " <> plain s] CEFileNotFound f -> ["file not found: " <> plain f] - CEFileAlreadyReceiving f -> ["file is already accepted: " <> plain f] + CEFileAlreadyReceiving f -> ["file is already being received: " <> plain f] CEFileCancelled f -> ["file cancelled: " <> plain f] CEFileAlreadyExists f -> ["file already exists: " <> plain f] CEFileRead f e -> ["cannot read file " <> plain f, sShow e] diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs index 8ba92d0fb1..7dc479496e 100644 --- a/tests/ChatClient.hs +++ b/tests/ChatClient.hs @@ -13,6 +13,7 @@ import Control.Concurrent.Async import Control.Concurrent.STM import Control.Exception (bracket, bracket_) import Control.Monad.Except +import Data.Functor (($>)) import Data.List (dropWhileEnd, find) import Data.Maybe (fromJust, isNothing) import qualified Data.Text as T @@ -145,7 +146,11 @@ withNewTestChatOpts :: ChatOpts -> String -> Profile -> (TestCC -> IO a) -> IO a withNewTestChatOpts = withNewTestChatCfgOpts testCfg withNewTestChatCfgOpts :: ChatConfig -> ChatOpts -> String -> Profile -> (TestCC -> IO a) -> IO a -withNewTestChatCfgOpts cfg opts dbPrefix profile = bracket (createTestChat cfg opts dbPrefix profile) (\cc -> cc > stopTestChat cc) +withNewTestChatCfgOpts cfg opts dbPrefix profile runTest = + bracket + (createTestChat cfg opts dbPrefix profile) + stopTestChat + (\cc -> runTest cc >>= ((cc )) withTestChatV1 :: String -> (TestCC -> IO a) -> IO a withTestChatV1 = withTestChatCfg testCfgV1 diff --git a/tests/ChatTests.hs b/tests/ChatTests.hs index 71d1ede1ec..141641c7f5 100644 --- a/tests/ChatTests.hs +++ b/tests/ChatTests.hs @@ -1,7 +1,9 @@ {-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PostfixOperators #-} +{-# LANGUAGE ScopedTypeVariables #-} module ChatTests where @@ -9,15 +11,18 @@ import ChatClient import Control.Concurrent (threadDelay) import Control.Concurrent.Async (concurrently_) import Control.Concurrent.STM -import Control.Monad (forM_, when) +import Control.Monad (forM_, unless, when) import Data.Aeson (ToJSON) import qualified Data.Aeson as J import qualified Data.ByteString.Char8 as B import qualified Data.ByteString.Lazy.Char8 as LB import Data.Char (isDigit) +import Data.List (isPrefixOf) +import Data.Maybe (fromMaybe) +import Data.String import qualified Data.Text as T import Simplex.Chat.Call -import Simplex.Chat.Controller (ChatController (..)) +import Simplex.Chat.Controller (ChatConfig (..), ChatController (..), InlineFilesConfig (..), defaultInlineFilesConfig) import Simplex.Chat.Options (ChatOpts (..)) import Simplex.Chat.Types (ConnStatus (..), GroupMemberRole (..), ImageData (..), LocalProfile (..), Profile (..), User (..)) import Simplex.Messaging.Encoding.String @@ -66,15 +71,17 @@ chatTests = do it "update user profiles and notify contacts" testUpdateProfile it "update user profile with image" testUpdateProfileImage describe "sending and receiving files" $ do - it "send and receive file" testFileTransfer - it "send and receive a small file" testSmallFileTransfer - it "sender cancelled file transfer before transfer" testFileSndCancelBeforeTransfer + describe "send and receive file" $ fileTestMatrix2 runTestFileTransfer + it "send and receive file inline (without accepting)" testInlineFileTransfer + describe "send and receive a small file" $ fileTestMatrix2 runTestSmallFileTransfer + describe "sender cancelled file transfer before transfer" $ fileTestMatrix2 runTestFileSndCancelBeforeTransfer it "sender cancelled file transfer during transfer" testFileSndCancelDuringTransfer it "recipient cancelled file transfer" testFileRcvCancel - it "send and receive file to group" testGroupFileTransfer - it "sender cancelled group file transfer before transfer" testGroupFileSndCancelBeforeTransfer + describe "send and receive file to group" $ fileTestMatrix3 runTestGroupFileTransfer + it "send and receive file inline to group (without accepting)" testInlineGroupFileTransfer + describe "sender cancelled group file transfer before transfer" $ fileTestMatrix3 runTestGroupFileSndCancelBeforeTransfer describe "messages with files" $ do - it "send and receive message with file" testMessageWithFile + describe "send and receive message with file" $ fileTestMatrix2 runTestMessageWithFile it "send and receive image" testSendImage it "files folder: send and receive image" testFilesFoldersSendImage it "files folder: sender deleted file during transfer" testFilesFoldersImageSndDelete @@ -133,40 +140,56 @@ versionTestMatrix2 :: (TestCC -> TestCC -> IO ()) -> Spec versionTestMatrix2 runTest = do it "v2" $ testChat2 aliceProfile bobProfile runTest it "v1" $ testChatCfg2 testCfgV1 aliceProfile bobProfile runTest - it "v1 to v2" . withTmpFiles $ - withNewTestChat "alice" aliceProfile $ \alice -> - withNewTestChatV1 "bob" bobProfile $ \bob -> - runTest alice bob - it "v2 to v1" . withTmpFiles $ - withNewTestChatV1 "alice" aliceProfile $ \alice -> - withNewTestChat "bob" bobProfile $ \bob -> - runTest alice bob + it "v1 to v2" $ runTestCfg2 testCfg testCfgV1 runTest + it "v2 to v1" $ runTestCfg2 testCfgV1 testCfg runTest versionTestMatrix3 :: (TestCC -> TestCC -> TestCC -> IO ()) -> Spec versionTestMatrix3 runTest = do it "v2" $ testChat3 aliceProfile bobProfile cathProfile runTest -- it "v1" $ testChatCfg3 testCfgV1 aliceProfile bobProfile cathProfile runTest --- it "v1 to v2" . withTmpFiles $ --- withNewTestChat "alice" aliceProfile $ \alice -> --- withNewTestChatV1 "bob" bobProfile $ \bob -> --- withNewTestChatV1 "cath" cathProfile $ \cath -> --- runTest alice bob cath --- it "v2+v1 to v2" . withTmpFiles $ --- withNewTestChat "alice" aliceProfile $ \alice -> --- withNewTestChat "bob" bobProfile $ \bob -> --- withNewTestChatV1 "cath" cathProfile $ \cath -> --- runTest alice bob cath --- it "v2 to v1" . withTmpFiles $ --- withNewTestChatV1 "alice" aliceProfile $ \alice -> --- withNewTestChat "bob" bobProfile $ \bob -> --- withNewTestChat "cath" cathProfile $ \cath -> --- runTest alice bob cath --- it "v2+v1 to v1" . withTmpFiles $ --- withNewTestChatV1 "alice" aliceProfile $ \alice -> --- withNewTestChat "bob" bobProfile $ \bob -> --- withNewTestChatV1 "cath" cathProfile $ \cath -> --- runTest alice bob cath +-- it "v1 to v2" $ runTestCfg3 testCfg testCfgV1 testCfgV1 runTest +-- it "v2+v1 to v2" $ runTestCfg3 testCfg testCfg testCfgV1 runTest +-- it "v2 to v1" $ runTestCfg3 testCfgV1 testCfg testCfg runTest +-- it "v2+v1 to v1" $ runTestCfg3 testCfgV1 testCfg testCfgV1 runTest + +inlineCfg :: Integer -> ChatConfig +inlineCfg n = testCfg {inlineFiles = defaultInlineFilesConfig {offerChunks = n, receiveChunks = n}} + +fileTestMatrix2 :: (TestCC -> TestCC -> IO ()) -> Spec +fileTestMatrix2 runTest = do + it "via connection" $ runTestCfg2 viaConn viaConn runTest + it "inline (accepting)" $ runTestCfg2 inline inline runTest + it "via connection (inline offered)" $ runTestCfg2 inline viaConn runTest + it "via connection (inline supported)" $ runTestCfg2 viaConn inline runTest + where + inline = inlineCfg 100 + viaConn = inlineCfg 0 + +fileTestMatrix3 :: (TestCC -> TestCC -> TestCC -> IO ()) -> Spec +fileTestMatrix3 runTest = do + it "via connection" $ runTestCfg3 viaConn viaConn viaConn runTest + it "inline" $ runTestCfg3 inline inline inline runTest + it "via connection (inline offered)" $ runTestCfg3 inline viaConn viaConn runTest + it "via connection (inline supported)" $ runTestCfg3 viaConn inline inline runTest + where + inline = inlineCfg 100 + viaConn = inlineCfg 0 + +runTestCfg2 :: ChatConfig -> ChatConfig -> (TestCC -> TestCC -> IO ()) -> IO () +runTestCfg2 aliceCfg bobCfg runTest = + withTmpFiles $ + withNewTestChatCfg aliceCfg "alice" aliceProfile $ \alice -> + withNewTestChatCfg bobCfg "bob" bobProfile $ \bob -> + runTest alice bob + +runTestCfg3 :: ChatConfig -> ChatConfig -> ChatConfig -> (TestCC -> TestCC -> TestCC -> IO ()) -> IO () +runTestCfg3 aliceCfg bobCfg cathCfg runTest = + withTmpFiles $ + withNewTestChatCfg aliceCfg "alice" aliceProfile $ \alice -> + withNewTestChatCfg bobCfg "bob" bobProfile $ \bob -> + withNewTestChatCfg cathCfg "cath" cathProfile $ \cath -> + runTest alice bob cath testAddContact :: Spec testAddContact = versionTestMatrix2 runTestAddContact @@ -1351,68 +1374,88 @@ testUpdateProfileImage = bob <## "use @alice2 to send messages" (bob do - connectUsers alice bob - startFileTransfer alice bob - concurrentlyN_ - [ do - bob #> "@alice receiving here..." - bob <## "completed receiving file 1 (test.jpg) from alice", - do - alice <# "bob> receiving here..." - alice <## "completed sending file 1 (test.jpg) to bob" - ] - src <- B.readFile "./tests/fixtures/test.jpg" - dest <- B.readFile "./tests/tmp/test.jpg" - dest `shouldBe` src +runTestFileTransfer :: TestCC -> TestCC -> IO () +runTestFileTransfer alice bob = do + connectUsers alice bob + startFileTransfer' alice bob "test.pdf" "266.0 KiB / 272376 bytes" + concurrentlyN_ + [ do + bob #> "@alice receiving here..." + bob <## "completed receiving file 1 (test.pdf) from alice", + alice + <### [ WithTime "bob> receiving here...", + "completed sending file 1 (test.pdf) to bob" + ] + ] + src <- B.readFile "./tests/fixtures/test.pdf" + dest <- B.readFile "./tests/tmp/test.pdf" + dest `shouldBe` src -testSmallFileTransfer :: IO () -testSmallFileTransfer = - testChat2 aliceProfile bobProfile $ - \alice bob -> do - connectUsers alice bob - alice #> "/f @bob ./tests/fixtures/test.txt" - alice <## "use /fc 1 to cancel sending" - bob <# "alice> sends file test.txt (11 bytes / 11 bytes)" - bob <## "use /fr 1 [/ | ] to receive it" - bob ##> "/fr 1 ./tests/tmp" - bob <## "saving file 1 from alice to ./tests/tmp/test.txt" - concurrentlyN_ - [ do - bob <## "started receiving file 1 (test.txt) from alice" - bob <## "completed receiving file 1 (test.txt) from alice", - do - alice <## "started sending file 1 (test.txt) to bob" - alice <## "completed sending file 1 (test.txt) to bob" - ] - src <- B.readFile "./tests/fixtures/test.txt" - dest <- B.readFile "./tests/tmp/test.txt" - dest `shouldBe` src +testInlineFileTransfer :: IO () +testInlineFileTransfer = + testChatCfg2 cfg aliceProfile bobProfile $ \alice bob -> do + connectUsers alice bob + bob ##> "/_files_folder ./tests/tmp/" + bob <## "ok" + alice #> "/f @bob ./tests/fixtures/test.jpg" + -- below is not shown in "sent" mode + -- alice <## "use /fc 1 to cancel sending" + bob <# "alice> sends file test.jpg (136.5 KiB / 139737 bytes)" + -- below is not shown in "sent" mode + -- bob <## "use /fr 1 [/ | ] to receive it" + bob <## "started receiving file 1 (test.jpg) from alice" + concurrently_ + (alice <## "completed sending file 1 (test.jpg) to bob") + (bob <## "completed receiving file 1 (test.jpg) from alice") + src <- B.readFile "./tests/fixtures/test.jpg" + dest <- B.readFile "./tests/tmp/test.jpg" + dest `shouldBe` src + where + cfg = testCfg {inlineFiles = defaultInlineFilesConfig {offerChunks = 100, sendChunks = 100, receiveChunks = 100}} -testFileSndCancelBeforeTransfer :: IO () -testFileSndCancelBeforeTransfer = - testChat2 aliceProfile bobProfile $ - \alice bob -> do - connectUsers alice bob - alice #> "/f @bob ./tests/fixtures/test.txt" - alice <## "use /fc 1 to cancel sending" - bob <# "alice> sends file test.txt (11 bytes / 11 bytes)" - bob <## "use /fr 1 [/ | ] to receive it" - alice ##> "/fc 1" - concurrentlyN_ - [ alice <## "cancelled sending file 1 (test.txt) to bob", - bob <## "alice cancelled sending file 1 (test.txt)" - ] - alice ##> "/fs 1" - alice <## "sending file 1 (test.txt) cancelled: bob" - alice <## "file transfer cancelled" - bob ##> "/fs 1" - bob <## "receiving file 1 (test.txt) cancelled" - bob ##> "/fr 1 ./tests/tmp" - bob <## "file cancelled: test.txt" +runTestSmallFileTransfer :: TestCC -> TestCC -> IO () +runTestSmallFileTransfer alice bob = do + connectUsers alice bob + alice #> "/f @bob ./tests/fixtures/test.txt" + alice <## "use /fc 1 to cancel sending" + bob <# "alice> sends file test.txt (11 bytes / 11 bytes)" + bob <## "use /fr 1 [/ | ] to receive it" + bob ##> "/fr 1 ./tests/tmp" + bob <## "saving file 1 from alice to ./tests/tmp/test.txt" + concurrentlyN_ + [ do + bob <## "started receiving file 1 (test.txt) from alice" + bob <## "completed receiving file 1 (test.txt) from alice", + do + alice <## "started sending file 1 (test.txt) to bob" + alice <## "completed sending file 1 (test.txt) to bob" + ] + src <- B.readFile "./tests/fixtures/test.txt" + dest <- B.readFile "./tests/tmp/test.txt" + dest `shouldBe` src + +runTestFileSndCancelBeforeTransfer :: TestCC -> TestCC -> IO () +runTestFileSndCancelBeforeTransfer alice bob = do + connectUsers alice bob + alice #> "/f @bob ./tests/fixtures/test.txt" + alice <## "use /fc 1 to cancel sending" + bob <# "alice> sends file test.txt (11 bytes / 11 bytes)" + bob <## "use /fr 1 [/ | ] to receive it" + alice ##> "/fc 1" + concurrentlyN_ + [ alice <##. "cancelled sending file 1 (test.txt)", + bob <## "alice cancelled sending file 1 (test.txt)" + ] + alice ##> "/fs 1" + alice + <##.. [ "sending file 1 (test.txt): no file transfers", + "sending file 1 (test.txt) cancelled: bob" + ] + alice <## "file transfer cancelled" + bob ##> "/fs 1" + bob <## "receiving file 1 (test.txt) cancelled" + bob ##> "/fr 1 ./tests/tmp" + bob <## "file cancelled: test.txt" testFileSndCancelDuringTransfer :: IO () testFileSndCancelDuringTransfer = @@ -1456,101 +1499,138 @@ testFileRcvCancel = ] checkPartialTransfer "test.jpg" -testGroupFileTransfer :: IO () -testGroupFileTransfer = - testChat3 aliceProfile bobProfile cathProfile $ +runTestGroupFileTransfer :: TestCC -> TestCC -> TestCC -> IO () +runTestGroupFileTransfer alice bob cath = do + createGroup3 "team" alice bob cath + alice #> "/f #team ./tests/fixtures/test.jpg" + alice <## "use /fc 1 to cancel sending" + concurrentlyN_ + [ do + bob <# "#team alice> sends file test.jpg (136.5 KiB / 139737 bytes)" + bob <## "use /fr 1 [/ | ] to receive it", + do + cath <# "#team alice> sends file test.jpg (136.5 KiB / 139737 bytes)" + cath <## "use /fr 1 [/ | ] to receive it" + ] + alice ##> "/fs 1" + getTermLine alice >>= (`shouldStartWith` "sending file 1 (test.jpg): no file transfers") + bob ##> "/fr 1 ./tests/tmp/" + bob <## "saving file 1 from alice to ./tests/tmp/test.jpg" + concurrentlyN_ + [ do + alice <## "started sending file 1 (test.jpg) to bob" + alice <## "completed sending file 1 (test.jpg) to bob" + alice ##> "/fs 1" + alice <## "sending file 1 (test.jpg) complete: bob", + do + bob <## "started receiving file 1 (test.jpg) from alice" + bob <## "completed receiving file 1 (test.jpg) from alice" + ] + cath ##> "/fr 1 ./tests/tmp/" + cath <## "saving file 1 from alice to ./tests/tmp/test_1.jpg" + concurrentlyN_ + [ do + alice <## "started sending file 1 (test.jpg) to cath" + alice <## "completed sending file 1 (test.jpg) to cath" + alice ##> "/fs 1" + getTermLine alice >>= (`shouldStartWith` "sending file 1 (test.jpg) complete"), + do + cath <## "started receiving file 1 (test.jpg) from alice" + cath <## "completed receiving file 1 (test.jpg) from alice" + ] + src <- B.readFile "./tests/fixtures/test.jpg" + dest1 <- B.readFile "./tests/tmp/test.jpg" + dest2 <- B.readFile "./tests/tmp/test_1.jpg" + dest1 `shouldBe` src + dest2 `shouldBe` src + +testInlineGroupFileTransfer :: IO () +testInlineGroupFileTransfer = + testChatCfg3 cfg aliceProfile bobProfile cathProfile $ \alice bob cath -> do createGroup3 "team" alice bob cath + bob ##> "/_files_folder ./tests/tmp/bob/" + bob <## "ok" + cath ##> "/_files_folder ./tests/tmp/cath/" + cath <## "ok" alice #> "/f #team ./tests/fixtures/test.jpg" - alice <## "use /fc 1 to cancel sending" + -- below is not shown in "sent" mode + -- alice <## "use /fc 1 to cancel sending" concurrentlyN_ [ do + alice + <### [ "completed sending file 1 (test.jpg) to bob", + "completed sending file 1 (test.jpg) to cath" + ] + alice ##> "/fs 1" + alice <##. "sending file 1 (test.jpg) complete", + do bob <# "#team alice> sends file test.jpg (136.5 KiB / 139737 bytes)" - bob <## "use /fr 1 [/ | ] to receive it", + bob <## "started receiving file 1 (test.jpg) from alice" + bob <## "completed receiving file 1 (test.jpg) from alice", do cath <# "#team alice> sends file test.jpg (136.5 KiB / 139737 bytes)" - cath <## "use /fr 1 [/ | ] to receive it" - ] - alice ##> "/fs 1" - getTermLine alice >>= (`shouldStartWith` "sending file 1 (test.jpg): no file transfers") - bob ##> "/fr 1 ./tests/tmp/" - bob <## "saving file 1 from alice to ./tests/tmp/test.jpg" - concurrentlyN_ - [ do - alice <## "started sending file 1 (test.jpg) to bob" - alice <## "completed sending file 1 (test.jpg) to bob" - alice ##> "/fs 1" - alice <## "sending file 1 (test.jpg) complete: bob", - do - bob <## "started receiving file 1 (test.jpg) from alice" - bob <## "completed receiving file 1 (test.jpg) from alice" - ] - cath ##> "/fr 1 ./tests/tmp/" - cath <## "saving file 1 from alice to ./tests/tmp/test_1.jpg" - concurrentlyN_ - [ do - alice <## "started sending file 1 (test.jpg) to cath" - alice <## "completed sending file 1 (test.jpg) to cath" - alice ##> "/fs 1" - getTermLine alice >>= (`shouldStartWith` "sending file 1 (test.jpg) complete"), - do cath <## "started receiving file 1 (test.jpg) from alice" cath <## "completed receiving file 1 (test.jpg) from alice" ] - -testGroupFileSndCancelBeforeTransfer :: IO () -testGroupFileSndCancelBeforeTransfer = - testChat3 aliceProfile bobProfile cathProfile $ - \alice bob cath -> do - createGroup3 "team" alice bob cath - alice #> "/f #team ./tests/fixtures/test.txt" - alice <## "use /fc 1 to cancel sending" - concurrentlyN_ - [ do - bob <# "#team alice> sends file test.txt (11 bytes / 11 bytes)" - bob <## "use /fr 1 [/ | ] to receive it", - do - cath <# "#team alice> sends file test.txt (11 bytes / 11 bytes)" - cath <## "use /fr 1 [/ | ] to receive it" - ] - alice ##> "/fc 1" - concurrentlyN_ - [ alice <## "cancelled sending file 1 (test.txt)", - bob <## "alice cancelled sending file 1 (test.txt)", - cath <## "alice cancelled sending file 1 (test.txt)" - ] - alice ##> "/fs 1" - alice <## "sending file 1 (test.txt): no file transfers, file transfer cancelled" - bob ##> "/fs 1" - bob <## "receiving file 1 (test.txt) cancelled" - bob ##> "/fr 1 ./tests/tmp" - bob <## "file cancelled: test.txt" - -testMessageWithFile :: IO () -testMessageWithFile = - testChat2 aliceProfile bobProfile $ - \alice bob -> do - connectUsers alice bob - alice ##> "/_send @2 json {\"filePath\": \"./tests/fixtures/test.jpg\", \"msgContent\": {\"type\": \"text\", \"text\": \"hi, sending a file\"}}" - alice <# "@bob hi, sending a file" - alice <# "/f @bob ./tests/fixtures/test.jpg" - alice <## "use /fc 1 to cancel sending" - bob <# "alice> hi, sending a file" - bob <# "alice> sends file test.jpg (136.5 KiB / 139737 bytes)" - bob <## "use /fr 1 [/ | ] to receive it" - bob ##> "/fr 1 ./tests/tmp" - bob <## "saving file 1 from alice to ./tests/tmp/test.jpg" - concurrently_ - (bob <## "started receiving file 1 (test.jpg) from alice") - (alice <## "started sending file 1 (test.jpg) to bob") - concurrently_ - (bob <## "completed receiving file 1 (test.jpg) from alice") - (alice <## "completed sending file 1 (test.jpg) to bob") src <- B.readFile "./tests/fixtures/test.jpg" - dest <- B.readFile "./tests/tmp/test.jpg" - dest `shouldBe` src - alice #$> ("/_get chat @2 count=100", chatF, [((1, "hi, sending a file"), Just "./tests/fixtures/test.jpg")]) - bob #$> ("/_get chat @2 count=100", chatF, [((0, "hi, sending a file"), Just "./tests/tmp/test.jpg")]) + dest1 <- B.readFile "./tests/tmp/bob/test.jpg" + dest2 <- B.readFile "./tests/tmp/cath/test.jpg" + dest1 `shouldBe` src + dest2 `shouldBe` src + where + cfg = testCfg {inlineFiles = defaultInlineFilesConfig {offerChunks = 100, sendChunks = 100, totalSendChunks = 100, receiveChunks = 100}} + +runTestGroupFileSndCancelBeforeTransfer :: TestCC -> TestCC -> TestCC -> IO () +runTestGroupFileSndCancelBeforeTransfer alice bob cath = do + createGroup3 "team" alice bob cath + alice #> "/f #team ./tests/fixtures/test.txt" + alice <## "use /fc 1 to cancel sending" + concurrentlyN_ + [ do + bob <# "#team alice> sends file test.txt (11 bytes / 11 bytes)" + bob <## "use /fr 1 [/ | ] to receive it", + do + cath <# "#team alice> sends file test.txt (11 bytes / 11 bytes)" + cath <## "use /fr 1 [/ | ] to receive it" + ] + alice ##> "/fc 1" + concurrentlyN_ + [ alice <## "cancelled sending file 1 (test.txt)", + bob <## "alice cancelled sending file 1 (test.txt)", + cath <## "alice cancelled sending file 1 (test.txt)" + ] + alice ##> "/fs 1" + alice <## "sending file 1 (test.txt): no file transfers" + alice <## "file transfer cancelled" + bob ##> "/fs 1" + bob <## "receiving file 1 (test.txt) cancelled" + bob ##> "/fr 1 ./tests/tmp" + bob <## "file cancelled: test.txt" + +runTestMessageWithFile :: TestCC -> TestCC -> IO () +runTestMessageWithFile alice bob = do + connectUsers alice bob + alice ##> "/_send @2 json {\"filePath\": \"./tests/fixtures/test.jpg\", \"msgContent\": {\"type\": \"text\", \"text\": \"hi, sending a file\"}}" + alice <# "@bob hi, sending a file" + alice <# "/f @bob ./tests/fixtures/test.jpg" + alice <## "use /fc 1 to cancel sending" + bob <# "alice> hi, sending a file" + bob <# "alice> sends file test.jpg (136.5 KiB / 139737 bytes)" + bob <## "use /fr 1 [/ | ] to receive it" + bob ##> "/fr 1 ./tests/tmp" + bob <## "saving file 1 from alice to ./tests/tmp/test.jpg" + concurrently_ + (bob <## "started receiving file 1 (test.jpg) from alice") + (alice <## "started sending file 1 (test.jpg) to bob") + concurrently_ + (bob <## "completed receiving file 1 (test.jpg) from alice") + (alice <## "completed sending file 1 (test.jpg) to bob") + src <- B.readFile "./tests/fixtures/test.jpg" + dest <- B.readFile "./tests/tmp/test.jpg" + dest `shouldBe` src + alice #$> ("/_get chat @2 count=100", chatF, [((1, "hi, sending a file"), Just "./tests/fixtures/test.jpg")]) + bob #$> ("/_get chat @2 count=100", chatF, [((0, "hi, sending a file"), Just "./tests/tmp/test.jpg")]) testSendImage :: IO () testSendImage = @@ -2278,7 +2358,7 @@ testJoinGroupIncognito = testChat4 aliceProfile bobProfile cathProfile danProfil do dan <## "#secret_club: you joined the group" dan - <### [ "#secret_club: member " <> cathIncognito <> " is connected", + <### [ ConsoleString $ "#secret_club: member " <> cathIncognito <> " is connected", "#secret_club: member bob_1 (Bob) is connected", "contact bob_1 is merged into bob", "use @bob to send messages" @@ -2338,28 +2418,28 @@ testJoinGroupIncognito = testChat4 aliceProfile bobProfile cathProfile danProfil alice <### [ "alice (Alice): owner, you, created group", "bob (Bob): admin, invited, connected", - cathIncognito <> ": admin, invited, connected", + ConsoleString $ cathIncognito <> ": admin, invited, connected", "dan (Daniel): admin, invited, connected" ] bob ##> "/ms secret_club" bob <### [ "alice (Alice): owner, host, connected", "bob (Bob): admin, you, connected", - cathIncognito <> ": admin, connected", + ConsoleString $ cathIncognito <> ": admin, connected", "dan (Daniel): admin, connected" ] cath ##> "/ms secret_club" cath <### [ "alice (Alice): owner, host, connected", "bob_1 (Bob): admin, connected", - "i " <> cathIncognito <> ": admin, you, connected", + ConsoleString $ "i " <> cathIncognito <> ": admin, you, connected", "dan_1 (Daniel): admin, connected" ] dan ##> "/ms secret_club" dan <### [ "alice (Alice): owner, host, connected", "bob (Bob): admin, connected", - cathIncognito <> ": admin, connected", + ConsoleString $ cathIncognito <> ": admin, connected", "dan (Daniel): admin, you, connected" ] -- remove member @@ -3456,18 +3536,44 @@ cc <## line = do when (l /= line) $ print ("expected: " <> line, ", got: " <> l) l `shouldBe` line -getInAnyOrder :: (String -> String) -> TestCC -> [String] -> Expectation +(<##.) :: TestCC -> String -> Expectation +cc <##. line = do + l <- getTermLine cc + let prefix = line `isPrefixOf` l + unless prefix $ print ("expected to start from: " <> line, ", got: " <> l) + prefix `shouldBe` True + +(<##..) :: TestCC -> [String] -> Expectation +cc <##.. ls = do + l <- getTermLine cc + let prefix = any (`isPrefixOf` l) ls + unless prefix $ print ("expected to start from one of: " <> show ls, ", got: " <> l) + prefix `shouldBe` True + +data ConsoleResponse = ConsoleString String | WithTime String + deriving (Show) + +instance IsString ConsoleResponse where fromString = ConsoleString + +-- this assumes that the string can only match one option +getInAnyOrder :: (String -> String) -> TestCC -> [ConsoleResponse] -> Expectation getInAnyOrder _ _ [] = pure () getInAnyOrder f cc ls = do line <- f <$> getTermLine cc - if line `elem` ls - then getInAnyOrder f cc $ filter (/= line) ls + let rest = filter (not . expected line) ls + if length rest < length ls + then getInAnyOrder f cc rest else error $ "unexpected output: " <> line + where + expected :: String -> ConsoleResponse -> Bool + expected l = \case + ConsoleString s -> l == s + WithTime s -> dropTime_ l == Just s -(<###) :: TestCC -> [String] -> Expectation +(<###) :: TestCC -> [ConsoleResponse] -> Expectation (<###) = getInAnyOrder id -(<##?) :: TestCC -> [String] -> Expectation +(<##?) :: TestCC -> [ConsoleResponse] -> Expectation (<##?) = getInAnyOrder dropTime (<#) :: TestCC -> String -> Expectation @@ -3489,13 +3595,16 @@ cc1 <#? cc2 = do cc1 <## ("to reject: /rc " <> name <> " (the sender will NOT be notified)") dropTime :: String -> String -dropTime msg = case splitAt 6 msg of - ([m, m', ':', s, s', ' '], text) -> - if all isDigit [m, m', s, s'] then text else err - _ -> err +dropTime msg = fromMaybe err $ dropTime_ msg where err = error $ "invalid time: " <> msg +dropTime_ :: String -> Maybe String +dropTime_ msg = case splitAt 6 msg of + ([m, m', ':', s, s', ' '], text) -> + if all isDigit [m, m', s, s'] then Just text else Nothing + _ -> Nothing + getInvitation :: TestCC -> IO String getInvitation cc = do cc <## "pass this invitation link to your contact (via another channel):" diff --git a/tests/ProtocolTests.hs b/tests/ProtocolTests.hs index 37afd35f73..82373c2ca0 100644 --- a/tests/ProtocolTests.hs +++ b/tests/ProtocolTests.hs @@ -52,28 +52,28 @@ testE2ERatchetParams = E2ERatchetParamsUri e2eEncryptVRange testDhPubKey testDhP testConnReq :: ConnectionRequestUri 'CMInvitation testConnReq = CRInvitationUri connReqData testE2ERatchetParams -(==##) :: ByteString -> ChatMessage -> Expectation +(==##) :: MsgEncodingI e => ByteString -> ChatMessage e -> Expectation s ==## msg = do strDecode s `shouldBe` Right msg parseAll strP s `shouldBe` Right msg -(##==) :: ByteString -> ChatMessage -> Expectation +(##==) :: MsgEncodingI e => ByteString -> ChatMessage e -> Expectation s ##== msg = J.eitherDecodeStrict' (strEncode msg) `shouldBe` (J.eitherDecodeStrict' s :: Either String J.Value) -(##==##) :: ByteString -> ChatMessage -> Expectation +(##==##) :: MsgEncodingI e => ByteString -> ChatMessage e -> Expectation s ##==## msg = do s ##== msg s ==## msg -(==#) :: ByteString -> ChatMsgEvent -> Expectation +(==#) :: MsgEncodingI e => ByteString -> ChatMsgEvent e -> Expectation s ==# msg = s ==## ChatMessage Nothing msg -(#==) :: ByteString -> ChatMsgEvent -> Expectation +(#==) :: MsgEncodingI e => ByteString -> ChatMsgEvent e -> Expectation s #== msg = s ##== ChatMessage Nothing msg -(#==#) :: ByteString -> ChatMsgEvent -> Expectation +(#==#) :: MsgEncodingI e => ByteString -> ChatMsgEvent e -> Expectation s #==# msg = do s #== msg s ==# msg @@ -122,10 +122,10 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do ##==## ChatMessage (Just $ SharedMsgId "\1\2\3\4") (XMsgNew $ MCForward (ExtMsgContent (MCText "hello") Nothing)) it "x.msg.new simple text with file" $ "{\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"file\":{\"fileSize\":12345,\"fileName\":\"photo.jpg\"}}}" - #==# XMsgNew (MCSimple (ExtMsgContent (MCText "hello") (Just FileInvitation {fileName = "photo.jpg", fileSize = 12345, fileConnReq = Nothing}))) + #==# XMsgNew (MCSimple (ExtMsgContent (MCText "hello") (Just FileInvitation {fileName = "photo.jpg", fileSize = 12345, fileConnReq = Nothing, fileInline = Nothing}))) it "x.msg.new simple file with file" $ "{\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"\",\"type\":\"file\"},\"file\":{\"fileSize\":12345,\"fileName\":\"file.txt\"}}}" - #==# XMsgNew (MCSimple (ExtMsgContent (MCFile "") (Just FileInvitation {fileName = "file.txt", fileSize = 12345, fileConnReq = Nothing}))) + #==# XMsgNew (MCSimple (ExtMsgContent (MCFile "") (Just FileInvitation {fileName = "file.txt", fileSize = 12345, fileConnReq = Nothing, fileInline = Nothing}))) it "x.msg.new quote with file" $ "{\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello to you too\",\"type\":\"text\"},\"quote\":{\"content\":{\"text\":\"hello there!\",\"type\":\"text\"},\"msgRef\":{\"msgId\":\"BQYHCA==\",\"sent\":true,\"sentAt\":\"1970-01-01T00:00:01.000000001Z\"}},\"file\":{\"fileSize\":12345,\"fileName\":\"photo.jpg\"}}}" ##==## ChatMessage @@ -138,13 +138,13 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do ) ( ExtMsgContent (MCText "hello to you too") - (Just FileInvitation {fileName = "photo.jpg", fileSize = 12345, fileConnReq = Nothing}) + (Just FileInvitation {fileName = "photo.jpg", fileSize = 12345, fileConnReq = Nothing, fileInline = Nothing}) ) ) ) it "x.msg.new forward with file" $ "{\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"forward\":true,\"file\":{\"fileSize\":12345,\"fileName\":\"photo.jpg\"}}}" - ##==## ChatMessage (Just $ SharedMsgId "\1\2\3\4") (XMsgNew $ MCForward (ExtMsgContent (MCText "hello") (Just FileInvitation {fileName = "photo.jpg", fileSize = 12345, fileConnReq = Nothing}))) + ##==## ChatMessage (Just $ SharedMsgId "\1\2\3\4") (XMsgNew $ MCForward (ExtMsgContent (MCText "hello") (Just FileInvitation {fileName = "photo.jpg", fileSize = 12345, fileConnReq = Nothing, fileInline = Nothing}))) it "x.msg.update" $ "{\"event\":\"x.msg.update\",\"params\":{\"msgId\":\"AQIDBA==\", \"content\":{\"text\":\"hello\",\"type\":\"text\"}}}" #==# XMsgUpdate (SharedMsgId "\1\2\3\4") (MCText "hello") @@ -156,16 +156,19 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do #==# XMsgDeleted it "x.file" $ "{\"event\":\"x.file\",\"params\":{\"file\":{\"fileConnReq\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"fileSize\":12345,\"fileName\":\"photo.jpg\"}}}" - #==# XFile FileInvitation {fileName = "photo.jpg", fileSize = 12345, fileConnReq = Just testConnReq} + #==# XFile FileInvitation {fileName = "photo.jpg", fileSize = 12345, fileConnReq = Just testConnReq, fileInline = Nothing} it "x.file without file invitation" $ "{\"event\":\"x.file\",\"params\":{\"file\":{\"fileSize\":12345,\"fileName\":\"photo.jpg\"}}}" - #==# XFile FileInvitation {fileName = "photo.jpg", fileSize = 12345, fileConnReq = Nothing} + #==# XFile FileInvitation {fileName = "photo.jpg", fileSize = 12345, fileConnReq = Nothing, fileInline = Nothing} it "x.file.acpt" $ "{\"event\":\"x.file.acpt\",\"params\":{\"fileName\":\"photo.jpg\"}}" #==# XFileAcpt "photo.jpg" it "x.file.acpt.inv" $ "{\"event\":\"x.file.acpt.inv\",\"params\":{\"msgId\":\"AQIDBA==\",\"fileName\":\"photo.jpg\",\"fileConnReq\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}" - #==# XFileAcptInv (SharedMsgId "\1\2\3\4") testConnReq "photo.jpg" + #==# XFileAcptInv (SharedMsgId "\1\2\3\4") (Just testConnReq) "photo.jpg" + it "x.file.acpt.inv" $ + "{\"event\":\"x.file.acpt.inv\",\"params\":{\"msgId\":\"AQIDBA==\",\"fileName\":\"photo.jpg\"}}" + #==# XFileAcptInv (SharedMsgId "\1\2\3\4") Nothing "photo.jpg" it "x.file.cancel" $ "{\"event\":\"x.file.cancel\",\"params\":{\"msgId\":\"AQIDBA==\"}}" #==# XFileCancel (SharedMsgId "\1\2\3\4") diff --git a/tests/fixtures/test.pdf b/tests/fixtures/test.pdf new file mode 100644 index 0000000000000000000000000000000000000000..1ee281367223b3891da88658829d8a8e15259615 GIT binary patch literal 272376 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

$WoHk#k>LgzfRF#1^e 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ÎG19Dx&+!U2Jg-p$-uWG0Nil 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

c1 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 literal 0 HcmV?d00001