core: local encryption for auto-received inline files (e.g. small voice messages) (#3224)
* core: local encryption for auto-received inline files * update view, test
This commit is contained in:
parent
a35dc263b7
commit
c2a320640b
@ -257,6 +257,12 @@ func setXFTPConfig(_ cfg: XFTPFileConfig?) throws {
|
|||||||
throw r
|
throw r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func apiSetEncryptLocalFiles(_ enable: Boolean) throws {
|
||||||
|
let r = chatSendCmdSync(.apiSetEncryptLocalFiles(enable: enable))
|
||||||
|
if case .cmdOk = r { return }
|
||||||
|
throw r
|
||||||
|
}
|
||||||
|
|
||||||
func apiExportArchive(config: ArchiveConfig) async throws {
|
func apiExportArchive(config: ArchiveConfig) async throws {
|
||||||
try await sendCommandOkResp(.apiExportArchive(config: config))
|
try await sendCommandOkResp(.apiExportArchive(config: config))
|
||||||
}
|
}
|
||||||
@ -1152,6 +1158,7 @@ func initializeChat(start: Bool, dbKey: String? = nil, refreshInvitations: Bool
|
|||||||
try apiSetTempFolder(tempFolder: getTempFilesDirectory().path)
|
try apiSetTempFolder(tempFolder: getTempFilesDirectory().path)
|
||||||
try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path)
|
try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path)
|
||||||
try setXFTPConfig(getXFTPCfg())
|
try setXFTPConfig(getXFTPCfg())
|
||||||
|
// try apiSetEncryptLocalFiles(privacyEncryptLocalFilesGroupDefault.get())
|
||||||
m.chatInitialized = true
|
m.chatInitialized = true
|
||||||
m.currentUser = try apiGetActiveUser()
|
m.currentUser = try apiGetActiveUser()
|
||||||
if m.currentUser == nil {
|
if m.currentUser == nil {
|
||||||
|
@ -216,6 +216,7 @@ func startChat() -> DBMigrationResult? {
|
|||||||
try apiSetTempFolder(tempFolder: getTempFilesDirectory().path)
|
try apiSetTempFolder(tempFolder: getTempFilesDirectory().path)
|
||||||
try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path)
|
try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path)
|
||||||
try setXFTPConfig(xftpConfig)
|
try setXFTPConfig(xftpConfig)
|
||||||
|
// try apiSetEncryptLocalFiles(privacyEncryptLocalFilesGroupDefault.get())
|
||||||
let justStarted = try apiStartChat()
|
let justStarted = try apiStartChat()
|
||||||
chatStarted = true
|
chatStarted = true
|
||||||
if justStarted {
|
if justStarted {
|
||||||
@ -351,6 +352,12 @@ func setXFTPConfig(_ cfg: XFTPFileConfig?) throws {
|
|||||||
throw r
|
throw r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func apiSetEncryptLocalFiles(_ enable: Boolean) throws {
|
||||||
|
let r = chatSendCmdSync(.apiSetEncryptLocalFiles(enable: enable))
|
||||||
|
if case .cmdOk = r { return }
|
||||||
|
throw r
|
||||||
|
}
|
||||||
|
|
||||||
func apiGetNtfMessage(nonce: String, encNtfInfo: String) -> NtfMessages? {
|
func apiGetNtfMessage(nonce: String, encNtfInfo: String) -> NtfMessages? {
|
||||||
guard apiGetActiveUser() != nil else {
|
guard apiGetActiveUser() != nil else {
|
||||||
logger.debug("no active user")
|
logger.debug("no active user")
|
||||||
|
@ -32,6 +32,7 @@ public enum ChatCommand {
|
|||||||
case setTempFolder(tempFolder: String)
|
case setTempFolder(tempFolder: String)
|
||||||
case setFilesFolder(filesFolder: String)
|
case setFilesFolder(filesFolder: String)
|
||||||
case apiSetXFTPConfig(config: XFTPFileConfig?)
|
case apiSetXFTPConfig(config: XFTPFileConfig?)
|
||||||
|
case apiSetEncryptLocalFiles(enable: Bool)
|
||||||
case apiExportArchive(config: ArchiveConfig)
|
case apiExportArchive(config: ArchiveConfig)
|
||||||
case apiImportArchive(config: ArchiveConfig)
|
case apiImportArchive(config: ArchiveConfig)
|
||||||
case apiDeleteStorage
|
case apiDeleteStorage
|
||||||
@ -114,8 +115,8 @@ public enum ChatCommand {
|
|||||||
case apiGetNetworkStatuses
|
case apiGetNetworkStatuses
|
||||||
case apiChatRead(type: ChatType, id: Int64, itemRange: (Int64, Int64))
|
case apiChatRead(type: ChatType, id: Int64, itemRange: (Int64, Int64))
|
||||||
case apiChatUnread(type: ChatType, id: Int64, unreadChat: Bool)
|
case apiChatUnread(type: ChatType, id: Int64, unreadChat: Bool)
|
||||||
case receiveFile(fileId: Int64, encrypted: Bool, inline: Bool?)
|
case receiveFile(fileId: Int64, encrypted: Bool?, inline: Bool?)
|
||||||
case setFileToReceive(fileId: Int64, encrypted: Bool)
|
case setFileToReceive(fileId: Int64, encrypted: Bool?)
|
||||||
case cancelFile(fileId: Int64)
|
case cancelFile(fileId: Int64)
|
||||||
case showVersion
|
case showVersion
|
||||||
case string(String)
|
case string(String)
|
||||||
@ -152,6 +153,7 @@ public enum ChatCommand {
|
|||||||
} else {
|
} else {
|
||||||
return "/_xftp off"
|
return "/_xftp off"
|
||||||
}
|
}
|
||||||
|
case let .apiSetEncryptLocalFiles(enable): return "/_files_encrypt \(onOff(enable))"
|
||||||
case let .apiExportArchive(cfg): return "/_db export \(encodeJSON(cfg))"
|
case let .apiExportArchive(cfg): return "/_db export \(encodeJSON(cfg))"
|
||||||
case let .apiImportArchive(cfg): return "/_db import \(encodeJSON(cfg))"
|
case let .apiImportArchive(cfg): return "/_db import \(encodeJSON(cfg))"
|
||||||
case .apiDeleteStorage: return "/_db delete"
|
case .apiDeleteStorage: return "/_db delete"
|
||||||
@ -247,13 +249,8 @@ public enum ChatCommand {
|
|||||||
case .apiGetNetworkStatuses: return "/_network_statuses"
|
case .apiGetNetworkStatuses: return "/_network_statuses"
|
||||||
case let .apiChatRead(type, id, itemRange: (from, to)): return "/_read chat \(ref(type, id)) from=\(from) to=\(to)"
|
case let .apiChatRead(type, id, itemRange: (from, to)): return "/_read chat \(ref(type, id)) from=\(from) to=\(to)"
|
||||||
case let .apiChatUnread(type, id, unreadChat): return "/_unread chat \(ref(type, id)) \(onOff(unreadChat))"
|
case let .apiChatUnread(type, id, unreadChat): return "/_unread chat \(ref(type, id)) \(onOff(unreadChat))"
|
||||||
case let .receiveFile(fileId, encrypted, inline):
|
case let .receiveFile(fileId, encrypt, inline): return "/freceive \(fileId)\(onOffParam("encrypt", encrypt))\(onOffParam("inline", inline))"
|
||||||
let s = "/freceive \(fileId) encrypt=\(onOff(encrypted))"
|
case let .setFileToReceive(fileId, encrypt): return "/_set_file_to_receive \(fileId)\(onOffParam("encrypt", encrypt))"
|
||||||
if let inline = inline {
|
|
||||||
return s + " inline=\(onOff(inline))"
|
|
||||||
}
|
|
||||||
return s
|
|
||||||
case let .setFileToReceive(fileId, encrypted): return "/_set_file_to_receive \(fileId) encrypt=\(onOff(encrypted))"
|
|
||||||
case let .cancelFile(fileId): return "/fcancel \(fileId)"
|
case let .cancelFile(fileId): return "/fcancel \(fileId)"
|
||||||
case .showVersion: return "/version"
|
case .showVersion: return "/version"
|
||||||
case let .string(str): return str
|
case let .string(str): return str
|
||||||
@ -283,6 +280,7 @@ public enum ChatCommand {
|
|||||||
case .setTempFolder: return "setTempFolder"
|
case .setTempFolder: return "setTempFolder"
|
||||||
case .setFilesFolder: return "setFilesFolder"
|
case .setFilesFolder: return "setFilesFolder"
|
||||||
case .apiSetXFTPConfig: return "apiSetXFTPConfig"
|
case .apiSetXFTPConfig: return "apiSetXFTPConfig"
|
||||||
|
case .apiSetEncryptLocalFiles: return "apiSetEncryptLocalFiles"
|
||||||
case .apiExportArchive: return "apiExportArchive"
|
case .apiExportArchive: return "apiExportArchive"
|
||||||
case .apiImportArchive: return "apiImportArchive"
|
case .apiImportArchive: return "apiImportArchive"
|
||||||
case .apiDeleteStorage: return "apiDeleteStorage"
|
case .apiDeleteStorage: return "apiDeleteStorage"
|
||||||
@ -420,6 +418,13 @@ public enum ChatCommand {
|
|||||||
b ? "on" : "off"
|
b ? "on" : "off"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func onOffParam(_ param: String, _ b: Bool?) -> String {
|
||||||
|
if let b = b {
|
||||||
|
return " \(param)=\(onOff(b))"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
private func maybePwd(_ pwd: String?) -> String {
|
private func maybePwd(_ pwd: String?) -> String {
|
||||||
pwd == "" || pwd == nil ? "" : " " + encodeJSON(pwd)
|
pwd == "" || pwd == nil ? "" : " " + encodeJSON(pwd)
|
||||||
}
|
}
|
||||||
|
@ -338,6 +338,7 @@ object ChatController {
|
|||||||
apiSetTempFolder(coreTmpDir.absolutePath)
|
apiSetTempFolder(coreTmpDir.absolutePath)
|
||||||
apiSetFilesFolder(appFilesDir.absolutePath)
|
apiSetFilesFolder(appFilesDir.absolutePath)
|
||||||
apiSetXFTPConfig(getXFTPCfg())
|
apiSetXFTPConfig(getXFTPCfg())
|
||||||
|
// apiSetEncryptLocalFiles(appPrefs.privacyEncryptLocalFiles.get())
|
||||||
val justStarted = apiStartChat()
|
val justStarted = apiStartChat()
|
||||||
val users = listUsers()
|
val users = listUsers()
|
||||||
chatModel.users.clear()
|
chatModel.users.clear()
|
||||||
@ -559,6 +560,8 @@ object ChatController {
|
|||||||
throw Error("apiSetXFTPConfig bad response: ${r.responseType} ${r.details}")
|
throw Error("apiSetXFTPConfig bad response: ${r.responseType} ${r.details}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun apiSetEncryptLocalFiles(enable: Boolean) = sendCommandOkResp(CC.ApiSetEncryptLocalFiles(enable))
|
||||||
|
|
||||||
suspend fun apiExportArchive(config: ArchiveConfig) {
|
suspend fun apiExportArchive(config: ArchiveConfig) {
|
||||||
val r = sendCmd(CC.ApiExportArchive(config))
|
val r = sendCmd(CC.ApiExportArchive(config))
|
||||||
if (r is CR.CmdOk) return
|
if (r is CR.CmdOk) return
|
||||||
@ -1327,6 +1330,13 @@ object ChatController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun sendCommandOkResp(cmd: CC): Boolean {
|
||||||
|
val r = sendCmd(cmd)
|
||||||
|
val ok = r is CR.CmdOk
|
||||||
|
if (!ok) apiErrorAlert(cmd.cmdType, generalGetString(MR.strings.error), r)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun apiGetVersion(): CoreVersionInfo? {
|
suspend fun apiGetVersion(): CoreVersionInfo? {
|
||||||
val r = sendCmd(CC.ShowVersion())
|
val r = sendCmd(CC.ShowVersion())
|
||||||
return if (r is CR.VersionInfo) {
|
return if (r is CR.VersionInfo) {
|
||||||
@ -1858,6 +1868,7 @@ sealed class CC {
|
|||||||
class SetTempFolder(val tempFolder: String): CC()
|
class SetTempFolder(val tempFolder: String): CC()
|
||||||
class SetFilesFolder(val filesFolder: String): CC()
|
class SetFilesFolder(val filesFolder: String): CC()
|
||||||
class ApiSetXFTPConfig(val config: XFTPFileConfig?): CC()
|
class ApiSetXFTPConfig(val config: XFTPFileConfig?): CC()
|
||||||
|
class ApiSetEncryptLocalFiles(val enable: Boolean): CC()
|
||||||
class ApiExportArchive(val config: ArchiveConfig): CC()
|
class ApiExportArchive(val config: ArchiveConfig): CC()
|
||||||
class ApiImportArchive(val config: ArchiveConfig): CC()
|
class ApiImportArchive(val config: ArchiveConfig): CC()
|
||||||
class ApiDeleteStorage: CC()
|
class ApiDeleteStorage: CC()
|
||||||
@ -1931,7 +1942,7 @@ sealed class CC {
|
|||||||
class ApiRejectContact(val contactReqId: Long): CC()
|
class ApiRejectContact(val contactReqId: Long): CC()
|
||||||
class ApiChatRead(val type: ChatType, val id: Long, val range: ItemRange): CC()
|
class ApiChatRead(val type: ChatType, val id: Long, val range: ItemRange): CC()
|
||||||
class ApiChatUnread(val type: ChatType, val id: Long, val unreadChat: Boolean): CC()
|
class ApiChatUnread(val type: ChatType, val id: Long, val unreadChat: Boolean): CC()
|
||||||
class ReceiveFile(val fileId: Long, val encrypted: Boolean, val inline: Boolean?): CC()
|
class ReceiveFile(val fileId: Long, val encrypt: Boolean?, val inline: Boolean?): CC()
|
||||||
class CancelFile(val fileId: Long): CC()
|
class CancelFile(val fileId: Long): CC()
|
||||||
class ShowVersion(): CC()
|
class ShowVersion(): CC()
|
||||||
|
|
||||||
@ -1963,6 +1974,7 @@ sealed class CC {
|
|||||||
is SetTempFolder -> "/_temp_folder $tempFolder"
|
is SetTempFolder -> "/_temp_folder $tempFolder"
|
||||||
is SetFilesFolder -> "/_files_folder $filesFolder"
|
is SetFilesFolder -> "/_files_folder $filesFolder"
|
||||||
is ApiSetXFTPConfig -> if (config != null) "/_xftp on ${json.encodeToString(config)}" else "/_xftp off"
|
is ApiSetXFTPConfig -> if (config != null) "/_xftp on ${json.encodeToString(config)}" else "/_xftp off"
|
||||||
|
is ApiSetEncryptLocalFiles -> "/_files_encrypt ${onOff(enable)}"
|
||||||
is ApiExportArchive -> "/_db export ${json.encodeToString(config)}"
|
is ApiExportArchive -> "/_db export ${json.encodeToString(config)}"
|
||||||
is ApiImportArchive -> "/_db import ${json.encodeToString(config)}"
|
is ApiImportArchive -> "/_db import ${json.encodeToString(config)}"
|
||||||
is ApiDeleteStorage -> "/_db delete"
|
is ApiDeleteStorage -> "/_db delete"
|
||||||
@ -2039,7 +2051,10 @@ sealed class CC {
|
|||||||
is ApiGetNetworkStatuses -> "/_network_statuses"
|
is ApiGetNetworkStatuses -> "/_network_statuses"
|
||||||
is ApiChatRead -> "/_read chat ${chatRef(type, id)} from=${range.from} to=${range.to}"
|
is ApiChatRead -> "/_read chat ${chatRef(type, id)} from=${range.from} to=${range.to}"
|
||||||
is ApiChatUnread -> "/_unread chat ${chatRef(type, id)} ${onOff(unreadChat)}"
|
is ApiChatUnread -> "/_unread chat ${chatRef(type, id)} ${onOff(unreadChat)}"
|
||||||
is ReceiveFile -> "/freceive $fileId encrypt=${onOff(encrypted)}" + (if (inline == null) "" else " inline=${onOff(inline)}")
|
is ReceiveFile ->
|
||||||
|
"/freceive $fileId" +
|
||||||
|
(if (encrypt == null) "" else " encrypt=${onOff(encrypt)}") +
|
||||||
|
(if (inline == null) "" else " inline=${onOff(inline)}")
|
||||||
is CancelFile -> "/fcancel $fileId"
|
is CancelFile -> "/fcancel $fileId"
|
||||||
is ShowVersion -> "/version"
|
is ShowVersion -> "/version"
|
||||||
}
|
}
|
||||||
@ -2063,6 +2078,7 @@ sealed class CC {
|
|||||||
is SetTempFolder -> "setTempFolder"
|
is SetTempFolder -> "setTempFolder"
|
||||||
is SetFilesFolder -> "setFilesFolder"
|
is SetFilesFolder -> "setFilesFolder"
|
||||||
is ApiSetXFTPConfig -> "apiSetXFTPConfig"
|
is ApiSetXFTPConfig -> "apiSetXFTPConfig"
|
||||||
|
is ApiSetEncryptLocalFiles -> "apiSetEncryptLocalFiles"
|
||||||
is ApiExportArchive -> "apiExportArchive"
|
is ApiExportArchive -> "apiExportArchive"
|
||||||
is ApiImportArchive -> "apiImportArchive"
|
is ApiImportArchive -> "apiImportArchive"
|
||||||
is ApiDeleteStorage -> "apiDeleteStorage"
|
is ApiDeleteStorage -> "apiDeleteStorage"
|
||||||
|
@ -209,6 +209,7 @@ newChatController ChatDatabase {chatStore, agentStore} user cfg@ChatConfig {agen
|
|||||||
cleanupManagerAsync <- newTVarIO Nothing
|
cleanupManagerAsync <- newTVarIO Nothing
|
||||||
timedItemThreads <- atomically TM.empty
|
timedItemThreads <- atomically TM.empty
|
||||||
showLiveItems <- newTVarIO False
|
showLiveItems <- newTVarIO False
|
||||||
|
encryptLocalFiles <- newTVarIO False
|
||||||
userXFTPFileConfig <- newTVarIO $ xftpFileConfig cfg
|
userXFTPFileConfig <- newTVarIO $ xftpFileConfig cfg
|
||||||
tempDirectory <- newTVarIO tempDir
|
tempDirectory <- newTVarIO tempDir
|
||||||
contactMergeEnabled <- newTVarIO True
|
contactMergeEnabled <- newTVarIO True
|
||||||
@ -236,6 +237,7 @@ newChatController ChatDatabase {chatStore, agentStore} user cfg@ChatConfig {agen
|
|||||||
cleanupManagerAsync,
|
cleanupManagerAsync,
|
||||||
timedItemThreads,
|
timedItemThreads,
|
||||||
showLiveItems,
|
showLiveItems,
|
||||||
|
encryptLocalFiles,
|
||||||
userXFTPFileConfig,
|
userXFTPFileConfig,
|
||||||
tempDirectory,
|
tempDirectory,
|
||||||
logFilePath = logFile,
|
logFilePath = logFile,
|
||||||
@ -515,6 +517,7 @@ processChatCommand = \case
|
|||||||
APISetXFTPConfig cfg -> do
|
APISetXFTPConfig cfg -> do
|
||||||
asks userXFTPFileConfig >>= atomically . (`writeTVar` cfg)
|
asks userXFTPFileConfig >>= atomically . (`writeTVar` cfg)
|
||||||
ok_
|
ok_
|
||||||
|
APISetEncryptLocalFiles on -> chatWriteVar encryptLocalFiles on >> ok_
|
||||||
SetContactMergeEnabled onOff -> do
|
SetContactMergeEnabled onOff -> do
|
||||||
asks contactMergeEnabled >>= atomically . (`writeTVar` onOff)
|
asks contactMergeEnabled >>= atomically . (`writeTVar` onOff)
|
||||||
ok_
|
ok_
|
||||||
@ -1773,19 +1776,16 @@ processChatCommand = \case
|
|||||||
ForwardFile chatName fileId -> forwardFile chatName fileId SendFile
|
ForwardFile chatName fileId -> forwardFile chatName fileId SendFile
|
||||||
ForwardImage chatName fileId -> forwardFile chatName fileId SendImage
|
ForwardImage chatName fileId -> forwardFile chatName fileId SendImage
|
||||||
SendFileDescription _chatName _f -> pure $ chatCmdError Nothing "TODO"
|
SendFileDescription _chatName _f -> pure $ chatCmdError Nothing "TODO"
|
||||||
ReceiveFile fileId encrypted rcvInline_ filePath_ -> withUser $ \_ ->
|
ReceiveFile fileId encrypted_ rcvInline_ filePath_ -> withUser $ \_ ->
|
||||||
withChatLock "receiveFile" . procCmd $ do
|
withChatLock "receiveFile" . procCmd $ do
|
||||||
(user, ft) <- withStore (`getRcvFileTransferById` fileId)
|
(user, ft) <- withStore (`getRcvFileTransferById` fileId)
|
||||||
ft' <- if encrypted then encryptLocalFile ft else pure ft
|
encrypt <- (`fromMaybe` encrypted_) <$> chatReadVar encryptLocalFiles
|
||||||
|
ft' <- (if encrypt then setFileToEncrypt else pure) ft
|
||||||
receiveFile' user ft' rcvInline_ filePath_
|
receiveFile' user ft' rcvInline_ filePath_
|
||||||
where
|
SetFileToReceive fileId encrypted_ -> withUser $ \_ -> do
|
||||||
encryptLocalFile ft = do
|
|
||||||
cfArgs <- liftIO $ CF.randomArgs
|
|
||||||
withStore' $ \db -> setFileCryptoArgs db fileId cfArgs
|
|
||||||
pure (ft :: RcvFileTransfer) {cryptoArgs = Just cfArgs}
|
|
||||||
SetFileToReceive fileId encrypted -> withUser $ \_ -> do
|
|
||||||
withChatLock "setFileToReceive" . procCmd $ do
|
withChatLock "setFileToReceive" . procCmd $ do
|
||||||
cfArgs <- if encrypted then Just <$> liftIO CF.randomArgs else pure Nothing
|
encrypt <- (`fromMaybe` encrypted_) <$> chatReadVar encryptLocalFiles
|
||||||
|
cfArgs <- if encrypt then Just <$> liftIO CF.randomArgs else pure Nothing
|
||||||
withStore' $ \db -> setRcvFileToReceive db fileId cfArgs
|
withStore' $ \db -> setRcvFileToReceive db fileId cfArgs
|
||||||
ok_
|
ok_
|
||||||
CancelFile fileId -> withUser $ \user@User {userId} ->
|
CancelFile fileId -> withUser $ \user@User {userId} ->
|
||||||
@ -2410,6 +2410,12 @@ toFSFilePath :: ChatMonad' m => FilePath -> m FilePath
|
|||||||
toFSFilePath f =
|
toFSFilePath f =
|
||||||
maybe f (</> f) <$> (readTVarIO =<< asks filesFolder)
|
maybe f (</> f) <$> (readTVarIO =<< asks filesFolder)
|
||||||
|
|
||||||
|
setFileToEncrypt :: ChatMonad m => RcvFileTransfer -> m RcvFileTransfer
|
||||||
|
setFileToEncrypt ft@RcvFileTransfer {fileId} = do
|
||||||
|
cfArgs <- liftIO CF.randomArgs
|
||||||
|
withStore' $ \db -> setFileCryptoArgs db fileId cfArgs
|
||||||
|
pure (ft :: RcvFileTransfer) {cryptoArgs = Just cfArgs}
|
||||||
|
|
||||||
receiveFile' :: ChatMonad m => User -> RcvFileTransfer -> Maybe Bool -> Maybe FilePath -> m ChatResponse
|
receiveFile' :: ChatMonad m => User -> RcvFileTransfer -> Maybe Bool -> Maybe FilePath -> m ChatResponse
|
||||||
receiveFile' user ft rcvInline_ filePath_ = do
|
receiveFile' user ft rcvInline_ filePath_ = do
|
||||||
(CRRcvFileAccepted user <$> acceptFileReceive user ft rcvInline_ filePath_) `catchChatError` processError
|
(CRRcvFileAccepted user <$> acceptFileReceive user ft rcvInline_ filePath_) `catchChatError` processError
|
||||||
@ -3931,14 +3937,17 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
|
|||||||
inline <- receiveInlineMode fInv (Just mc) fileChunkSize
|
inline <- receiveInlineMode fInv (Just mc) fileChunkSize
|
||||||
ft@RcvFileTransfer {fileId, xftpRcvFile} <- withStore $ \db -> createRcvFT db fInv inline fileChunkSize
|
ft@RcvFileTransfer {fileId, xftpRcvFile} <- withStore $ \db -> createRcvFT db fInv inline fileChunkSize
|
||||||
let fileProtocol = if isJust xftpRcvFile then FPXFTP else FPSMP
|
let fileProtocol = if isJust xftpRcvFile then FPXFTP else FPSMP
|
||||||
(filePath, fileStatus) <- case inline of
|
(filePath, fileStatus, ft') <- case inline of
|
||||||
Just IFMSent -> do
|
Just IFMSent -> do
|
||||||
|
encrypt <- chatReadVar encryptLocalFiles
|
||||||
|
ft' <- (if encrypt then setFileToEncrypt else pure) ft
|
||||||
fPath <- getRcvFilePath fileId Nothing fileName True
|
fPath <- getRcvFilePath fileId Nothing fileName True
|
||||||
withStore' $ \db -> startRcvInlineFT db user ft fPath inline
|
withStore' $ \db -> startRcvInlineFT db user ft' fPath inline
|
||||||
pure (Just fPath, CIFSRcvAccepted)
|
pure (Just fPath, CIFSRcvAccepted, ft')
|
||||||
_ -> pure (Nothing, CIFSRcvInvitation)
|
_ -> pure (Nothing, CIFSRcvInvitation, ft)
|
||||||
let fileSource = CF.plain <$> filePath
|
let RcvFileTransfer {cryptoArgs} = ft'
|
||||||
pure (ft, CIFile {fileId, fileName, fileSize, fileSource, fileStatus, fileProtocol})
|
fileSource = (`CryptoFile` cryptoArgs) <$> filePath
|
||||||
|
pure (ft', CIFile {fileId, fileName, fileSize, fileSource, fileStatus, fileProtocol})
|
||||||
|
|
||||||
messageUpdate :: Contact -> SharedMsgId -> MsgContent -> RcvMessage -> MsgMeta -> Maybe Int -> Maybe Bool -> m ()
|
messageUpdate :: Contact -> SharedMsgId -> MsgContent -> RcvMessage -> MsgMeta -> Maybe Int -> Maybe Bool -> m ()
|
||||||
messageUpdate ct@Contact {contactId} sharedMsgId mc msg@RcvMessage {msgId} msgMeta ttl live_ = do
|
messageUpdate ct@Contact {contactId} sharedMsgId mc msg@RcvMessage {msgId} msgMeta ttl live_ = do
|
||||||
@ -5567,6 +5576,7 @@ chatCommandP =
|
|||||||
("/_files_folder " <|> "/files_folder ") *> (SetFilesFolder <$> filePath),
|
("/_files_folder " <|> "/files_folder ") *> (SetFilesFolder <$> filePath),
|
||||||
"/_xftp " *> (APISetXFTPConfig <$> ("on " *> (Just <$> jsonP) <|> ("off" $> Nothing))),
|
"/_xftp " *> (APISetXFTPConfig <$> ("on " *> (Just <$> jsonP) <|> ("off" $> Nothing))),
|
||||||
"/xftp " *> (APISetXFTPConfig <$> ("on" *> (Just <$> xftpCfgP) <|> ("off" $> Nothing))),
|
"/xftp " *> (APISetXFTPConfig <$> ("on" *> (Just <$> xftpCfgP) <|> ("off" $> Nothing))),
|
||||||
|
"/_files_encrypt " *> (APISetEncryptLocalFiles <$> onOffP),
|
||||||
"/contact_merge " *> (SetContactMergeEnabled <$> onOffP),
|
"/contact_merge " *> (SetContactMergeEnabled <$> onOffP),
|
||||||
"/_db export " *> (APIExportArchive <$> jsonP),
|
"/_db export " *> (APIExportArchive <$> jsonP),
|
||||||
"/db export" $> ExportArchive,
|
"/db export" $> ExportArchive,
|
||||||
@ -5743,8 +5753,8 @@ chatCommandP =
|
|||||||
("/fforward " <|> "/ff ") *> (ForwardFile <$> chatNameP' <* A.space <*> A.decimal),
|
("/fforward " <|> "/ff ") *> (ForwardFile <$> chatNameP' <* A.space <*> A.decimal),
|
||||||
("/image_forward " <|> "/imgf ") *> (ForwardImage <$> chatNameP' <* A.space <*> A.decimal),
|
("/image_forward " <|> "/imgf ") *> (ForwardImage <$> chatNameP' <* A.space <*> A.decimal),
|
||||||
("/fdescription " <|> "/fd") *> (SendFileDescription <$> chatNameP' <* A.space <*> filePath),
|
("/fdescription " <|> "/fd") *> (SendFileDescription <$> chatNameP' <* A.space <*> filePath),
|
||||||
("/freceive " <|> "/fr ") *> (ReceiveFile <$> A.decimal <*> (" encrypt=" *> onOffP <|> pure False) <*> optional (" inline=" *> onOffP) <*> optional (A.space *> filePath)),
|
("/freceive " <|> "/fr ") *> (ReceiveFile <$> A.decimal <*> optional (" encrypt=" *> onOffP) <*> optional (" inline=" *> onOffP) <*> optional (A.space *> filePath)),
|
||||||
"/_set_file_to_receive " *> (SetFileToReceive <$> A.decimal <*> (" encrypt=" *> onOffP <|> pure False)),
|
"/_set_file_to_receive " *> (SetFileToReceive <$> A.decimal <*> optional (" encrypt=" *> onOffP)),
|
||||||
("/fcancel " <|> "/fc ") *> (CancelFile <$> A.decimal),
|
("/fcancel " <|> "/fc ") *> (CancelFile <$> A.decimal),
|
||||||
("/fstatus " <|> "/fs ") *> (FileStatus <$> A.decimal),
|
("/fstatus " <|> "/fs ") *> (FileStatus <$> A.decimal),
|
||||||
"/simplex" *> (ConnectSimplex <$> incognitoP),
|
"/simplex" *> (ConnectSimplex <$> incognitoP),
|
||||||
|
@ -179,6 +179,7 @@ data ChatController = ChatController
|
|||||||
cleanupManagerAsync :: TVar (Maybe (Async ())),
|
cleanupManagerAsync :: TVar (Maybe (Async ())),
|
||||||
timedItemThreads :: TMap (ChatRef, ChatItemId) (TVar (Maybe (Weak ThreadId))),
|
timedItemThreads :: TMap (ChatRef, ChatItemId) (TVar (Maybe (Weak ThreadId))),
|
||||||
showLiveItems :: TVar Bool,
|
showLiveItems :: TVar Bool,
|
||||||
|
encryptLocalFiles :: TVar Bool,
|
||||||
userXFTPFileConfig :: TVar (Maybe XFTPFileConfig),
|
userXFTPFileConfig :: TVar (Maybe XFTPFileConfig),
|
||||||
tempDirectory :: TVar (Maybe FilePath),
|
tempDirectory :: TVar (Maybe FilePath),
|
||||||
logFilePath :: Maybe FilePath,
|
logFilePath :: Maybe FilePath,
|
||||||
@ -221,6 +222,7 @@ data ChatCommand
|
|||||||
| SetTempFolder FilePath
|
| SetTempFolder FilePath
|
||||||
| SetFilesFolder FilePath
|
| SetFilesFolder FilePath
|
||||||
| APISetXFTPConfig (Maybe XFTPFileConfig)
|
| APISetXFTPConfig (Maybe XFTPFileConfig)
|
||||||
|
| APISetEncryptLocalFiles Bool
|
||||||
| SetContactMergeEnabled Bool
|
| SetContactMergeEnabled Bool
|
||||||
| APIExportArchive ArchiveConfig
|
| APIExportArchive ArchiveConfig
|
||||||
| ExportArchive
|
| ExportArchive
|
||||||
@ -393,8 +395,8 @@ data ChatCommand
|
|||||||
| ForwardFile ChatName FileTransferId
|
| ForwardFile ChatName FileTransferId
|
||||||
| ForwardImage ChatName FileTransferId
|
| ForwardImage ChatName FileTransferId
|
||||||
| SendFileDescription ChatName FilePath
|
| SendFileDescription ChatName FilePath
|
||||||
| ReceiveFile {fileId :: FileTransferId, storeEncrypted :: Bool, fileInline :: Maybe Bool, filePath :: Maybe FilePath}
|
| ReceiveFile {fileId :: FileTransferId, storeEncrypted :: Maybe Bool, fileInline :: Maybe Bool, filePath :: Maybe FilePath}
|
||||||
| SetFileToReceive {fileId :: FileTransferId, storeEncrypted :: Bool}
|
| SetFileToReceive {fileId :: FileTransferId, storeEncrypted :: Maybe Bool}
|
||||||
| CancelFile FileTransferId
|
| CancelFile FileTransferId
|
||||||
| FileStatus FileTransferId
|
| FileStatus FileTransferId
|
||||||
| ShowProfile -- UserId (not used in UI)
|
| ShowProfile -- UserId (not used in UI)
|
||||||
|
@ -166,7 +166,7 @@ responseToView user_ ChatConfig {logLevel, showReactions, showReceipts, testView
|
|||||||
CRRcvFileDescrReady _ _ -> []
|
CRRcvFileDescrReady _ _ -> []
|
||||||
CRRcvFileDescrNotReady _ _ -> []
|
CRRcvFileDescrNotReady _ _ -> []
|
||||||
CRRcvFileProgressXFTP {} -> []
|
CRRcvFileProgressXFTP {} -> []
|
||||||
CRRcvFileAccepted u ci -> ttyUser u $ savingFile' testView ci
|
CRRcvFileAccepted u ci -> ttyUser u $ savingFile' ci
|
||||||
CRRcvFileAcceptedSndCancelled u ft -> ttyUser u $ viewRcvFileSndCancelled ft
|
CRRcvFileAcceptedSndCancelled u ft -> ttyUser u $ viewRcvFileSndCancelled ft
|
||||||
CRSndFileCancelled u _ ftm fts -> ttyUser u $ viewSndFileCancelled ftm fts
|
CRSndFileCancelled u _ ftm fts -> ttyUser u $ viewSndFileCancelled ftm fts
|
||||||
CRRcvFileCancelled u _ ft -> ttyUser u $ receivingFile_ "cancelled" ft
|
CRRcvFileCancelled u _ ft -> ttyUser u $ receivingFile_ "cancelled" ft
|
||||||
@ -178,10 +178,10 @@ responseToView user_ ChatConfig {logLevel, showReactions, showReceipts, testView
|
|||||||
CRContactUpdated {user = u, fromContact = c, toContact = c'} -> ttyUser u $ viewContactUpdated c c' <> viewContactPrefsUpdated u c c'
|
CRContactUpdated {user = u, fromContact = c, toContact = c'} -> ttyUser u $ viewContactUpdated c c' <> viewContactPrefsUpdated u c c'
|
||||||
CRContactsMerged u intoCt mergedCt ct' -> ttyUser u $ viewContactsMerged intoCt mergedCt ct'
|
CRContactsMerged u intoCt mergedCt ct' -> ttyUser u $ viewContactsMerged intoCt mergedCt ct'
|
||||||
CRReceivedContactRequest u UserContactRequest {localDisplayName = c, profile} -> ttyUser u $ viewReceivedContactRequest c profile
|
CRReceivedContactRequest u UserContactRequest {localDisplayName = c, profile} -> ttyUser u $ viewReceivedContactRequest c profile
|
||||||
CRRcvFileStart u ci -> ttyUser u $ receivingFile_' "started" ci
|
CRRcvFileStart u ci -> ttyUser u $ receivingFile_' testView "started" ci
|
||||||
CRRcvFileComplete u ci -> ttyUser u $ receivingFile_' "completed" ci
|
CRRcvFileComplete u ci -> ttyUser u $ receivingFile_' testView "completed" ci
|
||||||
CRRcvFileSndCancelled u _ ft -> ttyUser u $ viewRcvFileSndCancelled ft
|
CRRcvFileSndCancelled u _ ft -> ttyUser u $ viewRcvFileSndCancelled ft
|
||||||
CRRcvFileError u ci e -> ttyUser u $ receivingFile_' "error" ci <> [sShow e]
|
CRRcvFileError u ci e -> ttyUser u $ receivingFile_' testView "error" ci <> [sShow e]
|
||||||
CRSndFileStart u _ ft -> ttyUser u $ sendingFile_ "started" ft
|
CRSndFileStart u _ ft -> ttyUser u $ sendingFile_ "started" ft
|
||||||
CRSndFileComplete u _ ft -> ttyUser u $ sendingFile_ "completed" ft
|
CRSndFileComplete u _ ft -> ttyUser u $ sendingFile_ "completed" ft
|
||||||
CRSndFileStartXFTP {} -> []
|
CRSndFileStartXFTP {} -> []
|
||||||
@ -1449,27 +1449,28 @@ humanReadableSize size
|
|||||||
mB = kB * 1024
|
mB = kB * 1024
|
||||||
gB = mB * 1024
|
gB = mB * 1024
|
||||||
|
|
||||||
savingFile' :: Bool -> AChatItem -> [StyledString]
|
savingFile' :: AChatItem -> [StyledString]
|
||||||
savingFile' testView (AChatItem _ _ chat ChatItem {file = Just CIFile {fileId, fileSource = Just (CryptoFile filePath cfArgs_)}, chatDir}) =
|
savingFile' (AChatItem _ _ chat ChatItem {file = Just CIFile {fileId, fileSource = Just (CryptoFile filePath _)}, chatDir}) =
|
||||||
let from = case (chat, chatDir) of
|
["saving file " <> sShow fileId <> fileFrom chat chatDir <> " to " <> plain filePath]
|
||||||
(DirectChat Contact {localDisplayName = c}, CIDirectRcv) -> " from " <> ttyContact c
|
savingFile' _ = ["saving file"] -- shouldn't happen
|
||||||
(_, CIGroupRcv GroupMember {localDisplayName = m}) -> " from " <> ttyContact m
|
|
||||||
_ -> ""
|
|
||||||
in ["saving file " <> sShow fileId <> from <> " to " <> plain filePath] <> cfArgsStr
|
|
||||||
where
|
|
||||||
cfArgsStr = case cfArgs_ of
|
|
||||||
Just cfArgs@(CFArgs key nonce)
|
|
||||||
| testView -> [plain $ LB.unpack $ J.encode cfArgs]
|
|
||||||
| otherwise -> [plain $ "encryption key: " <> strEncode key <> ", nonce: " <> strEncode nonce]
|
|
||||||
_ -> []
|
|
||||||
savingFile' _ _ = ["saving file"] -- shouldn't happen
|
|
||||||
|
|
||||||
receivingFile_' :: StyledString -> AChatItem -> [StyledString]
|
receivingFile_' :: Bool -> String -> AChatItem -> [StyledString]
|
||||||
receivingFile_' status (AChatItem _ _ (DirectChat c) ChatItem {file = Just CIFile {fileId, fileName}, chatDir = CIDirectRcv}) =
|
receivingFile_' testView status (AChatItem _ _ chat ChatItem {file = Just CIFile {fileId, fileName, fileSource = Just (CryptoFile _ cfArgs_)}, chatDir}) =
|
||||||
[status <> " receiving " <> fileTransferStr fileId fileName <> " from " <> ttyContact' c]
|
[plain status <> " receiving " <> fileTransferStr fileId fileName <> fileFrom chat chatDir] <> cfArgsStr cfArgs_
|
||||||
receivingFile_' status (AChatItem _ _ _ ChatItem {file = Just CIFile {fileId, fileName}, chatDir = CIGroupRcv m}) =
|
where
|
||||||
[status <> " receiving " <> fileTransferStr fileId fileName <> " from " <> ttyMember m]
|
cfArgsStr (Just cfArgs@(CFArgs key nonce)) = [plain s | status == "completed"]
|
||||||
receivingFile_' status _ = [status <> " receiving file"] -- shouldn't happen
|
where
|
||||||
|
s =
|
||||||
|
if testView
|
||||||
|
then LB.toStrict $ J.encode cfArgs
|
||||||
|
else "encryption key: " <> strEncode key <> ", nonce: " <> strEncode nonce
|
||||||
|
cfArgsStr _ = []
|
||||||
|
receivingFile_' _ status _ = [plain status <> " receiving file"] -- shouldn't happen
|
||||||
|
|
||||||
|
fileFrom :: ChatInfo c -> CIDirection c d -> StyledString
|
||||||
|
fileFrom (DirectChat ct) CIDirectRcv = " from " <> ttyContact' ct
|
||||||
|
fileFrom _ (CIGroupRcv m) = " from " <> ttyMember m
|
||||||
|
fileFrom _ _ = ""
|
||||||
|
|
||||||
receivingFile_ :: StyledString -> RcvFileTransfer -> [StyledString]
|
receivingFile_ :: StyledString -> RcvFileTransfer -> [StyledString]
|
||||||
receivingFile_ status ft@RcvFileTransfer {senderDisplayName = c} =
|
receivingFile_ status ft@RcvFileTransfer {senderDisplayName = c} =
|
||||||
|
@ -33,6 +33,7 @@ chatFileTests = do
|
|||||||
describe "send and receive file" $ fileTestMatrix2 runTestFileTransfer
|
describe "send and receive file" $ fileTestMatrix2 runTestFileTransfer
|
||||||
describe "send file, receive and locally encrypt file" $ fileTestMatrix2 runTestFileTransferEncrypted
|
describe "send file, receive and locally encrypt file" $ fileTestMatrix2 runTestFileTransferEncrypted
|
||||||
it "send and receive file inline (without accepting)" testInlineFileTransfer
|
it "send and receive file inline (without accepting)" testInlineFileTransfer
|
||||||
|
it "send inline file, receive (without accepting) and locally encrypt" testInlineFileTransferEncrypted
|
||||||
xit'' "accept inline file transfer, sender cancels during transfer" testAcceptInlineFileSndCancelDuringTransfer
|
xit'' "accept inline file transfer, sender cancels during transfer" testAcceptInlineFileSndCancelDuringTransfer
|
||||||
it "send and receive small file inline (default config)" testSmallInlineFileTransfer
|
it "send and receive small file inline (default config)" testSmallInlineFileTransfer
|
||||||
it "small file sent without acceptance is ignored in terminal by default" testSmallInlineFileIgnored
|
it "small file sent without acceptance is ignored in terminal by default" testSmallInlineFileIgnored
|
||||||
@ -107,7 +108,6 @@ runTestFileTransferEncrypted alice bob = do
|
|||||||
bob <## "use /fr 1 [<dir>/ | <path>] to receive it"
|
bob <## "use /fr 1 [<dir>/ | <path>] to receive it"
|
||||||
bob ##> "/fr 1 encrypt=on ./tests/tmp"
|
bob ##> "/fr 1 encrypt=on ./tests/tmp"
|
||||||
bob <## "saving file 1 from alice to ./tests/tmp/test.pdf"
|
bob <## "saving file 1 from alice to ./tests/tmp/test.pdf"
|
||||||
Just (CFArgs key nonce) <- J.decode . LB.pack <$> getTermLine bob
|
|
||||||
concurrently_
|
concurrently_
|
||||||
(bob <## "started receiving file 1 (test.pdf) from alice")
|
(bob <## "started receiving file 1 (test.pdf) from alice")
|
||||||
(alice <## "started sending file 1 (test.pdf) to bob")
|
(alice <## "started sending file 1 (test.pdf) to bob")
|
||||||
@ -123,6 +123,7 @@ runTestFileTransferEncrypted alice bob = do
|
|||||||
"completed sending file 1 (test.pdf) to bob"
|
"completed sending file 1 (test.pdf) to bob"
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
Just (CFArgs key nonce) <- J.decode . LB.pack <$> getTermLine bob
|
||||||
src <- B.readFile "./tests/fixtures/test.pdf"
|
src <- B.readFile "./tests/fixtures/test.pdf"
|
||||||
-- dest <- B.readFile "./tests/tmp/test.pdf"
|
-- dest <- B.readFile "./tests/tmp/test.pdf"
|
||||||
-- dest `shouldBe` src
|
-- dest `shouldBe` src
|
||||||
@ -154,6 +155,34 @@ testInlineFileTransfer =
|
|||||||
where
|
where
|
||||||
cfg = testCfg {inlineFiles = defaultInlineFilesConfig {offerChunks = 100, sendChunks = 100, receiveChunks = 100}}
|
cfg = testCfg {inlineFiles = defaultInlineFilesConfig {offerChunks = 100, sendChunks = 100, receiveChunks = 100}}
|
||||||
|
|
||||||
|
testInlineFileTransferEncrypted :: HasCallStack => FilePath -> IO ()
|
||||||
|
testInlineFileTransferEncrypted =
|
||||||
|
testChatCfg2 cfg aliceProfile bobProfile $ \alice bob -> do
|
||||||
|
connectUsers alice bob
|
||||||
|
bob ##> "/_files_folder ./tests/tmp/"
|
||||||
|
bob <## "ok"
|
||||||
|
bob ##> "/_files_encrypt on"
|
||||||
|
bob <## "ok"
|
||||||
|
alice ##> "/_send @2 json {\"msgContent\":{\"type\":\"voice\", \"duration\":10, \"text\":\"\"}, \"filePath\":\"./tests/fixtures/test.jpg\"}"
|
||||||
|
alice <# "@bob voice message (00:10)"
|
||||||
|
alice <# "/f @bob ./tests/fixtures/test.jpg"
|
||||||
|
-- below is not shown in "sent" mode
|
||||||
|
-- alice <## "use /fc 1 to cancel sending"
|
||||||
|
bob <# "alice> voice message (00:10)"
|
||||||
|
bob <# "alice> sends file test.jpg (136.5 KiB / 139737 bytes)"
|
||||||
|
-- below is not shown in "sent" mode
|
||||||
|
-- bob <## "use /fr 1 [<dir>/ | <path>] 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")
|
||||||
|
Just (CFArgs key nonce) <- J.decode . LB.pack <$> getTermLine bob
|
||||||
|
src <- B.readFile "./tests/fixtures/test.jpg"
|
||||||
|
Right dest <- chatReadFile "./tests/tmp/test.jpg" (strEncode key) (strEncode nonce)
|
||||||
|
LB.toStrict dest `shouldBe` src
|
||||||
|
where
|
||||||
|
cfg = testCfg {inlineFiles = defaultInlineFilesConfig {offerChunks = 100, sendChunks = 100, receiveChunks = 100}}
|
||||||
|
|
||||||
testAcceptInlineFileSndCancelDuringTransfer :: HasCallStack => FilePath -> IO ()
|
testAcceptInlineFileSndCancelDuringTransfer :: HasCallStack => FilePath -> IO ()
|
||||||
testAcceptInlineFileSndCancelDuringTransfer =
|
testAcceptInlineFileSndCancelDuringTransfer =
|
||||||
testChatCfg2 cfg aliceProfile bobProfile $ \alice bob -> do
|
testChatCfg2 cfg aliceProfile bobProfile $ \alice bob -> do
|
||||||
@ -1077,10 +1106,10 @@ testXFTPFileTransferEncrypted =
|
|||||||
bob <## "use /fr 1 [<dir>/ | <path>] to receive it"
|
bob <## "use /fr 1 [<dir>/ | <path>] to receive it"
|
||||||
bob ##> "/fr 1 encrypt=on ./tests/tmp/bob/"
|
bob ##> "/fr 1 encrypt=on ./tests/tmp/bob/"
|
||||||
bob <## "saving file 1 from alice to ./tests/tmp/bob/test.pdf"
|
bob <## "saving file 1 from alice to ./tests/tmp/bob/test.pdf"
|
||||||
Just (CFArgs key nonce) <- J.decode . LB.pack <$> getTermLine bob
|
|
||||||
alice <## "completed uploading file 1 (test.pdf) for bob"
|
alice <## "completed uploading file 1 (test.pdf) for bob"
|
||||||
bob <## "started receiving file 1 (test.pdf) from alice"
|
bob <## "started receiving file 1 (test.pdf) from alice"
|
||||||
bob <## "completed receiving file 1 (test.pdf) from alice"
|
bob <## "completed receiving file 1 (test.pdf) from alice"
|
||||||
|
Just (CFArgs key nonce) <- J.decode . LB.pack <$> getTermLine bob
|
||||||
Right dest <- chatReadFile "./tests/tmp/bob/test.pdf" (strEncode key) (strEncode nonce)
|
Right dest <- chatReadFile "./tests/tmp/bob/test.pdf" (strEncode key) (strEncode nonce)
|
||||||
LB.length dest `shouldBe` fromIntegral srcLen
|
LB.length dest `shouldBe` fromIntegral srcLen
|
||||||
LB.toStrict dest `shouldBe` src
|
LB.toStrict dest `shouldBe` src
|
||||||
|
Loading…
Reference in New Issue
Block a user