From d2d450d1d7a7c7faa38fdb35c4735e30b8a08d85 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Mon, 20 May 2024 17:23:29 +0400 Subject: [PATCH 1/3] core: check known relays before file reception, support user approval of unknown relays (#4043) * core: check known relays before file reception, support user approval of unknown relays * comment * reset on not approved agent error * add privacyAskToApproveRelays to AppSettings * filter distinct servers * update simplexmq * remember user_approved_relays * refactor * rename * update simplexmq --------- Co-authored-by: Evgeny Poberezkin --- cabal.project | 2 +- scripts/nix/sha256map.nix | 2 +- simplex-chat.cabal | 1 + src/Simplex/Chat.hs | 110 +++++++++++++----- src/Simplex/Chat/AppSettings.hs | 6 + src/Simplex/Chat/Controller.hs | 7 +- src/Simplex/Chat/Messages.hs | 8 ++ ...20240515_rcv_files_user_approved_relays.hs | 18 +++ src/Simplex/Chat/Migrations/chat_schema.sql | 3 +- src/Simplex/Chat/Store/Files.hs | 49 ++++---- src/Simplex/Chat/Store/Migrations.hs | 4 +- src/Simplex/Chat/Types.hs | 3 +- src/Simplex/Chat/View.hs | 2 + 13 files changed, 155 insertions(+), 60 deletions(-) create mode 100644 src/Simplex/Chat/Migrations/M20240515_rcv_files_user_approved_relays.hs diff --git a/cabal.project b/cabal.project index 1a6942e4b5..190c6e06e0 100644 --- a/cabal.project +++ b/cabal.project @@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 1116aeeea1869e0de38e9faccea76b329b549804 + tag: 2e5433676eaa5de93ed1ea9726706b9633308477 source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 06875457b0..512638d454 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."1116aeeea1869e0de38e9faccea76b329b549804" = "07ynn7f70hfsdrirmhb9zd257bx90d29l5gjyhh50wd12gaqdm0w"; + "https://github.com/simplex-chat/simplexmq.git"."2e5433676eaa5de93ed1ea9726706b9633308477" = "0ichdf5vsdizqxqy8amx3f5grx5sghiv2gajd2w3l73vnr2rv3bd"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d"; "https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl"; diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 525dc6295f..2b0a55bb68 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -144,6 +144,7 @@ library Simplex.Chat.Migrations.M20240430_ui_theme Simplex.Chat.Migrations.M20240501_chat_deleted Simplex.Chat.Migrations.M20240510_chat_items_via_proxy + Simplex.Chat.Migrations.M20240515_rcv_files_user_approved_relays Simplex.Chat.Mobile Simplex.Chat.Mobile.File Simplex.Chat.Mobile.Shared diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index acdfe116ff..743cef932c 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -47,6 +47,7 @@ import Data.Map.Strict (Map) import qualified Data.Map.Strict as M import Data.Maybe (catMaybes, fromMaybe, isJust, isNothing, listToMaybe, mapMaybe, maybeToList) import Data.Ord (Down (..)) +import qualified Data.Set as S import Data.Text (Text) import qualified Data.Text as T import Data.Text.Encoding (decodeLatin1, encodeUtf8) @@ -90,8 +91,9 @@ import Simplex.FileTransfer.Client.Presets (defaultXFTPServers) import Simplex.FileTransfer.Description (FileDescriptionURI (..), ValidFileDescription) import qualified Simplex.FileTransfer.Description as FD import Simplex.FileTransfer.Protocol (FileParty (..), FilePartyI) +import qualified Simplex.FileTransfer.Transport as XFTP import Simplex.Messaging.Agent as Agent -import Simplex.Messaging.Agent.Client (AgentStatsKey (..), SubInfo (..), agentClientStore, getAgentWorkersDetails, getAgentWorkersSummary, temporaryAgentError, withLockMap) +import Simplex.Messaging.Agent.Client (AgentStatsKey (..), SubInfo (..), agentClientStore, getAgentWorkersDetails, getAgentWorkersSummary, ipAddressProtected, temporaryAgentError, withLockMap) import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..), createAgentStore, defaultAgentConfig) import Simplex.Messaging.Agent.Lock (withLock) import Simplex.Messaging.Agent.Protocol @@ -109,7 +111,7 @@ import qualified Simplex.Messaging.Crypto.Ratchet as CR import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (base64P) -import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..), EntityId, ErrorType (..), MsgBody, MsgFlags (..), NtfServer, ProtoServerWithAuth, ProtocolTypeI, SProtocolType (..), SubscriptionMode (..), UserProtocol, userProtocol) +import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..), EntityId, ErrorType (..), MsgBody, MsgFlags (..), NtfServer, ProtoServerWithAuth (..), ProtocolTypeI, SProtocolType (..), SubscriptionMode (..), UserProtocol, XFTPServer, userProtocol) import qualified Simplex.Messaging.Protocol as SMP import Simplex.Messaging.ServiceScheme (ServiceScheme (..)) import qualified Simplex.Messaging.TMap as TM @@ -427,7 +429,7 @@ startReceiveUserFiles user = do filesToReceive <- withStore' (`getRcvFilesToReceive` user) forM_ filesToReceive $ \ft -> flip catchChatError (toView . CRChatError (Just user)) $ - toView =<< receiveFile' user ft Nothing Nothing + toView =<< receiveFile' user ft False Nothing Nothing restoreCalls :: CM' () restoreCalls = do @@ -2055,17 +2057,17 @@ processChatCommand' vr = \case ForwardFile chatName fileId -> forwardFile chatName fileId SendFile ForwardImage chatName fileId -> forwardFile chatName fileId SendImage SendFileDescription _chatName _f -> pure $ chatCmdError Nothing "TODO" - ReceiveFile fileId encrypted_ rcvInline_ filePath_ -> withUser $ \_ -> + ReceiveFile fileId userApprovedRelays encrypted_ rcvInline_ filePath_ -> withUser $ \_ -> withFileLock "receiveFile" fileId . procCmd $ do (user, ft@RcvFileTransfer {fileStatus}) <- withStore (`getRcvFileTransferById` fileId) encrypt <- (`fromMaybe` encrypted_) <$> chatReadVar encryptLocalFiles ft' <- (if encrypt && fileStatus == RFSNew then setFileToEncrypt else pure) ft - receiveFile' user ft' rcvInline_ filePath_ - SetFileToReceive fileId encrypted_ -> withUser $ \_ -> do + receiveFile' user ft' userApprovedRelays rcvInline_ filePath_ + SetFileToReceive fileId userApprovedRelays encrypted_ -> withUser $ \_ -> do withFileLock "setFileToReceive" fileId . procCmd $ do encrypt <- (`fromMaybe` encrypted_) <$> chatReadVar encryptLocalFiles cfArgs <- if encrypt then Just <$> (atomically . CF.randomArgs =<< asks random) else pure Nothing - withStore' $ \db -> setRcvFileToReceive db fileId cfArgs + withStore' $ \db -> setRcvFileToReceive db fileId userApprovedRelays cfArgs ok_ CancelFile fileId -> withUser $ \user@User {userId} -> withFileLock "cancelFile" fileId . procCmd $ @@ -2105,13 +2107,8 @@ processChatCommand' vr = \case liftIO $ removeFile fsFilePath `catchAll_` pure () lift . forM_ agentRcvFileId $ \(AgentRcvFileId aFileId) -> withAgent' (`xftpDeleteRcvFile` aFileId) - ci <- withStore $ \db -> do - liftIO $ do - updateCIFileStatus db user fileId CIFSRcvInvitation - updateRcvFileStatus db fileId FSNew - updateRcvFileAgentId db fileId Nothing - lookupChatItemByFileId db vr user fileId - pure $ CRRcvFileCancelled user ci ftr + aci_ <- resetRcvCIFileStatus user fileId CIFSRcvInvitation + pure $ CRRcvFileCancelled user aci_ ftr FileStatus fileId -> withUser $ \user -> do withStore (\db -> lookupChatItemByFileId db vr user fileId) >>= \case Nothing -> do @@ -3052,9 +3049,9 @@ setFileToEncrypt ft@RcvFileTransfer {fileId} = do withStore' $ \db -> setFileCryptoArgs db fileId cfArgs pure (ft :: RcvFileTransfer) {cryptoArgs = Just cfArgs} -receiveFile' :: User -> RcvFileTransfer -> Maybe Bool -> Maybe FilePath -> CM ChatResponse -receiveFile' user ft rcvInline_ filePath_ = do - (CRRcvFileAccepted user <$> acceptFileReceive user ft rcvInline_ filePath_) `catchChatError` processError +receiveFile' :: User -> RcvFileTransfer -> Bool -> Maybe Bool -> Maybe FilePath -> CM ChatResponse +receiveFile' user ft userApprovedRelays rcvInline_ filePath_ = do + (CRRcvFileAccepted user <$> acceptFileReceive user ft userApprovedRelays rcvInline_ filePath_) `catchChatError` processError where processError = \case -- TODO AChatItem in Cancelled events @@ -3062,8 +3059,8 @@ receiveFile' user ft rcvInline_ filePath_ = do ChatErrorAgent (CONN DUPLICATE) _ -> pure $ CRRcvFileAcceptedSndCancelled user ft e -> throwError e -acceptFileReceive :: User -> RcvFileTransfer -> Maybe Bool -> Maybe FilePath -> CM AChatItem -acceptFileReceive user@User {userId} RcvFileTransfer {fileId, xftpRcvFile, fileInvitation = FileInvitation {fileName = fName, fileConnReq, fileInline, fileSize}, fileStatus, grpMemberId, cryptoArgs} rcvInline_ filePath_ = do +acceptFileReceive :: User -> RcvFileTransfer -> Bool -> Maybe Bool -> Maybe FilePath -> CM AChatItem +acceptFileReceive user@User {userId} RcvFileTransfer {fileId, xftpRcvFile, fileInvitation = FileInvitation {fileName = fName, fileConnReq, fileInline, fileSize}, fileStatus, grpMemberId, cryptoArgs} userApprovedRelays rcvInline_ filePath_ = do unless (fileStatus == RFSNew) $ case fileStatus of RFSCancelled _ -> throwChatError $ CEFileCancelled fName _ -> throwChatError $ CEFileAlreadyReceiving fName @@ -3077,15 +3074,16 @@ acceptFileReceive user@User {userId} RcvFileTransfer {fileId, xftpRcvFile, fileI filePath <- getRcvFilePath fileId filePath_ fName True withStore $ \db -> acceptRcvFileTransfer db vr user fileId connIds ConnJoined filePath subMode -- XFTP - (Just XFTPRcvFile {}, _) -> do + (Just XFTPRcvFile {userApprovedRelays = approvedBeforeReady}, _) -> do + let userApproved = approvedBeforeReady || userApprovedRelays filePath <- getRcvFilePath fileId filePath_ fName False (ci, rfd) <- withStore $ \db -> do -- marking file as accepted and reading description in the same transaction -- to prevent race condition with appending description - ci <- xftpAcceptRcvFT db vr user fileId filePath + ci <- xftpAcceptRcvFT db vr user fileId filePath userApproved rfd <- getRcvFileDescrByRcvFileId db fileId pure (ci, rfd) - receiveViaCompleteFD user fileId rfd cryptoArgs + receiveViaCompleteFD user fileId rfd userApproved cryptoArgs pure ci -- group & direct file protocol _ -> do @@ -3130,18 +3128,61 @@ acceptFileReceive user@User {userId} RcvFileTransfer {fileId, xftpRcvFile, fileI || (rcvInline_ == Just True && fileSize <= fileChunkSize * offerChunks) ) -receiveViaCompleteFD :: User -> FileTransferId -> RcvFileDescr -> Maybe CryptoFileArgs -> CM () -receiveViaCompleteFD user fileId RcvFileDescr {fileDescrText, fileDescrComplete} cfArgs = +receiveViaCompleteFD :: User -> FileTransferId -> RcvFileDescr -> Bool -> Maybe CryptoFileArgs -> CM () +receiveViaCompleteFD user fileId RcvFileDescr {fileDescrText, fileDescrComplete} userApprovedRelays cfArgs = when fileDescrComplete $ do rd <- parseFileDescription fileDescrText - aFileId <- withAgent $ \a -> xftpReceiveFile a (aUserId user) rd cfArgs - startReceivingFile user fileId - withStore' $ \db -> updateRcvFileAgentId db fileId (Just $ AgentRcvFileId aFileId) + if userApprovedRelays + then receive' rd True + else do + let srvs = fileServers rd + unknownSrvs <- getUnknownSrvs srvs + let approved = null unknownSrvs + ifM + ((approved ||) <$> ipProtectedForSrvs srvs) + (receive' rd approved) + (relaysNotApproved unknownSrvs) + where + receive' :: ValidFileDescription 'FRecipient -> Bool -> CM () + receive' rd approved = do + aFileId <- withAgent $ \a -> xftpReceiveFile a (aUserId user) rd cfArgs approved + startReceivingFile user fileId + withStore' $ \db -> updateRcvFileAgentId db fileId (Just $ AgentRcvFileId aFileId) + fileServers :: ValidFileDescription 'FRecipient -> [XFTPServer] + fileServers (FD.ValidFileDescription FD.FileDescription {chunks}) = + S.toList $ S.fromList $ concatMap (\FD.FileChunk {replicas} -> map (\FD.FileChunkReplica {server} -> server) replicas) chunks + getUnknownSrvs :: [XFTPServer] -> CM [XFTPServer] + getUnknownSrvs srvs = do + ChatConfig {defaultServers = DefaultAgentServers {xftp = defXftp}} <- asks config + storedSrvs <- map (\ServerCfg {server} -> protoServer server) <$> withStore' (`getProtocolServers` user) + let defXftp' = L.map protoServer defXftp + knownSrvs = fromMaybe defXftp' $ nonEmpty storedSrvs + pure $ filter (`notElem` knownSrvs) srvs + ipProtectedForSrvs :: [XFTPServer] -> CM Bool + ipProtectedForSrvs srvs = do + netCfg <- lift $ withAgent' getNetworkConfig + pure $ all (ipAddressProtected netCfg) srvs + relaysNotApproved :: [XFTPServer] -> CM () + relaysNotApproved unknownSrvs = do + aci_ <- resetRcvCIFileStatus user fileId CIFSRcvInvitation + forM_ aci_ $ \aci -> toView $ CRChatItemUpdated user aci + throwChatError $ CEFileNotApproved fileId unknownSrvs + +resetRcvCIFileStatus :: User -> FileTransferId -> CIFileStatus 'MDRcv -> CM (Maybe AChatItem) +resetRcvCIFileStatus user fileId ciFileStatus = do + vr <- chatVersionRange + withStore $ \db -> do + liftIO $ do + updateCIFileStatus db user fileId ciFileStatus + updateRcvFileStatus db fileId FSNew + updateRcvFileAgentId db fileId Nothing + lookupChatItemByFileId db vr user fileId receiveViaURI :: User -> FileDescriptionURI -> CryptoFile -> CM RcvFileTransfer receiveViaURI user@User {userId} FileDescriptionURI {description} cf@CryptoFile {cryptoArgs} = do fileId <- withStore $ \db -> createRcvStandaloneFileTransfer db userId cf fileSize chunkSize - aFileId <- withAgent $ \a -> xftpReceiveFile a (aUserId user) description cryptoArgs + -- currently the only use case is user migrating via their configured servers, so we pass approvedRelays = True + aFileId <- withAgent $ \a -> xftpReceiveFile a (aUserId user) description cryptoArgs True withStore $ \db -> do liftIO $ do updateRcvFileStatus db fileId FSConnected @@ -3811,6 +3852,10 @@ processAgentMsgRcvFile _corrId aFileId msg = do RFERR e | temporaryAgentError e -> throwChatError $ CEXFTPRcvFile fileId (AgentRcvFileId aFileId) e + | e == XFTP "" XFTP.NOT_APPROVED -> do + aci_ <- resetRcvCIFileStatus user fileId CIFSRcvAborted + agentXFTPDeleteRcvFile aFileId fileId + forM_ aci_ $ \aci -> toView $ CRChatItemUpdated user aci | otherwise -> do ci <- withStore $ \db -> do liftIO $ updateFileCancelled db user fileId CIFSRcvError @@ -4862,8 +4907,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = autoAcceptFile :: Maybe (RcvFileTransfer, CIFile 'MDRcv) -> CM () autoAcceptFile = mapM_ $ \(ft, CIFile {fileSize}) -> do + -- ! autoAcceptFileSize is only used in tests ChatConfig {autoAcceptFileSize = sz} <- asks config - when (sz > fileSize) $ receiveFile' user ft Nothing Nothing >>= toView + when (sz > fileSize) $ receiveFile' user ft False Nothing Nothing >>= toView messageFileDescription :: Contact -> SharedMsgId -> FileDescr -> CM () messageFileDescription ct@Contact {contactId} sharedMsgId fileDescr = do @@ -4889,7 +4935,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = ci <- withStore $ \db -> getAChatItemBySharedMsgId db user cd sharedMsgId toView $ CRRcvFileDescrReady user ci ft' rfd case (fileStatus, xftpRcvFile) of - (RFSAccepted _, Just XFTPRcvFile {}) -> receiveViaCompleteFD user fileId rfd cryptoArgs + (RFSAccepted _, Just XFTPRcvFile {userApprovedRelays}) -> receiveViaCompleteFD user fileId rfd userApprovedRelays cryptoArgs _ -> pure () processFileInvitation :: Maybe FileInvitation -> MsgContent -> (DB.Connection -> FileInvitation -> Maybe InlineFileMode -> Integer -> ExceptT StoreError IO RcvFileTransfer) -> CM (Maybe (RcvFileTransfer, CIFile 'MDRcv)) @@ -7315,8 +7361,8 @@ chatCommandP = ("/fforward " <|> "/ff ") *> (ForwardFile <$> chatNameP' <* A.space <*> A.decimal), ("/image_forward " <|> "/imgf ") *> (ForwardImage <$> chatNameP' <* A.space <*> A.decimal), ("/fdescription " <|> "/fd") *> (SendFileDescription <$> chatNameP' <* 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 <*> optional (" encrypt=" *> onOffP)), + ("/freceive " <|> "/fr ") *> (ReceiveFile <$> A.decimal <*> (" approved_relays=" *> onOffP <|> pure False) <*> optional (" encrypt=" *> onOffP) <*> optional (" inline=" *> onOffP) <*> optional (A.space *> filePath)), + "/_set_file_to_receive " *> (SetFileToReceive <$> A.decimal <*> (" approved_relays=" *> onOffP <|> pure False) <*> optional (" encrypt=" *> onOffP)), ("/fcancel " <|> "/fc ") *> (CancelFile <$> A.decimal), ("/fstatus " <|> "/fs ") *> (FileStatus <$> A.decimal), "/_connect contact " *> (APIConnectContactViaAddress <$> A.decimal <*> incognitoOnOffP <* A.space <*> A.decimal), diff --git a/src/Simplex/Chat/AppSettings.hs b/src/Simplex/Chat/AppSettings.hs index 3d63cb2109..2b8b531dc3 100644 --- a/src/Simplex/Chat/AppSettings.hs +++ b/src/Simplex/Chat/AppSettings.hs @@ -29,6 +29,7 @@ data AppSettings = AppSettings { appPlatform :: Maybe AppPlatform, networkConfig :: Maybe NetworkConfig, privacyEncryptLocalFiles :: Maybe Bool, + privacyAskToApproveRelays :: Maybe Bool, privacyAcceptImages :: Maybe Bool, privacyLinkPreviews :: Maybe Bool, privacyShowChatPreviews :: Maybe Bool, @@ -61,6 +62,7 @@ defaultAppSettings = { appPlatform = Nothing, networkConfig = Just defaultNetworkConfig, privacyEncryptLocalFiles = Just True, + privacyAskToApproveRelays = Just True, privacyAcceptImages = Just True, privacyLinkPreviews = Just True, privacyShowChatPreviews = Just True, @@ -92,6 +94,7 @@ defaultParseAppSettings = { appPlatform = Nothing, networkConfig = Nothing, privacyEncryptLocalFiles = Nothing, + privacyAskToApproveRelays = Nothing, privacyAcceptImages = Nothing, privacyLinkPreviews = Nothing, privacyShowChatPreviews = Nothing, @@ -123,6 +126,7 @@ combineAppSettings platformDefaults storedSettings = { appPlatform = p appPlatform, networkConfig = p networkConfig, privacyEncryptLocalFiles = p privacyEncryptLocalFiles, + privacyAskToApproveRelays = p privacyAskToApproveRelays, privacyAcceptImages = p privacyAcceptImages, privacyLinkPreviews = p privacyLinkPreviews, privacyShowChatPreviews = p privacyShowChatPreviews, @@ -166,6 +170,7 @@ instance FromJSON AppSettings where appPlatform <- p "appPlatform" networkConfig <- p "networkConfig" privacyEncryptLocalFiles <- p "privacyEncryptLocalFiles" + privacyAskToApproveRelays <- p "privacyAskToApproveRelays" privacyAcceptImages <- p "privacyAcceptImages" privacyLinkPreviews <- p "privacyLinkPreviews" privacyShowChatPreviews <- p "privacyShowChatPreviews" @@ -194,6 +199,7 @@ instance FromJSON AppSettings where { appPlatform, networkConfig, privacyEncryptLocalFiles, + privacyAskToApproveRelays, privacyAcceptImages, privacyLinkPreviews, privacyShowChatPreviews, diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 267298f188..9ff903514f 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -81,7 +81,7 @@ import Simplex.Messaging.Crypto.Ratchet (PQEncryption) import Simplex.Messaging.Encoding.String import Simplex.Messaging.Notifications.Protocol (DeviceToken (..), NtfTknStatus) import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, parseAll, parseString, sumTypeJSON) -import Simplex.Messaging.Protocol (AProtoServerWithAuth, AProtocolType (..), CorrId, NtfServer, ProtoServerWithAuth, ProtocolTypeI, QueueId, SMPMsgMeta (..), SProtocolType, SubscriptionMode (..), UserProtocol, XFTPServerWithAuth, userProtocol) +import Simplex.Messaging.Protocol (AProtoServerWithAuth, AProtocolType (..), CorrId, NtfServer, ProtoServerWithAuth, ProtocolTypeI, QueueId, SMPMsgMeta (..), SProtocolType, SubscriptionMode (..), UserProtocol, XFTPServer, XFTPServerWithAuth, userProtocol) import Simplex.Messaging.TMap (TMap) import Simplex.Messaging.Transport (TLS, simplexMQVersion) import Simplex.Messaging.Transport.Client (TransportHost) @@ -458,8 +458,8 @@ data ChatCommand | ForwardFile ChatName FileTransferId | ForwardImage ChatName FileTransferId | SendFileDescription ChatName FilePath - | ReceiveFile {fileId :: FileTransferId, storeEncrypted :: Maybe Bool, fileInline :: Maybe Bool, filePath :: Maybe FilePath} - | SetFileToReceive {fileId :: FileTransferId, storeEncrypted :: Maybe Bool} + | ReceiveFile {fileId :: FileTransferId, userApprovedRelays :: Bool, storeEncrypted :: Maybe Bool, fileInline :: Maybe Bool, filePath :: Maybe FilePath} + | SetFileToReceive {fileId :: FileTransferId, userApprovedRelays :: Bool, storeEncrypted :: Maybe Bool} | CancelFile FileTransferId | FileStatus FileTransferId | ShowProfile -- UserId (not used in UI) @@ -1132,6 +1132,7 @@ data ChatErrorType | CEFileImageType {filePath :: FilePath} | CEFileImageSize {filePath :: FilePath} | CEFileNotReceived {fileId :: FileTransferId} + | CEFileNotApproved {fileId :: FileTransferId, unknownServers :: [XFTPServer]} | CEXFTPRcvFile {fileId :: FileTransferId, agentRcvFileId :: AgentRcvFileId, agentError :: AgentErrorType} | CEXFTPSndFile {fileId :: FileTransferId, agentSndFileId :: AgentSndFileId, agentError :: AgentErrorType} | CEFallbackToSMPProhibited {fileId :: FileTransferId} diff --git a/src/Simplex/Chat/Messages.hs b/src/Simplex/Chat/Messages.hs index 83417efa59..449731b91c 100644 --- a/src/Simplex/Chat/Messages.hs +++ b/src/Simplex/Chat/Messages.hs @@ -539,6 +539,7 @@ data CIFileStatus (d :: MsgDirection) where CIFSRcvInvitation :: CIFileStatus 'MDRcv CIFSRcvAccepted :: CIFileStatus 'MDRcv CIFSRcvTransfer :: {rcvProgress :: Int64, rcvTotal :: Int64} -> CIFileStatus 'MDRcv + CIFSRcvAborted :: CIFileStatus 'MDRcv CIFSRcvComplete :: CIFileStatus 'MDRcv CIFSRcvCancelled :: CIFileStatus 'MDRcv CIFSRcvError :: CIFileStatus 'MDRcv @@ -558,6 +559,7 @@ ciFileEnded = \case CIFSRcvInvitation -> False CIFSRcvAccepted -> False CIFSRcvTransfer {} -> False + CIFSRcvAborted -> True CIFSRcvCancelled -> True CIFSRcvComplete -> True CIFSRcvError -> True @@ -573,6 +575,7 @@ ciFileLoaded = \case CIFSRcvInvitation -> False CIFSRcvAccepted -> False CIFSRcvTransfer {} -> False + CIFSRcvAborted -> False CIFSRcvCancelled -> False CIFSRcvComplete -> True CIFSRcvError -> False @@ -592,6 +595,7 @@ instance MsgDirectionI d => StrEncoding (CIFileStatus d) where CIFSRcvInvitation -> "rcv_invitation" CIFSRcvAccepted -> "rcv_accepted" CIFSRcvTransfer rcvd total -> strEncode (Str "rcv_transfer", rcvd, total) + CIFSRcvAborted -> "rcv_aborted" CIFSRcvComplete -> "rcv_complete" CIFSRcvCancelled -> "rcv_cancelled" CIFSRcvError -> "rcv_error" @@ -614,6 +618,7 @@ instance StrEncoding ACIFileStatus where "rcv_invitation" -> pure $ AFS SMDRcv CIFSRcvInvitation "rcv_accepted" -> pure $ AFS SMDRcv CIFSRcvAccepted "rcv_transfer" -> AFS SMDRcv <$> progress CIFSRcvTransfer + "rcv_aborted" -> pure $ AFS SMDRcv CIFSRcvAborted "rcv_complete" -> pure $ AFS SMDRcv CIFSRcvComplete "rcv_cancelled" -> pure $ AFS SMDRcv CIFSRcvCancelled "rcv_error" -> pure $ AFS SMDRcv CIFSRcvError @@ -631,6 +636,7 @@ data JSONCIFileStatus | JCIFSRcvInvitation | JCIFSRcvAccepted | JCIFSRcvTransfer {rcvProgress :: Int64, rcvTotal :: Int64} + | JCIFSRcvAborted | JCIFSRcvComplete | JCIFSRcvCancelled | JCIFSRcvError @@ -646,6 +652,7 @@ jsonCIFileStatus = \case CIFSRcvInvitation -> JCIFSRcvInvitation CIFSRcvAccepted -> JCIFSRcvAccepted CIFSRcvTransfer rcvd total -> JCIFSRcvTransfer rcvd total + CIFSRcvAborted -> JCIFSRcvAborted CIFSRcvComplete -> JCIFSRcvComplete CIFSRcvCancelled -> JCIFSRcvCancelled CIFSRcvError -> JCIFSRcvError @@ -661,6 +668,7 @@ aciFileStatusJSON = \case JCIFSRcvInvitation -> AFS SMDRcv CIFSRcvInvitation JCIFSRcvAccepted -> AFS SMDRcv CIFSRcvAccepted JCIFSRcvTransfer rcvd total -> AFS SMDRcv $ CIFSRcvTransfer rcvd total + JCIFSRcvAborted -> AFS SMDRcv CIFSRcvAborted JCIFSRcvComplete -> AFS SMDRcv CIFSRcvComplete JCIFSRcvCancelled -> AFS SMDRcv CIFSRcvCancelled JCIFSRcvError -> AFS SMDRcv CIFSRcvError diff --git a/src/Simplex/Chat/Migrations/M20240515_rcv_files_user_approved_relays.hs b/src/Simplex/Chat/Migrations/M20240515_rcv_files_user_approved_relays.hs new file mode 100644 index 0000000000..cd4f647685 --- /dev/null +++ b/src/Simplex/Chat/Migrations/M20240515_rcv_files_user_approved_relays.hs @@ -0,0 +1,18 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Migrations.M20240515_rcv_files_user_approved_relays where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20240515_rcv_files_user_approved_relays :: Query +m20240515_rcv_files_user_approved_relays = + [sql| +ALTER TABLE rcv_files ADD COLUMN user_approved_relays INTEGER NOT NULL DEFAULT 0; +|] + +down_m20240515_rcv_files_user_approved_relays :: Query +down_m20240515_rcv_files_user_approved_relays = + [sql| +ALTER TABLE rcv_files DROP COLUMN user_approved_relays; +|] diff --git a/src/Simplex/Chat/Migrations/chat_schema.sql b/src/Simplex/Chat/Migrations/chat_schema.sql index 3ac9b9a98e..96d55badf9 100644 --- a/src/Simplex/Chat/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Migrations/chat_schema.sql @@ -229,7 +229,8 @@ CREATE TABLE rcv_files( REFERENCES xftp_file_descriptions ON DELETE SET NULL, agent_rcv_file_id BLOB NULL, agent_rcv_file_deleted INTEGER DEFAULT 0 CHECK(agent_rcv_file_deleted NOT NULL), - to_receive INTEGER + to_receive INTEGER, + user_approved_relays INTEGER NOT NULL DEFAULT 0 ); CREATE TABLE snd_file_chunks( file_id INTEGER NOT NULL, diff --git a/src/Simplex/Chat/Store/Files.hs b/src/Simplex/Chat/Store/Files.hs index 528290aa59..d70bbb8970 100644 --- a/src/Simplex/Chat/Store/Files.hs +++ b/src/Simplex/Chat/Store/Files.hs @@ -514,7 +514,7 @@ createRcvFileTransfer db userId Contact {contactId, localDisplayName = c} f@File rfd_ <- mapM (createRcvFD_ db userId currentTs) fileDescr let rfdId = (\RcvFileDescr {fileDescrId} -> fileDescrId) <$> rfd_ -- cryptoArgs = Nothing here, the decision to encrypt is made when receiving it - xftpRcvFile = (\rfd -> XFTPRcvFile {rcvFileDescription = rfd, agentRcvFileId = Nothing, agentRcvFileDeleted = False}) <$> rfd_ + xftpRcvFile = (\rfd -> XFTPRcvFile {rcvFileDescription = rfd, agentRcvFileId = Nothing, agentRcvFileDeleted = False, userApprovedRelays = False}) <$> rfd_ fileProtocol = if isJust rfd_ then FPXFTP else FPSMP fileId <- liftIO $ do DB.execute @@ -535,7 +535,7 @@ createRcvGroupFileTransfer db userId GroupMember {groupId, groupMemberId, localD rfd_ <- mapM (createRcvFD_ db userId currentTs) fileDescr let rfdId = (\RcvFileDescr {fileDescrId} -> fileDescrId) <$> rfd_ -- cryptoArgs = Nothing here, the decision to encrypt is made when receiving it - xftpRcvFile = (\rfd -> XFTPRcvFile {rcvFileDescription = rfd, agentRcvFileId = Nothing, agentRcvFileDeleted = False}) <$> rfd_ + xftpRcvFile = (\rfd -> XFTPRcvFile {rcvFileDescription = rfd, agentRcvFileId = Nothing, agentRcvFileDeleted = False, userApprovedRelays = False}) <$> rfd_ fileProtocol = if isJust rfd_ then FPXFTP else FPSMP fileId <- liftIO $ do DB.execute @@ -676,7 +676,9 @@ getRcvFileTransfer_ db userId fileId = do [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, f.file_crypto_key, f.file_crypto_nonce, r.file_inline, r.rcv_file_inline, r.agent_rcv_file_id, r.agent_rcv_file_deleted, c.connection_id, c.agent_conn_id + f.file_path, f.file_crypto_key, f.file_crypto_nonce, r.file_inline, r.rcv_file_inline, + r.agent_rcv_file_id, r.agent_rcv_file_deleted, r.user_approved_relays, + 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 @@ -690,9 +692,9 @@ getRcvFileTransfer_ db userId fileId = do where rcvFileTransfer :: Maybe RcvFileDescr -> - (FileStatus, Maybe ConnReqInvitation, Maybe Int64, String, Integer, Integer, Maybe Bool) :. (Maybe ContactName, Maybe ContactName, Maybe FilePath, Maybe C.SbKey, Maybe C.CbNonce, Maybe InlineFileMode, Maybe InlineFileMode, Maybe AgentRcvFileId, Bool) :. (Maybe Int64, Maybe AgentConnId) -> + (FileStatus, Maybe ConnReqInvitation, Maybe Int64, String, Integer, Integer, Maybe Bool) :. (Maybe ContactName, Maybe ContactName, Maybe FilePath, Maybe C.SbKey, Maybe C.CbNonce, Maybe InlineFileMode, Maybe InlineFileMode, Maybe AgentRcvFileId, Bool, Bool) :. (Maybe Int64, Maybe AgentConnId) -> ExceptT StoreError IO RcvFileTransfer - rcvFileTransfer rfd_ ((fileStatus', fileConnReq, grpMemberId, fileName, fileSize, chunkSize, cancelled_) :. (contactName_, memberName_, filePath_, fileKey, fileNonce, fileInline, rcvFileInline, agentRcvFileId, agentRcvFileDeleted) :. (connId_, agentConnId_)) = + rcvFileTransfer rfd_ ((fileStatus', fileConnReq, grpMemberId, fileName, fileSize, chunkSize, cancelled_) :. (contactName_, memberName_, filePath_, fileKey, fileNonce, fileInline, rcvFileInline, agentRcvFileId, agentRcvFileDeleted, userApprovedRelays) :. (connId_, agentConnId_)) = case contactName_ <|> memberName_ <|> standaloneName_ of Nothing -> throwError $ SERcvFileInvalid fileId Just name -> @@ -709,7 +711,7 @@ getRcvFileTransfer_ db userId fileId = do ft senderDisplayName fileStatus = let fileInvitation = FileInvitation {fileName, fileSize, fileDigest = Nothing, fileConnReq, fileInline, fileDescr = Nothing} cryptoArgs = CFArgs <$> fileKey <*> fileNonce - xftpRcvFile = (\rfd -> XFTPRcvFile {rcvFileDescription = rfd, agentRcvFileId, agentRcvFileDeleted}) <$> rfd_ + xftpRcvFile = (\rfd -> XFTPRcvFile {rcvFileDescription = rfd, agentRcvFileId, agentRcvFileDeleted, userApprovedRelays}) <$> rfd_ in RcvFileTransfer {fileId, xftpRcvFile, fileInvitation, fileStatus, rcvFileInline, senderDisplayName, chunkSize, cancelled, grpMemberId, cryptoArgs} rfi = maybe (throwError $ SERcvFileInvalid fileId) pure =<< rfi_ rfi_ = case (filePath_, connId_, agentConnId_) of @@ -720,7 +722,7 @@ getRcvFileTransfer_ db userId fileId = do acceptRcvFileTransfer :: DB.Connection -> VersionRangeChat -> User -> Int64 -> (CommandId, ConnId) -> ConnStatus -> FilePath -> SubscriptionMode -> ExceptT StoreError IO AChatItem acceptRcvFileTransfer db vr user@User {userId} fileId (cmdId, acId) connStatus filePath subMode = ExceptT $ do currentTs <- getCurrentTime - acceptRcvFT_ db user fileId filePath Nothing currentTs + acceptRcvFT_ db user fileId filePath False Nothing currentTs DB.execute db "INSERT INTO connections (agent_conn_id, conn_status, conn_type, rcv_file_id, user_id, created_at, updated_at, to_subscribe) VALUES (?,?,?,?,?,?,?,?)" @@ -740,33 +742,40 @@ getContactByFileId db vr user@User {userId} fileId = do acceptRcvInlineFT :: DB.Connection -> VersionRangeChat -> User -> FileTransferId -> FilePath -> ExceptT StoreError IO AChatItem acceptRcvInlineFT db vr user fileId filePath = do - liftIO $ acceptRcvFT_ db user fileId filePath (Just IFMOffer) =<< getCurrentTime + liftIO $ acceptRcvFT_ db user fileId filePath False (Just IFMOffer) =<< getCurrentTime getChatItemByFileId db vr user fileId startRcvInlineFT :: DB.Connection -> User -> RcvFileTransfer -> FilePath -> Maybe InlineFileMode -> IO () startRcvInlineFT db user RcvFileTransfer {fileId} filePath rcvFileInline = - acceptRcvFT_ db user fileId filePath rcvFileInline =<< getCurrentTime + acceptRcvFT_ db user fileId filePath False rcvFileInline =<< getCurrentTime -xftpAcceptRcvFT :: DB.Connection -> VersionRangeChat -> User -> FileTransferId -> FilePath -> ExceptT StoreError IO AChatItem -xftpAcceptRcvFT db vr user fileId filePath = do - liftIO $ acceptRcvFT_ db user fileId filePath Nothing =<< getCurrentTime +xftpAcceptRcvFT :: DB.Connection -> VersionRangeChat -> User -> FileTransferId -> FilePath -> Bool -> ExceptT StoreError IO AChatItem +xftpAcceptRcvFT db vr user fileId filePath userApprovedRelays = do + liftIO $ acceptRcvFT_ db user fileId filePath userApprovedRelays Nothing =<< getCurrentTime getChatItemByFileId db vr user fileId -acceptRcvFT_ :: DB.Connection -> User -> FileTransferId -> FilePath -> Maybe InlineFileMode -> UTCTime -> IO () -acceptRcvFT_ db User {userId} fileId filePath rcvFileInline currentTs = do +acceptRcvFT_ :: DB.Connection -> User -> FileTransferId -> FilePath -> Bool -> Maybe InlineFileMode -> UTCTime -> IO () +acceptRcvFT_ db User {userId} fileId filePath userApprovedRelays rcvFileInline currentTs = do DB.execute db "UPDATE files SET file_path = ?, ci_file_status = ?, updated_at = ? WHERE user_id = ? AND file_id = ?" (filePath, CIFSRcvAccepted, currentTs, userId, fileId) DB.execute db - "UPDATE rcv_files SET rcv_file_inline = ?, file_status = ?, updated_at = ? WHERE file_id = ?" - (rcvFileInline, FSAccepted, currentTs, fileId) + "UPDATE rcv_files SET user_approved_relays = ?, rcv_file_inline = ?, file_status = ?, updated_at = ? WHERE file_id = ?" + (userApprovedRelays, rcvFileInline, FSAccepted, currentTs, fileId) -setRcvFileToReceive :: DB.Connection -> FileTransferId -> Maybe CryptoFileArgs -> IO () -setRcvFileToReceive db fileId cfArgs_ = do +setRcvFileToReceive :: DB.Connection -> FileTransferId -> Bool -> Maybe CryptoFileArgs -> IO () +setRcvFileToReceive db fileId userApprovedRelays cfArgs_ = do currentTs <- getCurrentTime - DB.execute db "UPDATE rcv_files SET to_receive = 1, updated_at = ? WHERE file_id = ?" (currentTs, fileId) + DB.execute + db + [sql| + UPDATE rcv_files + SET to_receive = 1, user_approved_relays = ?, updated_at = ? + WHERE file_id = ? + |] + (userApprovedRelays, currentTs, fileId) forM_ cfArgs_ $ \cfArgs -> setFileCryptoArgs_ db fileId cfArgs currentTs setFileCryptoArgs :: DB.Connection -> FileTransferId -> CryptoFileArgs -> IO () @@ -950,7 +959,7 @@ getFileTransferMeta_ db userId fileId = fileTransferMeta (fileName, fileSize, chunkSize, filePath, fileKey, fileNonce, fileInline, aSndFileId_, agentSndFileDeleted, privateSndFileDescr, cancelled_, xftpRedirectFor) = let cryptoArgs = CFArgs <$> fileKey <*> fileNonce xftpSndFile = (\fId -> XFTPSndFile {agentSndFileId = fId, privateSndFileDescr, agentSndFileDeleted, cryptoArgs}) <$> aSndFileId_ - in FileTransferMeta {fileId, xftpSndFile, xftpRedirectFor, fileName, fileSize, chunkSize, filePath, fileInline, cancelled = fromMaybe False cancelled_} + in FileTransferMeta {fileId, xftpSndFile, xftpRedirectFor, fileName, fileSize, chunkSize, filePath, fileInline, cancelled = fromMaybe False cancelled_} lookupFileTransferRedirectMeta :: DB.Connection -> User -> Int64 -> IO [FileTransferMeta] lookupFileTransferRedirectMeta db User {userId} fileId = do diff --git a/src/Simplex/Chat/Store/Migrations.hs b/src/Simplex/Chat/Store/Migrations.hs index ccc69d100e..a79a31f75d 100644 --- a/src/Simplex/Chat/Store/Migrations.hs +++ b/src/Simplex/Chat/Store/Migrations.hs @@ -108,6 +108,7 @@ import Simplex.Chat.Migrations.M20240402_item_forwarded import Simplex.Chat.Migrations.M20240430_ui_theme import Simplex.Chat.Migrations.M20240501_chat_deleted import Simplex.Chat.Migrations.M20240510_chat_items_via_proxy +import Simplex.Chat.Migrations.M20240515_rcv_files_user_approved_relays import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..)) schemaMigrations :: [(String, Query, Maybe Query)] @@ -215,7 +216,8 @@ schemaMigrations = ("20240402_item_forwarded", m20240402_item_forwarded, Just down_m20240402_item_forwarded), ("20240430_ui_theme", m20240430_ui_theme, Just down_m20240430_ui_theme), ("20240501_chat_deleted", m20240501_chat_deleted, Just down_m20240501_chat_deleted), - ("20240510_chat_items_via_proxy", m20240510_chat_items_via_proxy, Just down_m20240510_chat_items_via_proxy) + ("20240510_chat_items_via_proxy", m20240510_chat_items_via_proxy, Just down_m20240510_chat_items_via_proxy), + ("20240515_rcv_files_user_approved_relays", m20240515_rcv_files_user_approved_relays, Just down_m20240515_rcv_files_user_approved_relays) ] -- | The list of migrations in ascending order by date diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index 336448d7f5..ca6cd2e375 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -1072,7 +1072,8 @@ data RcvFileTransfer = RcvFileTransfer data XFTPRcvFile = XFTPRcvFile { rcvFileDescription :: RcvFileDescr, agentRcvFileId :: Maybe AgentRcvFileId, - agentRcvFileDeleted :: Bool + agentRcvFileDeleted :: Bool, + userApprovedRelays :: Bool } deriving (Eq, Show) diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 4b3240fe46..1c2b0b2cc1 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -1766,6 +1766,7 @@ viewFileTransferStatusXFTP (AChatItem _ _ _ ChatItem {file = Just CIFile {fileId CIFSRcvInvitation -> ["receiving " <> fstr <> " not accepted yet, use " <> highlight ("/fr " <> show fileId) <> " to receive file"] CIFSRcvAccepted -> ["receiving " <> fstr <> " just started"] CIFSRcvTransfer progress total -> ["receiving " <> fstr <> " progress " <> fileProgressXFTP progress total fileSize] + CIFSRcvAborted -> ["receiving " <> fstr <> " aborted, use " <> highlight ("/fr " <> show fileId) <> " to receive file"] CIFSRcvComplete -> ["receiving " <> fstr <> " complete" <> maybe "" (\(CryptoFile fp _) -> ", path: " <> plain fp) fileSource] CIFSRcvCancelled -> ["receiving " <> fstr <> " cancelled"] CIFSRcvError -> ["receiving " <> fstr <> " error"] @@ -1969,6 +1970,7 @@ viewChatError logLevel testView = \case CEFileImageType _ -> ["image type must be jpg, send as a file using " <> highlight' "/f"] CEFileImageSize _ -> ["max image size: " <> sShow maxImageSize <> " bytes, resize it or send as a file using " <> highlight' "/f"] CEFileNotReceived fileId -> ["file " <> sShow fileId <> " not received"] + CEFileNotApproved fileId unknownSrvs -> ["file " <> sShow fileId <> " aborted, unknwon XFTP servers:"] <> map (plain . show) unknownSrvs CEXFTPRcvFile fileId aFileId e -> ["error receiving XFTP file " <> sShow fileId <> ", agent file id " <> sShow aFileId <> ": " <> sShow e | logLevel == CLLError] CEXFTPSndFile fileId aFileId e -> ["error sending XFTP file " <> sShow fileId <> ", agent file id " <> sShow aFileId <> ": " <> sShow e | logLevel == CLLError] CEFallbackToSMPProhibited fileId -> ["recipient tried to accept file " <> sShow fileId <> " via old protocol, prohibited"] From ec7b35adb90f9a15ffb9ac9a62a660775d622821 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Mon, 20 May 2024 17:49:19 +0400 Subject: [PATCH 2/3] ios: check known relays before file reception, support user approval of unknown relays (#4044) * ios: check known relays before file reception, support user approval of unknown relays * add privacyAskToApproveRelays to AppSettings * less text * ui improvements, fixes * fix ntf * shorter alert * simplify settings * rename * icon --------- Co-authored-by: Evgeny Poberezkin --- apps/ios/Shared/Model/SimpleXAPI.swift | 54 +++++++++++++++---- apps/ios/Shared/Views/Chat/ChatInfoView.swift | 2 +- .../Views/Chat/ChatItem/CIFileView.swift | 7 ++- .../Views/Chat/ChatItem/CIImageView.swift | 6 +-- .../Views/Chat/ChatItem/CIVideoView.swift | 17 ++++-- .../Views/Chat/ChatItem/CIVoiceView.swift | 7 +-- .../Views/Chat/ChatItem/FramedItemView.swift | 2 +- .../Views/UserSettings/AppSettings.swift | 2 + .../Views/UserSettings/PrivacySettings.swift | 39 +++++++++----- .../ios/SimpleX NSE/NotificationService.swift | 6 ++- apps/ios/SimpleXChat/APITypes.swift | 12 +++-- apps/ios/SimpleXChat/AppGroup.swift | 4 ++ apps/ios/SimpleXChat/ChatTypes.swift | 4 ++ 13 files changed, 121 insertions(+), 41 deletions(-) diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index ef31d2f438..a6d574e38d 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -927,14 +927,19 @@ func standaloneFileInfo(url: String, ctrl: chat_ctrl? = nil) async -> MigrationF } } -func receiveFile(user: any UserLike, fileId: Int64, auto: Bool = false) async { - if let chatItem = await apiReceiveFile(fileId: fileId, encrypted: privacyEncryptLocalFilesGroupDefault.get(), auto: auto) { +func receiveFile(user: any UserLike, fileId: Int64, userApprovedRelays: Bool = false, auto: Bool = false) async { + if let chatItem = await apiReceiveFile( + fileId: fileId, + userApprovedRelays: userApprovedRelays || !privacyAskToApproveRelaysGroupDefault.get(), + encrypted: privacyEncryptLocalFilesGroupDefault.get(), + auto: auto + ) { await chatItemSimpleUpdate(user, chatItem) } } -func apiReceiveFile(fileId: Int64, encrypted: Bool, inline: Bool? = nil, auto: Bool = false) async -> AChatItem? { - let r = await chatSendCmd(.receiveFile(fileId: fileId, encrypted: encrypted, inline: inline)) +func apiReceiveFile(fileId: Int64, userApprovedRelays: Bool, encrypted: Bool, inline: Bool? = nil, auto: Bool = false) async -> AChatItem? { + let r = await chatSendCmd(.receiveFile(fileId: fileId, userApprovedRelays: userApprovedRelays, encrypted: encrypted, inline: inline)) let am = AlertManager.shared if case let .rcvFileAccepted(_, chatItem) = r { return chatItem } if case .rcvFileAcceptedSndCancelled = r { @@ -947,19 +952,50 @@ func apiReceiveFile(fileId: Int64, encrypted: Bool, inline: Bool? = nil, auto: B } } else if let networkErrorAlert = networkErrorAlert(r) { logger.error("apiReceiveFile network error: \(String(describing: r))") - am.showAlert(networkErrorAlert) + if !auto { + am.showAlert(networkErrorAlert) + } } else { switch chatError(r) { case .fileCancelled: logger.debug("apiReceiveFile ignoring fileCancelled error") case .fileAlreadyReceiving: logger.debug("apiReceiveFile ignoring fileAlreadyReceiving error") + case let .fileNotApproved(fileId, unknownServers): + logger.debug("apiReceiveFile fileNotApproved error") + if !auto { + let srvs = unknownServers.map { s in + if let srv = parseServerAddress(s), !srv.hostnames.isEmpty { + srv.hostnames[0] + } else { + serverHost(s) + } + } + am.showAlert(Alert( + title: Text("Unknown servers!"), + message: Text("Without Tor or VPN, your IP address will be visible to these XFTP relays: \(srvs.sorted().joined(separator: ", "))."), + primaryButton: .default( + Text("Download"), + action: { + Task { + logger.debug("apiReceiveFile fileNotApproved alert - in Task") + if let user = ChatModel.shared.currentUser { + await receiveFile(user: user, fileId: fileId, userApprovedRelays: true) + } + } + } + ), + secondaryButton: .cancel() + )) + } default: logger.error("apiReceiveFile error: \(String(describing: r))") - am.showAlertMsg( - title: "Error receiving file", - message: "Error: \(String(describing: r))" - ) + if !auto { + am.showAlertMsg( + title: "Error receiving file", + message: "Error: \(String(describing: r))" + ) + } } } return nil diff --git a/apps/ios/Shared/Views/Chat/ChatInfoView.swift b/apps/ios/Shared/Views/Chat/ChatInfoView.swift index 55e84f20d3..2e5a4f2af6 100644 --- a/apps/ios/Shared/Views/Chat/ChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatInfoView.swift @@ -49,7 +49,7 @@ func localizedInfoRow(_ title: LocalizedStringKey, _ value: LocalizedStringKey) } } -private func serverHost(_ s: String) -> String { +func serverHost(_ s: String) -> String { if let i = s.range(of: "@")?.lowerBound { return String(s[i...].dropFirst()) } else { diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift index ae9e09b138..0af0469e42 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift @@ -60,6 +60,7 @@ struct CIFileView: View { case .rcvInvitation: return true case .rcvAccepted: return true case .rcvTransfer: return false + case .rcvAborted: return true case .rcvComplete: return true case .rcvCancelled: return false case .rcvError: return false @@ -73,10 +74,10 @@ struct CIFileView: View { logger.debug("CIFileView fileAction") if let file = file { switch (file.fileStatus) { - case .rcvInvitation: + case .rcvInvitation, .rcvAborted: if fileSizeValid(file) { Task { - logger.debug("CIFileView fileAction - in .rcvInvitation, in Task") + logger.debug("CIFileView fileAction - in .rcvInvitation, .rcvAborted, in Task") if let user = m.currentUser { await receiveFile(user: user, fileId: file.fileId) } @@ -148,6 +149,8 @@ struct CIFileView: View { } else { progressView() } + case .rcvAborted: + fileIcon("doc.fill", color: .accentColor, innerIcon: "exclamationmark.arrow.circlepath", innerIconSize: 12) case .rcvComplete: fileIcon("doc.fill") case .rcvCancelled: fileIcon("doc.fill", innerIcon: "xmark", innerIconSize: 10) case .rcvError: fileIcon("doc.fill", innerIcon: "xmark", innerIconSize: 10) diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift index 16974147c8..7d33df6c60 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift @@ -17,7 +17,6 @@ struct CIImageView: View { let maxWidth: CGFloat @Binding var imgWidth: CGFloat? @State var scrollProxy: ScrollViewProxy? - @State var metaColor: Color @State private var showFullScreenImage = false var body: some View { @@ -38,7 +37,7 @@ struct CIImageView: View { .onTapGesture { if let file = file { switch file.fileStatus { - case .rcvInvitation: + case .rcvInvitation, .rcvAborted: Task { if let user = m.currentUser { await receiveFile(user: user, fileId: file.fileId) @@ -103,6 +102,7 @@ struct CIImageView: View { case .rcvInvitation: fileIcon("arrow.down", 10, 13) case .rcvAccepted: fileIcon("ellipsis", 14, 11) case .rcvTransfer: progressView() + case .rcvAborted: fileIcon("exclamationmark.arrow.circlepath", 14, 11) case .rcvCancelled: fileIcon("xmark", 10, 13) case .rcvError: fileIcon("xmark", 10, 13) case .invalid: fileIcon("questionmark", 10, 13) @@ -116,7 +116,7 @@ struct CIImageView: View { .resizable() .aspectRatio(contentMode: .fit) .frame(width: size, height: size) - .foregroundColor(metaColor) + .foregroundColor(.white) .padding(padding) } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift index b4b190a43a..3bfe24b79d 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift @@ -69,7 +69,7 @@ struct CIVideoView: View { .onTapGesture { if let file = file { switch file.fileStatus { - case .rcvInvitation: + case .rcvInvitation, .rcvAborted: receiveFileIfValidSize(file: file, receiveFile: receiveFile) case .rcvAccepted: switch file.fileProtocol { @@ -95,7 +95,7 @@ struct CIVideoView: View { } durationProgress() } - if let file = file, case .rcvInvitation = file.fileStatus { + if let file = file, showDownloadButton(file.fileStatus) { Button { receiveFileIfValidSize(file: file, receiveFile: receiveFile) } label: { @@ -105,6 +105,14 @@ struct CIVideoView: View { } } + private func showDownloadButton(_ fileStatus: CIFileStatus) -> Bool { + switch fileStatus { + case .rcvInvitation: true + case .rcvAborted: true + default: false + } + } + private func videoViewEncrypted(_ file: CIFile, _ defaultPreview: UIImage, _ duration: Int) -> some View { return ZStack(alignment: .topTrailing) { ZStack(alignment: .center) { @@ -280,6 +288,7 @@ struct CIVideoView: View { } else { progressView() } + case .rcvAborted: fileIcon("exclamationmark.arrow.circlepath", 14, 11) case .rcvCancelled: fileIcon("xmark", 10, 13) case .rcvError: fileIcon("xmark", 10, 13) case .invalid: fileIcon("questionmark", 10, 13) @@ -318,10 +327,10 @@ struct CIVideoView: View { } // TODO encrypt: where file size is checked? - private func receiveFileIfValidSize(file: CIFile, receiveFile: @escaping (User, Int64, Bool) async -> Void) { + private func receiveFileIfValidSize(file: CIFile, receiveFile: @escaping (User, Int64, Bool, Bool) async -> Void) { Task { if let user = m.currentUser { - await receiveFile(user, file.fileId, false) + await receiveFile(user, file.fileId, false, false) } } } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift index 3aecb65ebd..ba1712f6e3 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift @@ -139,9 +139,10 @@ struct VoiceMessagePlayer: View { case .sndComplete: playbackButton() case .sndCancelled: playbackButton() case .sndError: playbackButton() - case .rcvInvitation: downloadButton(recordingFile) + case .rcvInvitation: downloadButton(recordingFile, "play.fill") case .rcvAccepted: loadingIcon() case .rcvTransfer: loadingIcon() + case .rcvAborted: downloadButton(recordingFile, "exclamationmark.arrow.circlepath") case .rcvComplete: playbackButton() case .rcvCancelled: playPauseIcon("play.fill", Color(uiColor: .tertiaryLabel)) case .rcvError: playPauseIcon("play.fill", Color(uiColor: .tertiaryLabel)) @@ -217,7 +218,7 @@ struct VoiceMessagePlayer: View { } } - private func downloadButton(_ recordingFile: CIFile) -> some View { + private func downloadButton(_ recordingFile: CIFile, _ icon: String) -> some View { Button { Task { if let user = chatModel.currentUser { @@ -225,7 +226,7 @@ struct VoiceMessagePlayer: View { } } } label: { - playPauseIcon("play.fill") + playPauseIcon(icon) } } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift index a1642769b3..9b4cecf526 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift @@ -115,7 +115,7 @@ struct FramedItemView: View { } else { switch (chatItem.content.msgContent) { case let .image(text, image): - CIImageView(chatItem: chatItem, image: image, maxWidth: maxWidth, imgWidth: $imgWidth, scrollProxy: scrollProxy, metaColor: metaColor) + CIImageView(chatItem: chatItem, image: image, maxWidth: maxWidth, imgWidth: $imgWidth, scrollProxy: scrollProxy) .overlay(DetermineWidth()) if text == "" && !chatItem.meta.isLive { Color.clear diff --git a/apps/ios/Shared/Views/UserSettings/AppSettings.swift b/apps/ios/Shared/Views/UserSettings/AppSettings.swift index ba192b333c..299c96626a 100644 --- a/apps/ios/Shared/Views/UserSettings/AppSettings.swift +++ b/apps/ios/Shared/Views/UserSettings/AppSettings.swift @@ -23,6 +23,7 @@ extension AppSettings { setNetCfg(val) } if let val = privacyEncryptLocalFiles { privacyEncryptLocalFilesGroupDefault.set(val) } + if let val = privacyAskToApproveRelays { privacyAskToApproveRelaysGroupDefault.set(val) } if let val = privacyAcceptImages { privacyAcceptImagesGroupDefault.set(val) def.setValue(val, forKey: DEFAULT_PRIVACY_ACCEPT_IMAGES) @@ -50,6 +51,7 @@ extension AppSettings { var c = AppSettings.defaults c.networkConfig = getNetCfg() c.privacyEncryptLocalFiles = privacyEncryptLocalFilesGroupDefault.get() + c.privacyAskToApproveRelays = privacyAskToApproveRelaysGroupDefault.get() c.privacyAcceptImages = privacyAcceptImagesGroupDefault.get() c.privacyLinkPreviews = def.bool(forKey: DEFAULT_PRIVACY_LINK_PREVIEWS) c.privacyShowChatPreviews = def.bool(forKey: DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS) diff --git a/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift b/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift index 8d13c6fb39..01f31d66b4 100644 --- a/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift +++ b/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift @@ -16,6 +16,7 @@ struct PrivacySettings: View { @AppStorage(DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS) private var showChatPreviews = true @AppStorage(DEFAULT_PRIVACY_SAVE_LAST_DRAFT) private var saveLastDraft = true @AppStorage(GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES, store: groupDefaults) private var encryptLocalFiles = true + @AppStorage(GROUP_DEFAULT_PRIVACY_ASK_TO_APPROVE_RELAYS, store: groupDefaults) private var askToApproveRelays = true @State private var simplexLinkMode = privacySimplexLinkModeDefault.get() @AppStorage(DEFAULT_PRIVACY_PROTECT_SCREEN) private var protectScreen = false @AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false @@ -64,18 +65,6 @@ struct PrivacySettings: View { } Section { - settingsRow("lock.doc") { - Toggle("Encrypt local files", isOn: $encryptLocalFiles) - .onChange(of: encryptLocalFiles) { - setEncryptLocalFiles($0) - } - } - settingsRow("photo") { - Toggle("Auto-accept images", isOn: $autoAcceptImages) - .onChange(of: autoAcceptImages) { - privacyAcceptImagesGroupDefault.set($0) - } - } settingsRow("network") { Toggle("Send link previews", isOn: $useLinkPreviews) } @@ -108,6 +97,32 @@ struct PrivacySettings: View { Text("Chats") } + Section { + settingsRow("lock.doc") { + Toggle("Encrypt local files", isOn: $encryptLocalFiles) + .onChange(of: encryptLocalFiles) { + setEncryptLocalFiles($0) + } + } + settingsRow("photo") { + Toggle("Auto-accept images", isOn: $autoAcceptImages) + .onChange(of: autoAcceptImages) { + privacyAcceptImagesGroupDefault.set($0) + } + } + settingsRow("network.badge.shield.half.filled") { + Toggle("Protect IP address", isOn: $askToApproveRelays) + } + } header: { + Text("Files") + } footer: { + if askToApproveRelays { + Text("The app will ask to confirm downloads from unknown file servers (except .onion).") + } else { + Text("Without Tor or VPN, your IP address will be visible to file servers.") + } + } + Section { settingsRow("person") { Toggle("Contacts", isOn: $contactReceipts) diff --git a/apps/ios/SimpleX NSE/NotificationService.swift b/apps/ios/SimpleX NSE/NotificationService.swift index faa7f4f44c..12094c7053 100644 --- a/apps/ios/SimpleX NSE/NotificationService.swift +++ b/apps/ios/SimpleX NSE/NotificationService.swift @@ -696,14 +696,16 @@ func apiGetNtfMessage(nonce: String, encNtfInfo: String) -> NtfMessages? { } func apiReceiveFile(fileId: Int64, encrypted: Bool, inline: Bool? = nil) -> AChatItem? { - let r = sendSimpleXCmd(.receiveFile(fileId: fileId, encrypted: encrypted, inline: inline)) + let userApprovedRelays = !privacyAskToApproveRelaysGroupDefault.get() + let r = sendSimpleXCmd(.receiveFile(fileId: fileId, userApprovedRelays: userApprovedRelays, encrypted: encrypted, inline: inline)) if case let .rcvFileAccepted(_, chatItem) = r { return chatItem } logger.error("receiveFile error: \(responseError(r))") return nil } func apiSetFileToReceive(fileId: Int64, encrypted: Bool) { - let r = sendSimpleXCmd(.setFileToReceive(fileId: fileId, encrypted: encrypted)) + let userApprovedRelays = !privacyAskToApproveRelaysGroupDefault.get() + let r = sendSimpleXCmd(.setFileToReceive(fileId: fileId, userApprovedRelays: userApprovedRelays, encrypted: encrypted)) if case .cmdOk = r { return } logger.error("setFileToReceive error: \(responseError(r))") } diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 97013ca2a4..7fa7e961ae 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -123,8 +123,8 @@ public enum ChatCommand { case apiGetNetworkStatuses case apiChatRead(type: ChatType, id: Int64, itemRange: (Int64, Int64)) case apiChatUnread(type: ChatType, id: Int64, unreadChat: Bool) - case receiveFile(fileId: Int64, encrypted: Bool?, inline: Bool?) - case setFileToReceive(fileId: Int64, encrypted: Bool?) + case receiveFile(fileId: Int64, userApprovedRelays: Bool, encrypted: Bool?, inline: Bool?) + case setFileToReceive(fileId: Int64, userApprovedRelays: Bool, encrypted: Bool?) case cancelFile(fileId: Int64) // remote desktop commands case setLocalDeviceName(displayName: String) @@ -282,8 +282,8 @@ public enum ChatCommand { 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 .apiChatUnread(type, id, unreadChat): return "/_unread chat \(ref(type, id)) \(onOff(unreadChat))" - case let .receiveFile(fileId, encrypt, inline): return "/freceive \(fileId)\(onOffParam("encrypt", encrypt))\(onOffParam("inline", inline))" - case let .setFileToReceive(fileId, encrypt): return "/_set_file_to_receive \(fileId)\(onOffParam("encrypt", encrypt))" + case let .receiveFile(fileId, userApprovedRelays, encrypt, inline): return "/freceive \(fileId)\(onOffParam("approved_relays", userApprovedRelays))\(onOffParam("encrypt", encrypt))\(onOffParam("inline", inline))" + case let .setFileToReceive(fileId, userApprovedRelays, encrypt): return "/_set_file_to_receive \(fileId)\(onOffParam("approved_relays", userApprovedRelays))\(onOffParam("encrypt", encrypt))" case let .cancelFile(fileId): return "/fcancel \(fileId)" case let .setLocalDeviceName(displayName): return "/set device name \(displayName)" case let .connectRemoteCtrl(xrcpInv): return "/connect remote ctrl \(xrcpInv)" @@ -1760,6 +1760,7 @@ public enum ChatErrorType: Decodable { case fileImageType(filePath: String) case fileImageSize(filePath: String) case fileNotReceived(fileId: Int64) + case fileNotApproved(fileId: Int64, unknownServers: [String]) // case xFTPRcvFile // case xFTPSndFile case fallbackToSMPProhibited(fileId: Int64) @@ -2038,6 +2039,7 @@ public struct MigrationFileLinkData: Codable { public struct AppSettings: Codable, Equatable { public var networkConfig: NetCfg? = nil public var privacyEncryptLocalFiles: Bool? = nil + public var privacyAskToApproveRelays: Bool? = nil public var privacyAcceptImages: Bool? = nil public var privacyLinkPreviews: Bool? = nil public var privacyShowChatPreviews: Bool? = nil @@ -2061,6 +2063,7 @@ public struct AppSettings: Codable, Equatable { let def = AppSettings.defaults if networkConfig != def.networkConfig { empty.networkConfig = networkConfig } if privacyEncryptLocalFiles != def.privacyEncryptLocalFiles { empty.privacyEncryptLocalFiles = privacyEncryptLocalFiles } + if privacyAskToApproveRelays != def.privacyAskToApproveRelays { empty.privacyAskToApproveRelays = privacyAskToApproveRelays } if privacyAcceptImages != def.privacyAcceptImages { empty.privacyAcceptImages = privacyAcceptImages } if privacyLinkPreviews != def.privacyLinkPreviews { empty.privacyLinkPreviews = privacyLinkPreviews } if privacyShowChatPreviews != def.privacyShowChatPreviews { empty.privacyShowChatPreviews = privacyShowChatPreviews } @@ -2085,6 +2088,7 @@ public struct AppSettings: Codable, Equatable { AppSettings ( networkConfig: NetCfg.defaults, privacyEncryptLocalFiles: true, + privacyAskToApproveRelays: true, privacyAcceptImages: true, privacyLinkPreviews: true, privacyShowChatPreviews: true, diff --git a/apps/ios/SimpleXChat/AppGroup.swift b/apps/ios/SimpleXChat/AppGroup.swift index 118acae993..90ac403999 100644 --- a/apps/ios/SimpleXChat/AppGroup.swift +++ b/apps/ios/SimpleXChat/AppGroup.swift @@ -23,6 +23,7 @@ public let GROUP_DEFAULT_NTF_ENABLE_PERIODIC = "ntfEnablePeriodic" // no longer let GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES = "privacyAcceptImages" public let GROUP_DEFAULT_PRIVACY_TRANSFER_IMAGES_INLINE = "privacyTransferImagesInline" // no longer used public let GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES = "privacyEncryptLocalFiles" +public let GROUP_DEFAULT_PRIVACY_ASK_TO_APPROVE_RELAYS = "privacyAskToApproveRelays" let GROUP_DEFAULT_NTF_BADGE_COUNT = "ntgBadgeCount" let GROUP_DEFAULT_NETWORK_USE_ONION_HOSTS = "networkUseOnionHosts" let GROUP_DEFAULT_NETWORK_SESSION_MODE = "networkSessionMode" @@ -73,6 +74,7 @@ public func registerGroupDefaults() { GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES: true, GROUP_DEFAULT_PRIVACY_TRANSFER_IMAGES_INLINE: false, GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES: true, + GROUP_DEFAULT_PRIVACY_ASK_TO_APPROVE_RELAYS: true, GROUP_DEFAULT_CONFIRM_DB_UPGRADES: false, GROUP_DEFAULT_CALL_KIT_ENABLED: true, GROUP_DEFAULT_PQ_EXPERIMENTAL_ENABLED: false, @@ -181,6 +183,8 @@ public let privacyAcceptImagesGroupDefault = BoolDefault(defaults: groupDefaults public let privacyEncryptLocalFilesGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES) +public let privacyAskToApproveRelaysGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PRIVACY_ASK_TO_APPROVE_RELAYS) + public let ntfBadgeCountGroupDefault = IntDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_NTF_BADGE_COUNT) public let networkUseOnionHostsGroupDefault = EnumDefault( diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 0d2a042d9d..27e5a9818c 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -3174,6 +3174,7 @@ public struct CIFile: Decodable { case .rcvInvitation: return false case .rcvAccepted: return false case .rcvTransfer: return false + case .rcvAborted: return false case .rcvCancelled: return false case .rcvComplete: return true case .rcvError: return false @@ -3198,6 +3199,7 @@ public struct CIFile: Decodable { case .rcvInvitation: return nil case .rcvAccepted: return rcvCancelAction case .rcvTransfer: return rcvCancelAction + case .rcvAborted: return nil case .rcvCancelled: return nil case .rcvComplete: return nil case .rcvError: return nil @@ -3312,6 +3314,7 @@ public enum CIFileStatus: Decodable, Equatable { case rcvInvitation case rcvAccepted case rcvTransfer(rcvProgress: Int64, rcvTotal: Int64) + case rcvAborted case rcvComplete case rcvCancelled case rcvError @@ -3327,6 +3330,7 @@ public enum CIFileStatus: Decodable, Equatable { case .rcvInvitation: return "rcvInvitation" case .rcvAccepted: return "rcvAccepted" case let .rcvTransfer(rcvProgress, rcvTotal): return "rcvTransfer \(rcvProgress) \(rcvTotal)" + case .rcvAborted: return "rcvAborted" case .rcvComplete: return "rcvComplete" case .rcvCancelled: return "rcvCancelled" case .rcvError: return "rcvError" From ba203faad42a8da4bb9ba8fd8217eec428243000 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Mon, 20 May 2024 17:58:30 +0400 Subject: [PATCH 3/3] android: check known relays before file reception, support user approval of unknown relays (#4196) --- .../chat/simplex/common/model/ChatModel.kt | 4 ++ .../chat/simplex/common/model/SimpleXAPI.kt | 48 +++++++++++++++---- .../common/views/chat/item/CIFileView.kt | 5 +- .../common/views/chat/item/CIImageView.kt | 10 ++-- .../common/views/chat/item/CIVIdeoView.kt | 5 +- .../common/views/chat/item/CIVoiceView.kt | 8 +++- .../common/views/chat/item/FramedItemView.kt | 2 +- .../views/usersettings/PrivacySettings.kt | 23 +++++++-- .../commonMain/resources/MR/base/strings.xml | 6 +++ 9 files changed, 84 insertions(+), 27 deletions(-) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index 3780066092..56487874ba 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -2644,6 +2644,7 @@ data class CIFile( is CIFileStatus.RcvInvitation -> false is CIFileStatus.RcvAccepted -> false is CIFileStatus.RcvTransfer -> false + is CIFileStatus.RcvAborted -> false is CIFileStatus.RcvCancelled -> false is CIFileStatus.RcvComplete -> true is CIFileStatus.RcvError -> false @@ -2665,6 +2666,7 @@ data class CIFile( is CIFileStatus.RcvInvitation -> null is CIFileStatus.RcvAccepted -> rcvCancelAction is CIFileStatus.RcvTransfer -> rcvCancelAction + is CIFileStatus.RcvAborted -> null is CIFileStatus.RcvCancelled -> null is CIFileStatus.RcvComplete -> null is CIFileStatus.RcvError -> null @@ -2845,6 +2847,7 @@ sealed class CIFileStatus { @Serializable @SerialName("rcvInvitation") object RcvInvitation: CIFileStatus() @Serializable @SerialName("rcvAccepted") object RcvAccepted: CIFileStatus() @Serializable @SerialName("rcvTransfer") class RcvTransfer(val rcvProgress: Long, val rcvTotal: Long): CIFileStatus() + @Serializable @SerialName("rcvAborted") object RcvAborted: CIFileStatus() @Serializable @SerialName("rcvComplete") object RcvComplete: CIFileStatus() @Serializable @SerialName("rcvCancelled") object RcvCancelled: CIFileStatus() @Serializable @SerialName("rcvError") object RcvError: CIFileStatus() @@ -2859,6 +2862,7 @@ sealed class CIFileStatus { is RcvInvitation -> false is RcvAccepted -> false is RcvTransfer -> false + is RcvAborted -> false is RcvComplete -> false is RcvCancelled -> false is RcvError -> false diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index fa9c2580ee..ca237cd52f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -12,6 +12,7 @@ import dev.icerock.moko.resources.compose.painterResource import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.call.* +import chat.simplex.common.views.chat.group.toggleShowMemberMessages import chat.simplex.common.views.migration.MigrationFileLinkData import chat.simplex.common.views.onboarding.OnboardingStage import chat.simplex.common.views.usersettings.* @@ -106,6 +107,7 @@ class AppPreferences { val privacySaveLastDraft = mkBoolPreference(SHARED_PREFS_PRIVACY_SAVE_LAST_DRAFT, true) val privacyDeliveryReceiptsSet = mkBoolPreference(SHARED_PREFS_PRIVACY_DELIVERY_RECEIPTS_SET, false) val privacyEncryptLocalFiles = mkBoolPreference(SHARED_PREFS_PRIVACY_ENCRYPT_LOCAL_FILES, true) + val privacyAskToApproveRelays = mkBoolPreference(SHARED_PREFS_PRIVACY_ASK_TO_APPROVE_RELAYS, true) val experimentalCalls = mkBoolPreference(SHARED_PREFS_EXPERIMENTAL_CALLS, false) val showUnreadAndFavorites = mkBoolPreference(SHARED_PREFS_SHOW_UNREAD_AND_FAVORITES, false) val chatArchiveName = mkStrPreference(SHARED_PREFS_CHAT_ARCHIVE_NAME, null) @@ -292,6 +294,7 @@ class AppPreferences { private const val SHARED_PREFS_PRIVACY_SAVE_LAST_DRAFT = "PrivacySaveLastDraft" private const val SHARED_PREFS_PRIVACY_DELIVERY_RECEIPTS_SET = "PrivacyDeliveryReceiptsSet" private const val SHARED_PREFS_PRIVACY_ENCRYPT_LOCAL_FILES = "PrivacyEncryptLocalFiles" + private const val SHARED_PREFS_PRIVACY_ASK_TO_APPROVE_RELAYS = "PrivacyAskToApproveRelays" const val SHARED_PREFS_PRIVACY_FULL_BACKUP = "FullBackup" private const val SHARED_PREFS_EXPERIMENTAL_CALLS = "ExperimentalCalls" private const val SHARED_PREFS_SHOW_UNREAD_AND_FAVORITES = "ShowUnreadAndFavorites" @@ -1337,9 +1340,9 @@ object ChatController { } } - suspend fun apiReceiveFile(rh: Long?, fileId: Long, encrypted: Boolean, inline: Boolean? = null, auto: Boolean = false): AChatItem? { + suspend fun apiReceiveFile(rh: Long?, fileId: Long, userApprovedRelays: Boolean, encrypted: Boolean, inline: Boolean? = null, auto: Boolean = false): AChatItem? { // -1 here is to override default behavior of providing current remote host id because file can be asked by local device while remote is connected - val r = sendCmd(rh, CC.ReceiveFile(fileId, encrypted, inline)) + val r = sendCmd(rh, CC.ReceiveFile(fileId, userApprovedRelays = userApprovedRelays, encrypt = encrypted, inline = inline)) return when (r) { is CR.RcvFileAccepted -> r.chatItem is CR.RcvFileAcceptedSndCancelled -> { @@ -1358,7 +1361,23 @@ object ChatController { val maybeChatError = chatError(r) if (maybeChatError is ChatErrorType.FileCancelled || maybeChatError is ChatErrorType.FileAlreadyReceiving) { Log.d(TAG, "apiReceiveFile ignoring FileCancelled or FileAlreadyReceiving error") - } else { + } else if (maybeChatError is ChatErrorType.FileNotApproved) { + Log.d(TAG, "apiReceiveFile FileNotApproved error") + if (!auto) { + val srvs = maybeChatError.unknownServers.map{ serverHostname(it) } + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.file_not_approved_title), + text = generalGetString(MR.strings.file_not_approved_descr).format(srvs.sorted().joinToString(separator = ", ")), + confirmText = generalGetString(MR.strings.download_file), + onConfirm = { + val user = chatModel.currentUser.value + if (user != null) { + withBGApi { chatModel.controller.receiveFile(rh, user, fileId, userApprovedRelays = true) } + } + }, + ) + } + } else if (!auto) { apiErrorAlert("apiReceiveFile", generalGetString(MR.strings.error_receiving_file), r) } } @@ -2216,9 +2235,14 @@ object ChatController { } } - suspend fun receiveFile(rhId: Long?, user: UserLike, fileId: Long, auto: Boolean = false) { - val encrypted = appPrefs.privacyEncryptLocalFiles.get() - val chatItem = apiReceiveFile(rhId, fileId, encrypted = encrypted, auto = auto) + suspend fun receiveFile(rhId: Long?, user: UserLike, fileId: Long, userApprovedRelays: Boolean = false, auto: Boolean = false) { + val chatItem = apiReceiveFile( + rhId, + fileId, + userApprovedRelays = userApprovedRelays || !appPrefs.privacyAskToApproveRelays.get(), + encrypted = appPrefs.privacyEncryptLocalFiles.get(), + auto = auto + ) if (chatItem != null) { chatItemSimpleUpdate(rhId, user, chatItem) } @@ -2501,7 +2525,7 @@ sealed class CC { class ApiRejectContact(val contactReqId: Long): 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 ReceiveFile(val fileId: Long, val encrypt: Boolean, val inline: Boolean?): CC() + class ReceiveFile(val fileId: Long, val userApprovedRelays: Boolean, val encrypt: Boolean, val inline: Boolean?): CC() class CancelFile(val fileId: Long): CC() // Remote control class SetLocalDeviceName(val displayName: String): CC() @@ -2652,6 +2676,7 @@ sealed class CC { is ApiChatUnread -> "/_unread chat ${chatRef(type, id)} ${onOff(unreadChat)}" is ReceiveFile -> "/freceive $fileId" + + (" approved_relays=${onOff(userApprovedRelays)}") + (if (encrypt == null) "" else " encrypt=${onOff(encrypt)}") + (if (inline == null) "" else " inline=${onOff(inline)}") is CancelFile -> "/fcancel $fileId" @@ -4892,13 +4917,14 @@ sealed class ChatErrorType { is FileCancel -> "fileCancel" is FileAlreadyExists -> "fileAlreadyExists" is FileRead -> "fileRead" - is FileWrite -> "fileWrite" + is FileWrite -> "fileWrite $message" is FileSend -> "fileSend" is FileRcvChunk -> "fileRcvChunk" is FileInternal -> "fileInternal" is FileImageType -> "fileImageType" is FileImageSize -> "fileImageSize" is FileNotReceived -> "fileNotReceived" + is FileNotApproved -> "fileNotApproved" // is XFTPRcvFile -> "xftpRcvFile" // is XFTPSndFile -> "xftpSndFile" is FallbackToSMPProhibited -> "fallbackToSMPProhibited" @@ -4978,6 +5004,7 @@ sealed class ChatErrorType { @Serializable @SerialName("fileImageType") class FileImageType(val filePath: String): ChatErrorType() @Serializable @SerialName("fileImageSize") class FileImageSize(val filePath: String): ChatErrorType() @Serializable @SerialName("fileNotReceived") class FileNotReceived(val fileId: Long): ChatErrorType() + @Serializable @SerialName("fileNotApproved") class FileNotApproved(val fileId: Long, val unknownServers: List): ChatErrorType() // @Serializable @SerialName("xFTPRcvFile") object XFTPRcvFile: ChatErrorType() // @Serializable @SerialName("xFTPSndFile") object XFTPSndFile: ChatErrorType() @Serializable @SerialName("fallbackToSMPProhibited") class FallbackToSMPProhibited(val fileId: Long): ChatErrorType() @@ -5476,6 +5503,7 @@ enum class NotificationsMode() { data class AppSettings( var networkConfig: NetCfg? = null, var privacyEncryptLocalFiles: Boolean? = null, + var privacyAskToApproveRelays: Boolean? = null, var privacyAcceptImages: Boolean? = null, var privacyLinkPreviews: Boolean? = null, var privacyShowChatPreviews: Boolean? = null, @@ -5499,6 +5527,7 @@ data class AppSettings( val def = defaults if (networkConfig != def.networkConfig) { empty.networkConfig = networkConfig } if (privacyEncryptLocalFiles != def.privacyEncryptLocalFiles) { empty.privacyEncryptLocalFiles = privacyEncryptLocalFiles } + if (privacyAskToApproveRelays != def.privacyAskToApproveRelays) { empty.privacyAskToApproveRelays = privacyAskToApproveRelays } if (privacyAcceptImages != def.privacyAcceptImages) { empty.privacyAcceptImages = privacyAcceptImages } if (privacyLinkPreviews != def.privacyLinkPreviews) { empty.privacyLinkPreviews = privacyLinkPreviews } if (privacyShowChatPreviews != def.privacyShowChatPreviews) { empty.privacyShowChatPreviews = privacyShowChatPreviews } @@ -5530,6 +5559,7 @@ data class AppSettings( setNetCfg(net) } privacyEncryptLocalFiles?.let { def.privacyEncryptLocalFiles.set(it) } + privacyAskToApproveRelays?.let { def.privacyAskToApproveRelays.set(it) } privacyAcceptImages?.let { def.privacyAcceptImages.set(it) } privacyLinkPreviews?.let { def.privacyLinkPreviews.set(it) } privacyShowChatPreviews?.let { def.privacyShowChatPreviews.set(it) } @@ -5554,6 +5584,7 @@ data class AppSettings( get() = AppSettings( networkConfig = NetCfg.defaults, privacyEncryptLocalFiles = true, + privacyAskToApproveRelays = true, privacyAcceptImages = true, privacyLinkPreviews = true, privacyShowChatPreviews = true, @@ -5579,6 +5610,7 @@ data class AppSettings( return defaults.copy( networkConfig = getNetCfg(), privacyEncryptLocalFiles = def.privacyEncryptLocalFiles.get(), + privacyAskToApproveRelays = def.privacyAskToApproveRelays.get(), privacyAcceptImages = def.privacyAcceptImages.get(), privacyLinkPreviews = def.privacyLinkPreviews.get(), privacyShowChatPreviews = def.privacyShowChatPreviews.get(), diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.kt index f079a152ab..a48bb2bb12 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.kt @@ -1,6 +1,5 @@ package chat.simplex.common.views.chat.item -import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CornerSize @@ -64,7 +63,7 @@ fun CIFileView( fun fileAction() { if (file != null) { when { - file.fileStatus is CIFileStatus.RcvInvitation -> { + file.fileStatus is CIFileStatus.RcvInvitation || file.fileStatus is CIFileStatus.RcvAborted -> { if (fileSizeValid(file)) { receiveFile(file.fileId) } else { @@ -176,6 +175,8 @@ fun CIFileView( } else { progressIndicator() } + is CIFileStatus.RcvAborted -> + fileIcon(innerIcon = painterResource(MR.images.ic_sync_problem), color = MaterialTheme.colors.primary) is CIFileStatus.RcvComplete -> fileIcon() is CIFileStatus.RcvCancelled -> fileIcon(innerIcon = painterResource(MR.images.ic_close)) is CIFileStatus.RcvError -> fileIcon(innerIcon = painterResource(MR.images.ic_close)) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt index 65fb38575d..7cffe4564b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt @@ -21,17 +21,12 @@ import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.DEFAULT_MAX_IMAGE_WIDTH import chat.simplex.res.MR import dev.icerock.moko.resources.StringResource -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.runBlocking -import java.io.File -import java.net.URI @Composable fun CIImageView( image: String, file: CIFile?, - metaColor: Color, imageProvider: () -> ImageGalleryProvider, showMenu: MutableState, receiveFile: (Long) -> Unit @@ -51,7 +46,7 @@ fun CIImageView( icon, stringResource(stringId), Modifier.fillMaxSize(), - tint = metaColor + tint = Color.White ) } @@ -78,6 +73,7 @@ fun CIImageView( is CIFileStatus.RcvInvitation -> fileIcon(painterResource(MR.images.ic_arrow_downward), MR.strings.icon_descr_asked_to_receive) is CIFileStatus.RcvAccepted -> fileIcon(painterResource(MR.images.ic_more_horiz), MR.strings.icon_descr_waiting_for_image) is CIFileStatus.RcvTransfer -> progressIndicator() + is CIFileStatus.RcvAborted -> fileIcon(painterResource(MR.images.ic_sync_problem), MR.strings.icon_descr_file) is CIFileStatus.RcvCancelled -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file) is CIFileStatus.RcvError -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file) is CIFileStatus.Invalid -> fileIcon(painterResource(MR.images.ic_question_mark), MR.strings.icon_descr_file) @@ -206,7 +202,7 @@ fun CIImageView( imageView(base64ToBitmap(image), onClick = { if (file != null) { when (file.fileStatus) { - CIFileStatus.RcvInvitation -> + CIFileStatus.RcvInvitation, CIFileStatus.RcvAborted -> if (fileSizeValid()) { receiveFile(file.fileId) } else { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVIdeoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVIdeoView.kt index a79e509d02..749816f918 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVIdeoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVIdeoView.kt @@ -73,7 +73,7 @@ fun CIVideoView( VideoPreviewImageView(preview, onClick = { if (file != null) { when (file.fileStatus) { - CIFileStatus.RcvInvitation -> + CIFileStatus.RcvInvitation, CIFileStatus.RcvAborted -> receiveFileIfValidSize(file, receiveFile) CIFileStatus.RcvAccepted -> when (file.fileProtocol) { @@ -102,7 +102,7 @@ fun CIVideoView( if (file != null) { DurationProgress(file, remember { mutableStateOf(false) }, remember { mutableStateOf(duration * 1000L) }, remember { mutableStateOf(0L) }/*, soundEnabled*/) } - if (file?.fileStatus is CIFileStatus.RcvInvitation) { + if (file?.fileStatus is CIFileStatus.RcvInvitation || file?.fileStatus is CIFileStatus.RcvAborted) { PlayButton(error = false, { showMenu.value = true }) { receiveFileIfValidSize(file, receiveFile) } } } @@ -396,6 +396,7 @@ private fun loadingIndicator(file: CIFile?) { } else { progressIndicator() } + is CIFileStatus.RcvAborted -> fileIcon(painterResource(MR.images.ic_sync_problem), MR.strings.icon_descr_file) is CIFileStatus.RcvCancelled -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file) is CIFileStatus.RcvError -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file) is CIFileStatus.Invalid -> fileIcon(painterResource(MR.images.ic_question_mark), MR.strings.icon_descr_file) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVoiceView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVoiceView.kt index cac89b2587..f973a6ea66 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVoiceView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVoiceView.kt @@ -22,6 +22,7 @@ import chat.simplex.common.views.helpers.* import chat.simplex.common.model.* import chat.simplex.common.platform.* import chat.simplex.res.MR +import dev.icerock.moko.resources.ImageResource import kotlinx.coroutines.flow.* // TODO refactor https://github.com/simplex-chat/simplex-chat/pull/1451#discussion_r1033429901 @@ -220,7 +221,8 @@ private fun PlayPauseButton( error: Boolean, play: () -> Unit, pause: () -> Unit, - longClick: () -> Unit + longClick: () -> Unit, + icon: ImageResource = MR.images.ic_play_arrow_filled, ) { val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage @@ -241,7 +243,7 @@ private fun PlayPauseButton( contentAlignment = Alignment.Center ) { Icon( - if (audioPlaying) painterResource(MR.images.ic_pause_filled) else painterResource(MR.images.ic_play_arrow_filled), + if (audioPlaying) painterResource(MR.images.ic_pause_filled) else painterResource(icon), contentDescription = null, Modifier.size(36.dp), tint = if (error) WarningOrange else if (!enabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary @@ -294,6 +296,8 @@ private fun VoiceMsgIndicator( ) { ProgressIndicator() } + } else if (file?.fileStatus is CIFileStatus.RcvAborted) { + PlayPauseButton(audioPlaying, sent, 0f, strokeWidth, strokeColor, true, error, { receiveFile(file.fileId) }, {}, longClick = longClick, icon = MR.images.ic_sync_problem) } else { PlayPauseButton(audioPlaying, sent, 0f, strokeWidth, strokeColor, false, false, {}, {}, longClick) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt index aec314d2c1..4dc0fd9cf5 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt @@ -241,7 +241,7 @@ fun FramedItemView( } else { when (val mc = ci.content.msgContent) { is MsgContent.MCImage -> { - CIImageView(image = mc.image, file = ci.file, metaColor = metaColor, imageProvider ?: return@PriorityLayout, showMenu, receiveFile) + CIImageView(image = mc.image, file = ci.file, imageProvider ?: return@PriorityLayout, showMenu, receiveFile) if (mc.text == "" && !ci.meta.isLive) { metaColor = Color.White } else { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt index b376285259..dc0760193d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt @@ -1,6 +1,7 @@ package chat.simplex.common.views.usersettings import SectionBottomSpacer +import SectionCustomFooter import SectionDividerSpaced import SectionItemView import SectionTextFooter @@ -63,10 +64,6 @@ fun PrivacySettingsView( SectionDividerSpaced() SectionView(stringResource(MR.strings.settings_section_title_chats)) { - SettingsPreferenceItem(painterResource(MR.images.ic_lock), stringResource(MR.strings.encrypt_local_files), chatModel.controller.appPrefs.privacyEncryptLocalFiles, onChange = { enable -> - withBGApi { chatModel.controller.apiSetEncryptLocalFiles(enable) } - }) - SettingsPreferenceItem(painterResource(MR.images.ic_image), stringResource(MR.strings.auto_accept_images), chatModel.controller.appPrefs.privacyAcceptImages) SettingsPreferenceItem(painterResource(MR.images.ic_travel_explore), stringResource(MR.strings.send_link_previews), chatModel.controller.appPrefs.privacyLinkPreviews) SettingsPreferenceItem( painterResource(MR.images.ic_chat_bubble), @@ -91,6 +88,22 @@ fun PrivacySettingsView( chatModel.simplexLinkMode.value = it }) } + SectionDividerSpaced() + + SectionView(stringResource(MR.strings.settings_section_title_files)) { + SettingsPreferenceItem(painterResource(MR.images.ic_lock), stringResource(MR.strings.encrypt_local_files), chatModel.controller.appPrefs.privacyEncryptLocalFiles, onChange = { enable -> + withBGApi { chatModel.controller.apiSetEncryptLocalFiles(enable) } + }) + SettingsPreferenceItem(painterResource(MR.images.ic_image), stringResource(MR.strings.auto_accept_images), chatModel.controller.appPrefs.privacyAcceptImages) + SettingsPreferenceItem(painterResource(MR.images.ic_security), stringResource(MR.strings.protect_ip_address), chatModel.controller.appPrefs.privacyAskToApproveRelays) + } + SectionCustomFooter { + if (chatModel.controller.appPrefs.privacyAskToApproveRelays.state.value) { + Text(stringResource(MR.strings.app_will_ask_to_confirm_unknown_file_servers)) + } else { + Text(stringResource(MR.strings.without_tor_or_vpn_ip_address_will_be_visible_to_file_servers)) + } + } val currentUser = chatModel.currentUser.value if (currentUser != null) { @@ -141,7 +154,7 @@ fun PrivacySettingsView( } if (!chatModel.desktopNoUserNoRemote) { - SectionDividerSpaced() + SectionDividerSpaced(maxTopPadding = true) DeliveryReceiptsSection( currentUser = currentUser, setOrAskSendReceiptsContacts = { enable -> diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index 93e6a902f4..9308f5886e 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -119,6 +119,8 @@ Error joining group Cannot receive file Sender cancelled file transfer. + Unknown servers! + Without Tor or VPN, your IP address will be visible to these XFTP relays:\n%1$s. Error receiving file Error creating address Contact already exists @@ -992,6 +994,9 @@ Protect app screen Encrypt local files Auto-accept images + Protect IP address + The app will ask to confirm downloads from unknown file servers (except .onion or when SOCKS proxy is enabled). + Without Tor or VPN, your IP address will be visible to file servers. Send link previews Show last messages Message draft @@ -1056,6 +1061,7 @@ APP DEVICE CHATS + FILES SEND DELIVERY RECEIPTS TO Restart Shutdown