diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 0089fd0879..2a6a768d69 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -257,6 +257,12 @@ func setXFTPConfig(_ cfg: XFTPFileConfig?) throws { throw r } +func apiSetEncryptLocalFiles(_ enable: Bool) throws { + let r = chatSendCmdSync(.apiSetEncryptLocalFiles(enable: enable)) + if case .cmdOk = r { return } + throw r +} + func apiExportArchive(config: ArchiveConfig) async throws { try await sendCommandOkResp(.apiExportArchive(config: config)) } @@ -1184,6 +1190,7 @@ func initializeChat(start: Bool, dbKey: String? = nil, refreshInvitations: Bool try apiSetTempFolder(tempFolder: getTempFilesDirectory().path) try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path) try setXFTPConfig(getXFTPCfg()) + try apiSetEncryptLocalFiles(privacyEncryptLocalFilesGroupDefault.get()) m.chatInitialized = true m.currentUser = try apiGetActiveUser() if m.currentUser == nil { diff --git a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift index 2eb6d9f6bd..b7b7e73dca 100644 --- a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift @@ -111,14 +111,17 @@ struct ChatPreviewView: View { private func chatPreviewLayout(_ text: Text, draft: Bool = false) -> some View { ZStack(alignment: .topTrailing) { - text + let t = text .lineLimit(2) .multilineTextAlignment(.leading) .frame(maxWidth: .infinity, alignment: .topLeading) .padding(.leading, 8) .padding(.trailing, 36) - .privacySensitive(!showChatPreviews && !draft) - .redacted(reason: .privacy) + if !showChatPreviews && !draft { + t.privacySensitive(true).redacted(reason: .privacy) + } else { + t + } let s = chat.chatStats if s.unreadCount > 0 || s.unreadChat { unreadCountText(s.unreadCount) diff --git a/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift b/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift index 34b6f147bd..16555fad3a 100644 --- a/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift +++ b/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift @@ -66,6 +66,9 @@ 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) @@ -183,6 +186,16 @@ struct PrivacySettings: View { } } + private func setEncryptLocalFiles(_ enable: Bool) { + do { + try apiSetEncryptLocalFiles(enable) + } catch let error { + let err = responseError(error) + logger.error("apiSetEncryptLocalFiles \(err)") + alert = .error(title: "Error", error: "\(err)") + } + } + private func setOrAskSendReceiptsContacts(_ enable: Bool) { contactReceiptsOverrides = m.chats.reduce(0) { count, chat in let sendRcpts = chat.chatInfo.contact?.chatSettings.sendRcpts diff --git a/apps/ios/SimpleX NSE/NotificationService.swift b/apps/ios/SimpleX NSE/NotificationService.swift index 645fdb5952..ea52f4be89 100644 --- a/apps/ios/SimpleX NSE/NotificationService.swift +++ b/apps/ios/SimpleX NSE/NotificationService.swift @@ -216,6 +216,7 @@ func startChat() -> DBMigrationResult? { try apiSetTempFolder(tempFolder: getTempFilesDirectory().path) try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path) try setXFTPConfig(xftpConfig) + try apiSetEncryptLocalFiles(privacyEncryptLocalFilesGroupDefault.get()) let justStarted = try apiStartChat() chatStarted = true if justStarted { @@ -351,6 +352,12 @@ func setXFTPConfig(_ cfg: XFTPFileConfig?) throws { throw r } +func apiSetEncryptLocalFiles(_ enable: Bool) throws { + let r = sendSimpleXCmd(.apiSetEncryptLocalFiles(enable: enable)) + if case .cmdOk = r { return } + throw r +} + func apiGetNtfMessage(nonce: String, encNtfInfo: String) -> NtfMessages? { guard apiGetActiveUser() != nil else { logger.debug("no active user") diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 761c1daf7c..31a7ae0b69 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -32,6 +32,7 @@ public enum ChatCommand { case setTempFolder(tempFolder: String) case setFilesFolder(filesFolder: String) case apiSetXFTPConfig(config: XFTPFileConfig?) + case apiSetEncryptLocalFiles(enable: Bool) case apiExportArchive(config: ArchiveConfig) case apiImportArchive(config: ArchiveConfig) case apiDeleteStorage @@ -114,8 +115,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, encrypted: Bool?, inline: Bool?) + case setFileToReceive(fileId: Int64, encrypted: Bool?) case cancelFile(fileId: Int64) case setLocalDeviceName(displayName: String) case startRemoteCtrl @@ -160,6 +161,7 @@ public enum ChatCommand { } else { return "/_xftp off" } + case let .apiSetEncryptLocalFiles(enable): return "/_files_encrypt \(onOff(enable))" case let .apiExportArchive(cfg): return "/_db export \(encodeJSON(cfg))" case let .apiImportArchive(cfg): return "/_db import \(encodeJSON(cfg))" case .apiDeleteStorage: return "/_db delete" @@ -255,13 +257,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, encrypted, inline): - let s = "/freceive \(fileId) encrypt=\(onOff(encrypted))" - 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 .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 .cancelFile(fileId): return "/fcancel \(fileId)" case let .setLocalDeviceName(displayName): return "/set device name \(displayName)" case .startRemoteCtrl: return "/start remote ctrl" @@ -299,6 +296,7 @@ public enum ChatCommand { case .setTempFolder: return "setTempFolder" case .setFilesFolder: return "setFilesFolder" case .apiSetXFTPConfig: return "apiSetXFTPConfig" + case .apiSetEncryptLocalFiles: return "apiSetEncryptLocalFiles" case .apiExportArchive: return "apiExportArchive" case .apiImportArchive: return "apiImportArchive" case .apiDeleteStorage: return "apiDeleteStorage" @@ -444,6 +442,13 @@ public enum ChatCommand { 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 { pwd == "" || pwd == nil ? "" : " " + encodeJSON(pwd) } 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 1f5cc09d47..0db504c15d 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 @@ -340,6 +340,7 @@ object ChatController { apiSetTempFolder(coreTmpDir.absolutePath) apiSetFilesFolder(appFilesDir.absolutePath) apiSetXFTPConfig(getXFTPCfg()) + apiSetEncryptLocalFiles(appPrefs.privacyEncryptLocalFiles.get()) val justStarted = apiStartChat() val users = listUsers() chatModel.users.clear() @@ -567,6 +568,8 @@ object ChatController { throw Error("apiSetXFTPConfig bad response: ${r.responseType} ${r.details}") } + suspend fun apiSetEncryptLocalFiles(enable: Boolean) = sendCommandOkResp(CC.ApiSetEncryptLocalFiles(enable)) + suspend fun apiExportArchive(config: ArchiveConfig) { val r = sendCmd(CC.ApiExportArchive(config)) if (r is CR.CmdOk) return @@ -1384,7 +1387,7 @@ 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) + if (!ok) apiErrorAlert(cmd.cmdType, generalGetString(MR.strings.error_alert_title), r) return ok } @@ -1927,6 +1930,7 @@ sealed class CC { class SetTempFolder(val tempFolder: String): CC() class SetFilesFolder(val filesFolder: String): CC() class ApiSetXFTPConfig(val config: XFTPFileConfig?): CC() + class ApiSetEncryptLocalFiles(val enable: Boolean): CC() class ApiExportArchive(val config: ArchiveConfig): CC() class ApiImportArchive(val config: ArchiveConfig): CC() class ApiDeleteStorage: CC() @@ -2000,7 +2004,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 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 SetLocalDeviceName(val displayName: String): CC() class CreateRemoteHost(): CC() @@ -2045,6 +2049,7 @@ sealed class CC { is SetTempFolder -> "/_temp_folder $tempFolder" is SetFilesFolder -> "/_files_folder $filesFolder" 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 ApiImportArchive -> "/_db import ${json.encodeToString(config)}" is ApiDeleteStorage -> "/_db delete" @@ -2121,7 +2126,10 @@ sealed class CC { is ApiGetNetworkStatuses -> "/_network_statuses" is ApiChatRead -> "/_read chat ${chatRef(type, id)} from=${range.from} to=${range.to}" 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 SetLocalDeviceName -> "/set device name $displayName" is CreateRemoteHost -> "/create remote host" @@ -2158,6 +2166,7 @@ sealed class CC { is SetTempFolder -> "setTempFolder" is SetFilesFolder -> "setFilesFolder" is ApiSetXFTPConfig -> "apiSetXFTPConfig" + is ApiSetEncryptLocalFiles -> "apiSetEncryptLocalFiles" is ApiExportArchive -> "apiExportArchive" is ApiImportArchive -> "apiImportArchive" is ApiDeleteStorage -> "apiDeleteStorage" 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 ef0940b2a0..210d3c6136 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 @@ -64,7 +64,9 @@ 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) + 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( 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 7912a5ad1b..114fe49e92 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -113,6 +113,7 @@ Server requires authorization to upload, check password Possibly, certificate fingerprint in server address is incorrect Error setting address + Error Connect Disconnect Create queue diff --git a/cabal.project b/cabal.project index 1364d77bf2..9a9a3e25da 100644 --- a/cabal.project +++ b/cabal.project @@ -9,7 +9,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 6b0da8ac50b1582c9f5187c316b93fc8f12c9365 + tag: 1ad69cf74f18f25713ce564e1629d2538313b9e0 source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index e1880738d7..17d650cb09 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."6b0da8ac50b1582c9f5187c316b93fc8f12c9365" = "18n0b1l1adraw5rq118a6iz9pqg43yf41vrzm193q1si06iwk24b"; + "https://github.com/simplex-chat/simplexmq.git"."1ad69cf74f18f25713ce564e1629d2538313b9e0" = "1kil0962pn3ksnxh7dcwcbnkidz95yl31rm4m585ps7wnh6fp0l9"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/kazu-yamamoto/http2.git"."b5a1b7200cf5bc7044af34ba325284271f6dff25" = "0dqb50j57an64nf4qcf5vcz4xkd1vzvghvf8bk529c1k30r9nfzb"; "https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "0kiwhvml42g9anw4d2v0zd1fpc790pj9syg5x3ik4l97fnkbbwpp"; diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index e1bd795b38..fd1b43bd2c 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -216,6 +216,7 @@ newChatController ChatDatabase {chatStore, agentStore} user cfg@ChatConfig {agen cleanupManagerAsync <- newTVarIO Nothing timedItemThreads <- atomically TM.empty showLiveItems <- newTVarIO False + encryptLocalFiles <- newTVarIO False userXFTPFileConfig <- newTVarIO $ xftpFileConfig cfg tempDirectory <- newTVarIO tempDir contactMergeEnabled <- newTVarIO True @@ -248,6 +249,7 @@ newChatController ChatDatabase {chatStore, agentStore} user cfg@ChatConfig {agen cleanupManagerAsync, timedItemThreads, showLiveItems, + encryptLocalFiles, userXFTPFileConfig, tempDirectory, logFilePath = logFile, @@ -535,6 +537,7 @@ processChatCommand = \case APISetXFTPConfig cfg -> do asks userXFTPFileConfig >>= atomically . (`writeTVar` cfg) ok_ + APISetEncryptLocalFiles on -> chatWriteVar encryptLocalFiles on >> ok_ SetContactMergeEnabled onOff -> do asks contactMergeEnabled >>= atomically . (`writeTVar` onOff) ok_ @@ -1381,13 +1384,13 @@ processChatCommand = \case APIConnect _ _ Nothing -> throwChatError CEInvalidConnReq Connect incognito aCReqUri@(Just cReqUri) -> withUser $ \user@User {userId} -> do plan <- connectPlan user cReqUri `catchChatError` const (pure $ CPInvitationLink ILPOk) - unless (connectionPlanOk plan) $ throwChatError (CEConnectionPlan plan) + unless (connectionPlanProceed plan) $ throwChatError (CEConnectionPlan plan) processChatCommand $ APIConnect userId incognito aCReqUri Connect _ Nothing -> throwChatError CEInvalidConnReq ConnectSimplex incognito -> withUser $ \user@User {userId} -> do let cReqUri = ACR SCMContact adminContactReq plan <- connectPlan user cReqUri `catchChatError` const (pure $ CPInvitationLink ILPOk) - unless (connectionPlanOk plan) $ throwChatError (CEConnectionPlan plan) + unless (connectionPlanProceed plan) $ throwChatError (CEConnectionPlan plan) processChatCommand $ APIConnect userId incognito (Just cReqUri) DeleteContact cName -> withContactName cName $ \ctId -> APIDeleteChat (ChatRef CTDirect ctId) True ClearContact cName -> withContactName cName $ APIClearChat . ChatRef CTDirect @@ -1793,19 +1796,16 @@ processChatCommand = \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 encrypted_ rcvInline_ filePath_ -> withUser $ \_ -> withChatLock "receiveFile" . procCmd $ do (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_ - where - encryptLocalFile ft = do - cfArgs <- liftIO $ CF.randomArgs - withStore' $ \db -> setFileCryptoArgs db fileId cfArgs - pure (ft :: RcvFileTransfer) {cryptoArgs = Just cfArgs} - SetFileToReceive fileId encrypted -> withUser $ \_ -> do + SetFileToReceive fileId encrypted_ -> withUser $ \_ -> 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 ok_ CancelFile fileId -> withUser $ \user@User {userId} -> @@ -2255,7 +2255,7 @@ processChatCommand = \case processChatCommand $ APISetChatSettings (ChatRef cType chatId) $ updateSettings chatSettings connectPlan :: User -> AConnectionRequestUri -> m ConnectionPlan connectPlan user (ACR SCMInvitation cReq) = do - withStore' (\db -> getConnectionEntityByConnReq db user cReq) >>= \case + withStore' (\db -> getConnectionEntityByConnReq db user cReqSchemas) >>= \case Nothing -> pure $ CPInvitationLink ILPOk Just (RcvDirectMsgConnection conn ct_) -> do let Connection {connStatus, contactConnInitiated} = conn @@ -2268,39 +2268,59 @@ processChatCommand = \case Just ct -> pure $ CPInvitationLink (ILPKnown ct) Nothing -> throwChatError $ CEInternalError "ready RcvDirectMsgConnection connection should have associated contact" Just _ -> throwChatError $ CECommandError "found connection entity is not RcvDirectMsgConnection" + where + cReqSchemas :: (ConnReqInvitation, ConnReqInvitation) + cReqSchemas = case cReq of + (CRInvitationUri crData e2e) -> + ( CRInvitationUri crData {crScheme = CRSSimplex} e2e, + CRInvitationUri crData {crScheme = simplexChat} e2e + ) connectPlan user (ACR SCMContact cReq) = do let CRContactUri ConnReqUriData {crClientData} = cReq groupLinkId = crClientData >>= decodeJSON >>= \(CRDataGroup gli) -> Just gli case groupLinkId of -- contact address Nothing -> - withStore' (`getUserContactLinkByConnReq` cReq) >>= \case + withStore' (`getUserContactLinkByConnReq` cReqSchemas) >>= \case Just _ -> pure $ CPContactAddress CAPOwnLink Nothing -> do - let cReqHash = ConnReqUriHash . C.sha256Hash $ strEncode cReq - withStore' (\db -> getContactByConnReqHash db user cReqHash) >>= \case + withStore' (\db -> getContactConnEntityByConnReqHash db user cReqHashes) >>= \case Nothing -> pure $ CPContactAddress CAPOk - Just ct - | not (contactReady ct) && contactActive ct -> pure $ CPContactAddress (CAPConnecting ct) + Just (RcvDirectMsgConnection _conn Nothing) -> pure $ CPContactAddress CAPConnectingConfirmReconnect + Just (RcvDirectMsgConnection _ (Just ct)) + | not (contactReady ct) && contactActive ct -> pure $ CPContactAddress (CAPConnectingProhibit ct) + | contactDeleted ct -> pure $ CPContactAddress CAPOk | otherwise -> pure $ CPContactAddress (CAPKnown ct) + Just _ -> throwChatError $ CECommandError "found connection entity is not RcvDirectMsgConnection" -- group link Just _ -> - withStore' (\db -> getGroupInfoByUserContactLinkConnReq db user cReq) >>= \case + withStore' (\db -> getGroupInfoByUserContactLinkConnReq db user cReqSchemas) >>= \case Just g -> pure $ CPGroupLink (GLPOwnLink g) Nothing -> do - let cReqHash = ConnReqUriHash . C.sha256Hash $ strEncode cReq - ct_ <- withStore' $ \db -> getContactByConnReqHash db user cReqHash - gInfo_ <- withStore' $ \db -> getGroupInfoByGroupLinkHash db user cReqHash - case (gInfo_, ct_) of + connEnt_ <- withStore' $ \db -> getContactConnEntityByConnReqHash db user cReqHashes + gInfo_ <- withStore' $ \db -> getGroupInfoByGroupLinkHash db user cReqHashes + case (gInfo_, connEnt_) of (Nothing, Nothing) -> pure $ CPGroupLink GLPOk - (Nothing, Just ct) - | not (contactReady ct) && contactActive ct -> pure $ CPGroupLink (GLPConnecting gInfo_) + (Nothing, Just (RcvDirectMsgConnection _conn Nothing)) -> pure $ CPGroupLink GLPConnectingConfirmReconnect + (Nothing, Just (RcvDirectMsgConnection _ (Just ct))) + | not (contactReady ct) && contactActive ct -> pure $ CPGroupLink (GLPConnectingProhibit gInfo_) | otherwise -> pure $ CPGroupLink GLPOk + (Nothing, Just _) -> throwChatError $ CECommandError "found connection entity is not RcvDirectMsgConnection" (Just gInfo@GroupInfo {membership}, _) | not (memberActive membership) && not (memberRemoved membership) -> - pure $ CPGroupLink (GLPConnecting gInfo_) + pure $ CPGroupLink (GLPConnectingProhibit gInfo_) | memberActive membership -> pure $ CPGroupLink (GLPKnown gInfo) | otherwise -> pure $ CPGroupLink GLPOk + where + cReqSchemas :: (ConnReqContact, ConnReqContact) + cReqSchemas = case cReq of + (CRContactUri crData) -> + ( CRContactUri crData {crScheme = CRSSimplex}, + CRContactUri crData {crScheme = simplexChat} + ) + cReqHashes :: (ConnReqUriHash, ConnReqUriHash) + cReqHashes = bimap hash hash cReqSchemas + hash = ConnReqUriHash . C.sha256Hash . strEncode assertDirectAllowed :: ChatMonad m => User -> MsgDirection -> Contact -> CMEventTag e -> m () assertDirectAllowed user dir ct event = @@ -2443,6 +2463,12 @@ toFSFilePath :: ChatMonad' m => FilePath -> m FilePath toFSFilePath f = 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' user ft rcvInline_ filePath_ = do (CRRcvFileAccepted user <$> acceptFileReceive user ft rcvInline_ filePath_) `catchChatError` processError @@ -3964,14 +3990,17 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do inline <- receiveInlineMode fInv (Just mc) fileChunkSize ft@RcvFileTransfer {fileId, xftpRcvFile} <- withStore $ \db -> createRcvFT db fInv inline fileChunkSize let fileProtocol = if isJust xftpRcvFile then FPXFTP else FPSMP - (filePath, fileStatus) <- case inline of + (filePath, fileStatus, ft') <- case inline of Just IFMSent -> do + encrypt <- chatReadVar encryptLocalFiles + ft' <- (if encrypt then setFileToEncrypt else pure) ft fPath <- getRcvFilePath fileId Nothing fileName True - withStore' $ \db -> startRcvInlineFT db user ft fPath inline - pure (Just fPath, CIFSRcvAccepted) - _ -> pure (Nothing, CIFSRcvInvitation) - let fileSource = CF.plain <$> filePath - pure (ft, CIFile {fileId, fileName, fileSize, fileSource, fileStatus, fileProtocol}) + withStore' $ \db -> startRcvInlineFT db user ft' fPath inline + pure (Just fPath, CIFSRcvAccepted, ft') + _ -> pure (Nothing, CIFSRcvInvitation, ft) + let RcvFileTransfer {cryptoArgs} = ft' + 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 ct@Contact {contactId} sharedMsgId mc msg@RcvMessage {msgId} msgMeta ttl live_ = do @@ -5597,6 +5626,7 @@ chatCommandP = ("/_files_folder " <|> "/files_folder ") *> (SetFilesFolder <$> filePath), "/_xftp " *> (APISetXFTPConfig <$> ("on " *> (Just <$> jsonP) <|> ("off" $> Nothing))), "/xftp " *> (APISetXFTPConfig <$> ("on" *> (Just <$> xftpCfgP) <|> ("off" $> Nothing))), + "/_files_encrypt " *> (APISetEncryptLocalFiles <$> onOffP), "/contact_merge " *> (SetContactMergeEnabled <$> onOffP), "/_db export " *> (APIExportArchive <$> jsonP), "/db export" $> ExportArchive, @@ -5773,8 +5803,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 <*> (" encrypt=" *> onOffP <|> pure False) <*> optional (" inline=" *> onOffP) <*> optional (A.space *> filePath)), - "/_set_file_to_receive " *> (SetFileToReceive <$> A.decimal <*> (" encrypt=" *> onOffP <|> pure False)), + ("/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)), ("/fcancel " <|> "/fc ") *> (CancelFile <$> A.decimal), ("/fstatus " <|> "/fs ") *> (FileStatus <$> A.decimal), "/simplex" *> (ConnectSimplex <$> incognitoP), @@ -5950,7 +5980,7 @@ chatCommandP = adminContactReq :: ConnReqContact adminContactReq = - either error id $ strDecode "https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D" + either error id $ strDecode "simplex:/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D" timeItToView :: ChatMonad' m => String -> m a -> m a timeItToView s action = do diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 78848cca22..276bc69c3e 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -189,6 +189,7 @@ data ChatController = ChatController cleanupManagerAsync :: TVar (Maybe (Async ())), timedItemThreads :: TMap (ChatRef, ChatItemId) (TVar (Maybe (Weak ThreadId))), showLiveItems :: TVar Bool, + encryptLocalFiles :: TVar Bool, userXFTPFileConfig :: TVar (Maybe XFTPFileConfig), tempDirectory :: TVar (Maybe FilePath), logFilePath :: Maybe FilePath, @@ -234,6 +235,7 @@ data ChatCommand | SetTempFolder FilePath | SetFilesFolder FilePath | APISetXFTPConfig (Maybe XFTPFileConfig) + | APISetEncryptLocalFiles Bool | SetContactMergeEnabled Bool | APIExportArchive ArchiveConfig | ExportArchive @@ -406,8 +408,8 @@ data ChatCommand | ForwardFile ChatName FileTransferId | ForwardImage ChatName FileTransferId | SendFileDescription ChatName FilePath - | ReceiveFile {fileId :: FileTransferId, storeEncrypted :: Bool, fileInline :: Maybe Bool, filePath :: Maybe FilePath} - | SetFileToReceive {fileId :: FileTransferId, storeEncrypted :: Bool} + | ReceiveFile {fileId :: FileTransferId, storeEncrypted :: Maybe Bool, fileInline :: Maybe Bool, filePath :: Maybe FilePath} + | SetFileToReceive {fileId :: FileTransferId, storeEncrypted :: Maybe Bool} | CancelFile FileTransferId | FileStatus FileTransferId | ShowProfile -- UserId (not used in UI) @@ -723,7 +725,8 @@ instance ToJSON InvitationLinkPlan where data ContactAddressPlan = CAPOk | CAPOwnLink - | CAPConnecting {contact :: Contact} + | CAPConnectingConfirmReconnect + | CAPConnectingProhibit {contact :: Contact} | CAPKnown {contact :: Contact} deriving (Show, Generic) @@ -737,7 +740,8 @@ instance ToJSON ContactAddressPlan where data GroupLinkPlan = GLPOk | GLPOwnLink {groupInfo :: GroupInfo} - | GLPConnecting {groupInfo_ :: Maybe GroupInfo} + | GLPConnectingConfirmReconnect + | GLPConnectingProhibit {groupInfo_ :: Maybe GroupInfo} | GLPKnown {groupInfo :: GroupInfo} deriving (Show, Generic) @@ -748,8 +752,8 @@ instance ToJSON GroupLinkPlan where toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "GLP" toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "GLP" -connectionPlanOk :: ConnectionPlan -> Bool -connectionPlanOk = \case +connectionPlanProceed :: ConnectionPlan -> Bool +connectionPlanProceed = \case CPInvitationLink ilp -> case ilp of ILPOk -> True ILPOwnLink -> True @@ -757,10 +761,12 @@ connectionPlanOk = \case CPContactAddress cap -> case cap of CAPOk -> True CAPOwnLink -> True + CAPConnectingConfirmReconnect -> True _ -> False CPGroupLink glp -> case glp of GLPOk -> True GLPOwnLink _ -> True + GLPConnectingConfirmReconnect -> True _ -> False newtype UserPwd = UserPwd {unUserPwd :: Text} diff --git a/src/Simplex/Chat/Markdown.hs b/src/Simplex/Chat/Markdown.hs index 64b1145539..793fa753e1 100644 --- a/src/Simplex/Chat/Markdown.hs +++ b/src/Simplex/Chat/Markdown.hs @@ -33,7 +33,7 @@ import Simplex.Chat.Types.Util import Simplex.Messaging.Agent.Protocol (AConnectionRequestUri (..), ConnReqScheme (..), ConnReqUriData (..), ConnectionRequestUri (..), SMPQueue (..)) import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (dropPrefix, enumJSON, fstToLower, sumTypeJSON) -import Simplex.Messaging.Protocol (ProtocolServer (..), SrvLoc (..)) +import Simplex.Messaging.Protocol (ProtocolServer (..)) import Simplex.Messaging.Util (safeDecodeUtf8) import System.Console.ANSI.Types import qualified Text.Email.Validate as Email @@ -49,7 +49,7 @@ data Format | Secret | Colored {color :: FormatColor} | Uri - | SimplexLink {linkType :: SimplexLinkType, simplexUri :: Text, trustedUri :: Bool, smpHosts :: NonEmpty Text} + | SimplexLink {linkType :: SimplexLinkType, simplexUri :: Text, smpHosts :: NonEmpty Text} | Email | Phone deriving (Eq, Show, Generic) @@ -248,15 +248,12 @@ markdownP = mconcat <$> A.many' fragmentP simplexUriFormat = \case ACR _ (CRContactUri crData) -> let uri = safeDecodeUtf8 . strEncode $ CRContactUri crData {crScheme = CRSSimplex} - in SimplexLink (linkType' crData) uri (trustedUri' crData) $ uriHosts crData + in SimplexLink (linkType' crData) uri $ uriHosts crData ACR _ (CRInvitationUri crData e2e) -> let uri = safeDecodeUtf8 . strEncode $ CRInvitationUri crData {crScheme = CRSSimplex} e2e - in SimplexLink XLInvitation uri (trustedUri' crData) $ uriHosts crData + in SimplexLink XLInvitation uri $ uriHosts crData where uriHosts ConnReqUriData {crSmpQueues} = L.map (safeDecodeUtf8 . strEncode) $ sconcat $ L.map (host . qServer) crSmpQueues - trustedUri' ConnReqUriData {crScheme} = case crScheme of - CRSSimplex -> True - CRSAppServer (SrvLoc host _) -> host == "simplex.chat" linkType' ConnReqUriData {crClientData} = case crClientData >>= decodeJSON of Just (CRDataGroup _) -> XLGroup Nothing -> XLContact diff --git a/src/Simplex/Chat/Store/Connections.hs b/src/Simplex/Chat/Store/Connections.hs index 3ef77cbb65..59ffb57c6e 100644 --- a/src/Simplex/Chat/Store/Connections.hs +++ b/src/Simplex/Chat/Store/Connections.hs @@ -10,6 +10,7 @@ module Simplex.Chat.Store.Connections ( getConnectionEntity, getConnectionEntityByConnReq, + getContactConnEntityByConnReqHash, getConnectionsToSubscribe, unsetConnectionToSubscribe, ) @@ -153,10 +154,33 @@ getConnectionEntity db user@User {userId, userContactId} agentConnId = do userContact_ [(cReq, groupId)] = Right UserContact {userContactLinkId, connReqContact = cReq, groupId} userContact_ _ = Left SEUserContactLinkNotFound -getConnectionEntityByConnReq :: DB.Connection -> User -> ConnReqInvitation -> IO (Maybe ConnectionEntity) -getConnectionEntityByConnReq db user cReq = do +getConnectionEntityByConnReq :: DB.Connection -> User -> (ConnReqInvitation, ConnReqInvitation) -> IO (Maybe ConnectionEntity) +getConnectionEntityByConnReq db user (cReqSchema1, cReqSchema2) = do connId_ <- maybeFirstRow fromOnly $ - DB.query db "SELECT agent_conn_id FROM connections WHERE conn_req_inv = ? LIMIT 1" (Only cReq) + DB.query db "SELECT agent_conn_id FROM connections WHERE conn_req_inv IN (?,?) LIMIT 1" (cReqSchema1, cReqSchema2) + maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getConnectionEntity db user) connId_ + +-- search connection for connection plan: +-- multiple connections can have same via_contact_uri_hash if request was repeated; +-- this function searches for latest connection with contact so that "known contact" plan would be chosen; +-- deleted connections are filtered out to allow re-connecting via same contact address +getContactConnEntityByConnReqHash :: DB.Connection -> User -> (ConnReqUriHash, ConnReqUriHash) -> IO (Maybe ConnectionEntity) +getContactConnEntityByConnReqHash db user (cReqHash1, cReqHash2) = do + connId_ <- maybeFirstRow fromOnly $ + DB.query + db + [sql| + SELECT agent_conn_id FROM ( + SELECT + agent_conn_id, + (CASE WHEN contact_id IS NOT NULL THEN 1 ELSE 0 END) AS conn_ord + FROM connections + WHERE via_contact_uri_hash IN (?,?) AND conn_status != ? + ORDER BY conn_ord DESC, created_at DESC + LIMIT 1 + ) + |] + (cReqHash1, cReqHash2, ConnDeleted) maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getConnectionEntity db user) connId_ getConnectionsToSubscribe :: DB.Connection -> IO ([ConnId], [ConnectionEntity]) diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs index 30e45a82dc..a4a19816da 100644 --- a/src/Simplex/Chat/Store/Groups.hs +++ b/src/Simplex/Chat/Store/Groups.hs @@ -1121,21 +1121,21 @@ getGroupInfo db User {userId, userContactId} groupId = |] (groupId, userId, userContactId) -getGroupInfoByUserContactLinkConnReq :: DB.Connection -> User -> ConnReqContact -> IO (Maybe GroupInfo) -getGroupInfoByUserContactLinkConnReq db user cReq = do +getGroupInfoByUserContactLinkConnReq :: DB.Connection -> User -> (ConnReqContact, ConnReqContact) -> IO (Maybe GroupInfo) +getGroupInfoByUserContactLinkConnReq db user (cReqSchema1, cReqSchema2) = do groupId_ <- maybeFirstRow fromOnly $ DB.query db [sql| SELECT group_id FROM user_contact_links - WHERE conn_req_contact = ? + WHERE conn_req_contact IN (?,?) |] - (Only cReq) + (cReqSchema1, cReqSchema2) maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getGroupInfo db user) groupId_ -getGroupInfoByGroupLinkHash :: DB.Connection -> User -> ConnReqUriHash -> IO (Maybe GroupInfo) -getGroupInfoByGroupLinkHash db user@User {userId, userContactId} groupLinkHash = do +getGroupInfoByGroupLinkHash :: DB.Connection -> User -> (ConnReqUriHash, ConnReqUriHash) -> IO (Maybe GroupInfo) +getGroupInfoByGroupLinkHash db user@User {userId, userContactId} (groupLinkHash1, groupLinkHash2) = do groupId_ <- maybeFirstRow fromOnly $ DB.query db @@ -1143,11 +1143,11 @@ getGroupInfoByGroupLinkHash db user@User {userId, userContactId} groupLinkHash = SELECT g.group_id FROM groups g JOIN group_members mu ON mu.group_id = g.group_id - WHERE g.user_id = ? AND g.via_group_link_uri_hash = ? + WHERE g.user_id = ? AND g.via_group_link_uri_hash IN (?,?) AND mu.contact_id = ? AND mu.member_status NOT IN (?,?,?) LIMIT 1 |] - (userId, groupLinkHash, userContactId, GSMemRemoved, GSMemLeft, GSMemGroupDeleted) + (userId, groupLinkHash1, groupLinkHash2, userContactId, GSMemRemoved, GSMemLeft, GSMemGroupDeleted) maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getGroupInfo db user) groupId_ getGroupIdByName :: DB.Connection -> User -> GroupName -> ExceptT StoreError IO GroupId diff --git a/src/Simplex/Chat/Store/Profiles.hs b/src/Simplex/Chat/Store/Profiles.hs index 70c8fbc211..a4ec6cc460 100644 --- a/src/Simplex/Chat/Store/Profiles.hs +++ b/src/Simplex/Chat/Store/Profiles.hs @@ -442,17 +442,17 @@ getUserContactLinkById db userId userContactLinkId = |] (userId, userContactLinkId) -getUserContactLinkByConnReq :: DB.Connection -> ConnReqContact -> IO (Maybe UserContactLink) -getUserContactLinkByConnReq db cReq = +getUserContactLinkByConnReq :: DB.Connection -> (ConnReqContact, ConnReqContact) -> IO (Maybe UserContactLink) +getUserContactLinkByConnReq db (cReqSchema1, cReqSchema2) = maybeFirstRow toUserContactLink $ DB.query db [sql| SELECT conn_req_contact, auto_accept, auto_accept_incognito, auto_reply_msg_content FROM user_contact_links - WHERE conn_req_contact = ? + WHERE conn_req_contact IN (?,?) |] - (Only cReq) + (cReqSchema1, cReqSchema2) updateUserAddressAutoAccept :: DB.Connection -> User -> Maybe AutoAccept -> ExceptT StoreError IO UserContactLink updateUserAddressAutoAccept db user@User {userId} autoAccept = do diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index 3783882a4b..b3c4ea09b2 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -207,6 +207,9 @@ contactReady Contact {activeConn} = connReady activeConn contactActive :: Contact -> Bool contactActive Contact {contactStatus} = contactStatus == CSActive +contactDeleted :: Contact -> Bool +contactDeleted Contact {contactStatus} = contactStatus == CSDeleted + contactSecurityCode :: Contact -> Maybe SecurityCode contactSecurityCode Contact {activeConn} = connectionCode activeConn diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 51dcd0c6b6..6c0e703d03 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -167,7 +167,7 @@ responseToView (currentRH, user_) ChatConfig {logLevel, showReactions, showRecei CRRcvFileDescrReady _ _ -> [] CRRcvFileDescrNotReady _ _ -> [] CRRcvFileProgressXFTP {} -> [] - CRRcvFileAccepted u ci -> ttyUser u $ savingFile' testView ci + CRRcvFileAccepted u ci -> ttyUser u $ savingFile' ci CRRcvFileAcceptedSndCancelled u ft -> ttyUser u $ viewRcvFileSndCancelled ft CRSndFileCancelled u _ ftm fts -> ttyUser u $ viewSndFileCancelled ftm fts CRRcvFileCancelled u _ ft -> ttyUser u $ receivingFile_ "cancelled" ft @@ -179,10 +179,10 @@ responseToView (currentRH, user_) ChatConfig {logLevel, showReactions, showRecei 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' CRReceivedContactRequest u UserContactRequest {localDisplayName = c, profile} -> ttyUser u $ viewReceivedContactRequest c profile - CRRcvFileStart u ci -> ttyUser u $ receivingFile_' "started" ci - CRRcvFileComplete u ci -> ttyUser u $ receivingFile_' "completed" ci + CRRcvFileStart u ci -> ttyUser u $ receivingFile_' testView "started" ci + CRRcvFileComplete u ci -> ttyUser u $ receivingFile_' testView "completed" ci 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 CRSndFileComplete u _ ft -> ttyUser u $ sendingFile_ "completed" ft CRSndFileStartXFTP {} -> [] @@ -707,11 +707,14 @@ viewConnReqInvitation :: ConnReqInvitation -> [StyledString] viewConnReqInvitation cReq = [ "pass this invitation link to your contact (via another channel): ", "", - (plain . strEncode) cReq, + (plain . strEncode) (simplexChatInvitation cReq), "", "and ask them to connect: " <> highlight' "/c " ] +simplexChatInvitation :: ConnReqInvitation -> ConnReqInvitation +simplexChatInvitation (CRInvitationUri crData e2e) = CRInvitationUri crData {crScheme = simplexChat} e2e + viewContactNotFound :: ContactName -> Maybe (GroupInfo, GroupMember) -> [StyledString] viewContactNotFound cName suspectedMember = ["no contact " <> ttyContact cName <> useMessageMember] @@ -750,7 +753,7 @@ connReqContact_ :: StyledString -> ConnReqContact -> [StyledString] connReqContact_ intro cReq = [ intro, "", - (plain . strEncode) cReq, + (plain . strEncode) (simplexChatContact cReq), "", "Anybody can send you contact requests with: " <> highlight' "/c ", "to show it again: " <> highlight' "/sa", @@ -758,6 +761,9 @@ connReqContact_ intro cReq = "to delete it: " <> highlight' "/da" <> " (accepted contacts will remain connected)" ] +simplexChatContact :: ConnReqContact -> ConnReqContact +simplexChatContact (CRContactUri crData) = CRContactUri crData {crScheme = simplexChat} + autoAcceptStatus_ :: Maybe AutoAccept -> [StyledString] autoAcceptStatus_ = \case Just AutoAccept {acceptIncognito, autoReply} -> @@ -769,7 +775,7 @@ groupLink_ :: StyledString -> GroupInfo -> ConnReqContact -> GroupMemberRole -> groupLink_ intro g cReq mRole = [ intro, "", - (plain . strEncode) cReq, + (plain . strEncode) (simplexChatContact cReq), "", "Anybody can connect to you and join group as " <> showRole mRole <> " with: " <> highlight' "/c ", "to show it again: " <> highlight ("/show link #" <> viewGroupName g), @@ -1036,7 +1042,7 @@ viewContactInfo :: Contact -> ConnectionStats -> Maybe Profile -> [StyledString] viewContactInfo ct@Contact {contactId, profile = LocalProfile {localAlias, contactLink}, activeConn} stats incognitoProfile = ["contact ID: " <> sShow contactId] <> viewConnectionStats stats - <> maybe [] (\l -> ["contact address: " <> (plain . strEncode) l]) contactLink + <> maybe [] (\l -> ["contact address: " <> (plain . strEncode) (simplexChatContact l)]) contactLink <> maybe ["you've shared main profile with this contact"] (\p -> ["you've shared incognito profile with this contact: " <> incognitoProfile' p]) @@ -1295,7 +1301,8 @@ viewConnectionPlan = \case CPContactAddress cap -> case cap of CAPOk -> [ctAddr "ok to connect"] CAPOwnLink -> [ctAddr "own address"] - CAPConnecting ct -> [ctAddr ("connecting to contact " <> ttyContact' ct)] + CAPConnectingConfirmReconnect -> [ctAddr "connecting, allowed to reconnect"] + CAPConnectingProhibit ct -> [ctAddr ("connecting to contact " <> ttyContact' ct)] CAPKnown ct -> [ ctAddr ("known contact " <> ttyContact' ct), "use " <> ttyToContact' ct <> highlight' "" <> " to send messages" @@ -1305,8 +1312,9 @@ viewConnectionPlan = \case CPGroupLink glp -> case glp of GLPOk -> [grpLink "ok to connect"] GLPOwnLink g -> [grpLink "own link for group " <> ttyGroup' g] - GLPConnecting Nothing -> [grpLink "connecting"] - GLPConnecting (Just g) -> [grpLink ("connecting to group " <> ttyGroup' g)] + GLPConnectingConfirmReconnect -> [grpLink "connecting, allowed to reconnect"] + GLPConnectingProhibit Nothing -> [grpLink "connecting"] + GLPConnectingProhibit (Just g) -> [grpLink ("connecting to group " <> ttyGroup' g)] GLPKnown g -> [ grpLink ("known group " <> ttyGroup' g), "use " <> ttyToGroup g <> highlight' "" <> " to send messages" @@ -1463,27 +1471,28 @@ humanReadableSize size mB = kB * 1024 gB = mB * 1024 -savingFile' :: Bool -> AChatItem -> [StyledString] -savingFile' testView (AChatItem _ _ chat ChatItem {file = Just CIFile {fileId, fileSource = Just (CryptoFile filePath cfArgs_)}, chatDir}) = - let from = case (chat, chatDir) of - (DirectChat Contact {localDisplayName = c}, CIDirectRcv) -> " from " <> ttyContact c - (_, 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 +savingFile' :: AChatItem -> [StyledString] +savingFile' (AChatItem _ _ chat ChatItem {file = Just CIFile {fileId, fileSource = Just (CryptoFile filePath _)}, chatDir}) = + ["saving file " <> sShow fileId <> fileFrom chat chatDir <> " to " <> plain filePath] +savingFile' _ = ["saving file"] -- shouldn't happen -receivingFile_' :: StyledString -> AChatItem -> [StyledString] -receivingFile_' status (AChatItem _ _ (DirectChat c) ChatItem {file = Just CIFile {fileId, fileName}, chatDir = CIDirectRcv}) = - [status <> " receiving " <> fileTransferStr fileId fileName <> " from " <> ttyContact' c] -receivingFile_' status (AChatItem _ _ _ ChatItem {file = Just CIFile {fileId, fileName}, chatDir = CIGroupRcv m}) = - [status <> " receiving " <> fileTransferStr fileId fileName <> " from " <> ttyMember m] -receivingFile_' status _ = [status <> " receiving file"] -- shouldn't happen +receivingFile_' :: Bool -> String -> AChatItem -> [StyledString] +receivingFile_' testView status (AChatItem _ _ chat ChatItem {file = Just CIFile {fileId, fileName, fileSource = Just (CryptoFile _ cfArgs_)}, chatDir}) = + [plain status <> " receiving " <> fileTransferStr fileId fileName <> fileFrom chat chatDir] <> cfArgsStr cfArgs_ + where + cfArgsStr (Just cfArgs@(CFArgs key nonce)) = [plain s | status == "completed"] + 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_ status ft@RcvFileTransfer {senderDisplayName = c} = diff --git a/stack.yaml b/stack.yaml index e467b040e9..6e047f7e6c 100644 --- a/stack.yaml +++ b/stack.yaml @@ -49,7 +49,7 @@ extra-deps: # - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561 # - ../simplexmq - github: simplex-chat/simplexmq - commit: 6b0da8ac50b1582c9f5187c316b93fc8f12c9365 + commit: 1ad69cf74f18f25713ce564e1629d2538313b9e0 - github: kazu-yamamoto/http2 commit: b5a1b7200cf5bc7044af34ba325284271f6dff25 # - ../direct-sqlcipher diff --git a/tests/ChatTests/Direct.hs b/tests/ChatTests/Direct.hs index 677bc4c09d..1a133fd8e3 100644 --- a/tests/ChatTests/Direct.hs +++ b/tests/ChatTests/Direct.hs @@ -270,6 +270,10 @@ testPlanInvitationLinkOwn tmp = alice ##> ("/_connect plan 1 " <> inv) alice <## "invitation link: own link" + let invSchema2 = linkAnotherSchema inv + alice ##> ("/_connect plan 1 " <> invSchema2) + alice <## "invitation link: own link" + alice ##> ("/c " <> inv) alice <## "confirmation sent!" alice @@ -305,6 +309,10 @@ testPlanInvitationLinkConnecting tmp = do bob ##> ("/_connect plan 1 " <> inv) bob <## "invitation link: connecting" + let invSchema2 = linkAnotherSchema inv + bob ##> ("/_connect plan 1 " <> invSchema2) + bob <## "invitation link: connecting" + testContactClear :: HasCallStack => FilePath -> IO () testContactClear = testChat2 aliceProfile bobProfile $ diff --git a/tests/ChatTests/Files.hs b/tests/ChatTests/Files.hs index 50f86d8e09..5231283903 100644 --- a/tests/ChatTests/Files.hs +++ b/tests/ChatTests/Files.hs @@ -33,6 +33,7 @@ chatFileTests = do describe "send and receive file" $ fileTestMatrix2 runTestFileTransfer describe "send file, receive and locally encrypt file" $ fileTestMatrix2 runTestFileTransferEncrypted 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 it "send and receive small file inline (default config)" testSmallInlineFileTransfer 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 [/ | ] to receive it" bob ##> "/fr 1 encrypt=on ./tests/tmp" bob <## "saving file 1 from alice to ./tests/tmp/test.pdf" - Just (CFArgs key nonce) <- J.decode . LB.pack <$> getTermLine bob concurrently_ (bob <## "started receiving file 1 (test.pdf) from alice") (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" ] ] + Just (CFArgs key nonce) <- J.decode . LB.pack <$> getTermLine bob src <- B.readFile "./tests/fixtures/test.pdf" -- dest <- B.readFile "./tests/tmp/test.pdf" -- dest `shouldBe` src @@ -154,6 +155,34 @@ testInlineFileTransfer = where 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 [/ | ] 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 = testChatCfg2 cfg aliceProfile bobProfile $ \alice bob -> do @@ -1077,10 +1106,10 @@ testXFTPFileTransferEncrypted = bob <## "use /fr 1 [/ | ] to receive it" bob ##> "/fr 1 encrypt=on ./tests/tmp/bob/" 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" bob <## "started 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) LB.length dest `shouldBe` fromIntegral srcLen LB.toStrict dest `shouldBe` src diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs index 4c31805265..7a8b1368bf 100644 --- a/tests/ChatTests/Groups.hs +++ b/tests/ChatTests/Groups.hs @@ -2290,6 +2290,11 @@ testPlanGroupLinkOkKnown = bob <## "group link: known group #team" bob <## "use #team to send messages" + let gLinkSchema2 = linkAnotherSchema gLink + bob ##> ("/_connect plan 1 " <> gLinkSchema2) + bob <## "group link: known group #team" + bob <## "use #team to send messages" + bob ##> ("/c " <> gLink) bob <## "group link: known group #team" bob <## "use #team to send messages" @@ -2331,6 +2336,11 @@ testPlanHostContactDeletedGroupLinkKnown = bob <## "group link: known group #team" bob <## "use #team to send messages" + let gLinkSchema2 = linkAnotherSchema gLink + bob ##> ("/_connect plan 1 " <> gLinkSchema2) + bob <## "group link: known group #team" + bob <## "use #team to send messages" + bob ##> ("/c " <> gLink) bob <## "group link: known group #team" bob <## "use #team to send messages" @@ -2347,6 +2357,10 @@ testPlanGroupLinkOwn tmp = alice ##> ("/_connect plan 1 " <> gLink) alice <## "group link: own link for group #team" + let gLinkSchema2 = linkAnotherSchema gLink + alice ##> ("/_connect plan 1 " <> gLinkSchema2) + alice <## "group link: own link for group #team" + alice ##> ("/c " <> gLink) alice <## "connection request sent!" alice <## "alice_1 (Alice): accepting request to join group #team..." @@ -2373,6 +2387,9 @@ testPlanGroupLinkOwn tmp = alice ##> ("/_connect plan 1 " <> gLink) alice <## "group link: own link for group #team" + alice ##> ("/_connect plan 1 " <> gLinkSchema2) + alice <## "group link: own link for group #team" + -- group works if merged contact is deleted alice ##> "/d alice_1" alice <## "alice_1: contact is deleted" @@ -2397,8 +2414,19 @@ testPlanGroupLinkConnecting tmp = do alice ##> "/create link #team" getGroupLink alice "team" GRMember True withNewTestChat tmp "bob" bobProfile $ \bob -> do + threadDelay 100000 + bob ##> ("/c " <> gLink) bob <## "connection request sent!" + + bob ##> ("/_connect plan 1 " <> gLink) + bob <## "group link: connecting, allowed to reconnect" + + let gLinkSchema2 = linkAnotherSchema gLink + bob ##> ("/_connect plan 1 " <> gLinkSchema2) + bob <## "group link: connecting, allowed to reconnect" + + threadDelay 100000 withTestChat tmp "alice" $ \alice -> do alice <### [ "1 group links active", @@ -2410,6 +2438,10 @@ testPlanGroupLinkConnecting tmp = do bob ##> ("/_connect plan 1 " <> gLink) bob <## "group link: connecting" + let gLinkSchema2 = linkAnotherSchema gLink + bob ##> ("/_connect plan 1 " <> gLinkSchema2) + bob <## "group link: connecting" + bob ##> ("/c " <> gLink) bob <## "group link: connecting" @@ -2455,6 +2487,10 @@ testPlanGroupLinkLeaveRejoin = bob ##> ("/_connect plan 1 " <> gLink) bob <## "group link: ok to connect" + let gLinkSchema2 = linkAnotherSchema gLink + bob ##> ("/_connect plan 1 " <> gLinkSchema2) + bob <## "group link: ok to connect" + bob ##> ("/c " <> gLink) bob <## "connection request sent!" alice <## "bob_1 (Bob): accepting request to join group #team..." @@ -2483,6 +2519,10 @@ testPlanGroupLinkLeaveRejoin = bob <## "group link: known group #team_1" bob <## "use #team_1 to send messages" + bob ##> ("/_connect plan 1 " <> gLinkSchema2) + bob <## "group link: known group #team_1" + bob <## "use #team_1 to send messages" + bob ##> ("/c " <> gLink) bob <## "group link: known group #team_1" bob <## "use #team_1 to send messages" diff --git a/tests/ChatTests/Profiles.hs b/tests/ChatTests/Profiles.hs index 80e1709222..68b925342e 100644 --- a/tests/ChatTests/Profiles.hs +++ b/tests/ChatTests/Profiles.hs @@ -599,6 +599,11 @@ testPlanAddressOkKnown = bob <## "contact address: known contact alice" bob <## "use @alice to send messages" + let cLinkSchema2 = linkAnotherSchema cLink + bob ##> ("/_connect plan 1 " <> cLinkSchema2) + bob <## "contact address: known contact alice" + bob <## "use @alice to send messages" + bob ##> ("/c " <> cLink) bob <## "contact address: known contact alice" bob <## "use @alice to send messages" @@ -612,11 +617,15 @@ testPlanAddressOwn tmp = alice ##> ("/_connect plan 1 " <> cLink) alice <## "contact address: own address" + let cLinkSchema2 = linkAnotherSchema cLink + alice ##> ("/_connect plan 1 " <> cLinkSchema2) + alice <## "contact address: own address" + alice ##> ("/c " <> cLink) alice <## "connection request sent!" alice <## "alice_1 (Alice) wants to connect to you!" alice <## "to accept: /ac alice_1" - alice <## ("to reject: /rc alice_1 (the sender will NOT be notified)") + alice <## "to reject: /rc alice_1 (the sender will NOT be notified)" alice @@@ [("<@alice_1", ""), (":2","")] alice ##> "/ac alice_1" alice <## "alice_1 (Alice): accepting contact request..." @@ -651,8 +660,17 @@ testPlanAddressConnecting tmp = do getContactLink alice True withNewTestChat tmp "bob" bobProfile $ \bob -> do threadDelay 100000 + bob ##> ("/c " <> cLink) bob <## "connection request sent!" + + bob ##> ("/_connect plan 1 " <> cLink) + bob <## "contact address: connecting, allowed to reconnect" + + let cLinkSchema2 = linkAnotherSchema cLink + bob ##> ("/_connect plan 1 " <> cLinkSchema2) + bob <## "contact address: connecting, allowed to reconnect" + threadDelay 100000 withTestChat tmp "alice" $ \alice -> do alice <## "Your address is active! To show: /sa" @@ -667,6 +685,10 @@ testPlanAddressConnecting tmp = do bob ##> ("/_connect plan 1 " <> cLink) bob <## "contact address: connecting to contact alice" + let cLinkSchema2 = linkAnotherSchema cLink + bob ##> ("/_connect plan 1 " <> cLinkSchema2) + bob <## "contact address: connecting to contact alice" + bob ##> ("/c " <> cLink) bob <## "contact address: connecting to contact alice" @@ -701,6 +723,10 @@ testPlanAddressContactDeletedReconnected = bob ##> ("/_connect plan 1 " <> cLink) bob <## "contact address: ok to connect" + let cLinkSchema2 = linkAnotherSchema cLink + bob ##> ("/_connect plan 1 " <> cLinkSchema2) + bob <## "contact address: ok to connect" + bob ##> ("/c " <> cLink) bob <## "connection request sent!" alice <## "bob (Bob) wants to connect to you!" @@ -721,6 +747,10 @@ testPlanAddressContactDeletedReconnected = bob <## "contact address: known contact alice_1" bob <## "use @alice_1 to send messages" + bob ##> ("/_connect plan 1 " <> cLinkSchema2) + bob <## "contact address: known contact alice_1" + bob <## "use @alice_1 to send messages" + bob ##> ("/c " <> cLink) bob <## "contact address: known contact alice_1" bob <## "use @alice_1 to send messages" diff --git a/tests/ChatTests/Utils.hs b/tests/ChatTests/Utils.hs index b881961ec2..6a69eb3c55 100644 --- a/tests/ChatTests/Utils.hs +++ b/tests/ChatTests/Utils.hs @@ -562,3 +562,11 @@ currentChatVRangeInfo = vRangeStr :: VersionRange -> String vRangeStr (VersionRange minVer maxVer) = "(" <> show minVer <> ", " <> show maxVer <> ")" + +linkAnotherSchema :: String -> String +linkAnotherSchema link + | "https://simplex.chat/" `isPrefixOf` link = + T.unpack $ T.replace "https://simplex.chat/" "simplex:/" $ T.pack link + | "simplex:/" `isPrefixOf` link = + T.unpack $ T.replace "simplex:/" "https://simplex.chat/" $ T.pack link + | otherwise = error "link starts with neither https://simplex.chat/ nor simplex:/" diff --git a/tests/MarkdownTests.hs b/tests/MarkdownTests.hs index 837849d7e4..83a180c745 100644 --- a/tests/MarkdownTests.hs +++ b/tests/MarkdownTests.hs @@ -137,8 +137,8 @@ textColor = describe "text color (red)" do uri :: Text -> Markdown uri = Markdown $ Just Uri -simplexLink :: SimplexLinkType -> Text -> Bool -> NonEmpty Text -> Text -> Markdown -simplexLink linkType simplexUri trustedUri smpHosts = Markdown $ Just SimplexLink {linkType, simplexUri, trustedUri, smpHosts} +simplexLink :: SimplexLinkType -> Text -> NonEmpty Text -> Text -> Markdown +simplexLink linkType simplexUri smpHosts = Markdown $ Just SimplexLink {linkType, simplexUri, smpHosts} textWithUri :: Spec textWithUri = describe "text with Uri" do @@ -152,13 +152,13 @@ textWithUri = describe "text with Uri" do parseMarkdown "this is _https://simplex.chat" `shouldBe` "this is _https://simplex.chat" it "SimpleX links" do let inv = "/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D" - parseMarkdown ("https://simplex.chat" <> inv) `shouldBe` simplexLink XLInvitation ("simplex:" <> inv) True ["smp.simplex.im"] ("https://simplex.chat" <> inv) - parseMarkdown ("simplex:" <> inv) `shouldBe` simplexLink XLInvitation ("simplex:" <> inv) True ["smp.simplex.im"] ("simplex:" <> inv) - parseMarkdown ("https://example.com" <> inv) `shouldBe` simplexLink XLInvitation ("simplex:" <> inv) False ["smp.simplex.im"] ("https://example.com" <> inv) + parseMarkdown ("https://simplex.chat" <> inv) `shouldBe` simplexLink XLInvitation ("simplex:" <> inv) ["smp.simplex.im"] ("https://simplex.chat" <> inv) + parseMarkdown ("simplex:" <> inv) `shouldBe` simplexLink XLInvitation ("simplex:" <> inv) ["smp.simplex.im"] ("simplex:" <> inv) + parseMarkdown ("https://example.com" <> inv) `shouldBe` simplexLink XLInvitation ("simplex:" <> inv) ["smp.simplex.im"] ("https://example.com" <> inv) let ct = "/contact#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D" - parseMarkdown ("https://simplex.chat" <> ct) `shouldBe` simplexLink XLContact ("simplex:" <> ct) True ["smp.simplex.im"] ("https://simplex.chat" <> ct) + parseMarkdown ("https://simplex.chat" <> ct) `shouldBe` simplexLink XLContact ("simplex:" <> ct) ["smp.simplex.im"] ("https://simplex.chat" <> ct) let gr = "/contact#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FWHV0YU1sYlU7NqiEHkHDB6gxO1ofTync%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAWbebOqVYuBXaiqHcXYjEHCpYi6VzDlu6CVaijDTmsQU%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22mL-7Divb94GGmGmRBef5Dg%3D%3D%22%7D" - parseMarkdown ("https://simplex.chat" <> gr) `shouldBe` simplexLink XLGroup ("simplex:" <> gr) True ["smp4.simplex.im", "o5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion"] ("https://simplex.chat" <> gr) + parseMarkdown ("https://simplex.chat" <> gr) `shouldBe` simplexLink XLGroup ("simplex:" <> gr) ["smp4.simplex.im", "o5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion"] ("https://simplex.chat" <> gr) email :: Text -> Markdown email = Markdown $ Just Email diff --git a/tests/ProtocolTests.hs b/tests/ProtocolTests.hs index d62d7a470a..0b99c5a4d8 100644 --- a/tests/ProtocolTests.hs +++ b/tests/ProtocolTests.hs @@ -39,7 +39,7 @@ queue = connReqData :: ConnReqUriData connReqData = ConnReqUriData - { crScheme = simplexChat, + { crScheme = CRSSimplex, crAgentVRange = mkVersionRange 1 1, crSmpQueues = [queue], crClientData = Nothing @@ -184,7 +184,7 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do "{\"v\":\"1\",\"event\":\"x.msg.deleted\",\"params\":{}}" #==# XMsgDeleted it "x.file" $ - "{\"v\":\"1\",\"event\":\"x.file\",\"params\":{\"file\":{\"fileConnReq\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"fileSize\":12345,\"fileName\":\"photo.jpg\"}}}" + "{\"v\":\"1\",\"event\":\"x.file\",\"params\":{\"file\":{\"fileConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"fileSize\":12345,\"fileName\":\"photo.jpg\"}}}" #==# XFile FileInvitation {fileName = "photo.jpg", fileSize = 12345, fileDigest = Nothing, fileConnReq = Just testConnReq, fileInline = Nothing, fileDescr = Nothing} it "x.file without file invitation" $ "{\"v\":\"1\",\"event\":\"x.file\",\"params\":{\"file\":{\"fileSize\":12345,\"fileName\":\"photo.jpg\"}}}" @@ -193,7 +193,7 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do "{\"v\":\"1\",\"event\":\"x.file.acpt\",\"params\":{\"fileName\":\"photo.jpg\"}}" #==# XFileAcpt "photo.jpg" it "x.file.acpt.inv" $ - "{\"v\":\"1\",\"event\":\"x.file.acpt.inv\",\"params\":{\"msgId\":\"AQIDBA==\",\"fileName\":\"photo.jpg\",\"fileConnReq\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}" + "{\"v\":\"1\",\"event\":\"x.file.acpt.inv\",\"params\":{\"msgId\":\"AQIDBA==\",\"fileName\":\"photo.jpg\",\"fileConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}" #==# XFileAcptInv (SharedMsgId "\1\2\3\4") (Just testConnReq) "photo.jpg" it "x.file.acpt.inv" $ "{\"v\":\"1\",\"event\":\"x.file.acpt.inv\",\"params\":{\"msgId\":\"AQIDBA==\",\"fileName\":\"photo.jpg\"}}" @@ -220,10 +220,10 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do "{\"v\":\"1\",\"event\":\"x.contact\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}" ==# XContact testProfile Nothing it "x.grp.inv" $ - "{\"v\":\"1\",\"event\":\"x.grp.inv\",\"params\":{\"groupInvitation\":{\"connRequest\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"invitedMember\":{\"memberRole\":\"member\",\"memberId\":\"BQYHCA==\"},\"groupProfile\":{\"fullName\":\"Team\",\"displayName\":\"team\",\"groupPreferences\":{\"reactions\":{\"enable\":\"on\"},\"voice\":{\"enable\":\"on\"}}},\"fromMember\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\"}}}}" + "{\"v\":\"1\",\"event\":\"x.grp.inv\",\"params\":{\"groupInvitation\":{\"connRequest\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"invitedMember\":{\"memberRole\":\"member\",\"memberId\":\"BQYHCA==\"},\"groupProfile\":{\"fullName\":\"Team\",\"displayName\":\"team\",\"groupPreferences\":{\"reactions\":{\"enable\":\"on\"},\"voice\":{\"enable\":\"on\"}}},\"fromMember\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\"}}}}" #==# XGrpInv GroupInvitation {fromMember = MemberIdRole (MemberId "\1\2\3\4") GRAdmin, invitedMember = MemberIdRole (MemberId "\5\6\7\8") GRMember, connRequest = testConnReq, groupProfile = testGroupProfile, groupLinkId = Nothing} it "x.grp.inv with group link id" $ - "{\"v\":\"1\",\"event\":\"x.grp.inv\",\"params\":{\"groupInvitation\":{\"connRequest\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"invitedMember\":{\"memberRole\":\"member\",\"memberId\":\"BQYHCA==\"},\"groupProfile\":{\"fullName\":\"Team\",\"displayName\":\"team\",\"groupPreferences\":{\"reactions\":{\"enable\":\"on\"},\"voice\":{\"enable\":\"on\"}}},\"fromMember\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\"}, \"groupLinkId\":\"AQIDBA==\"}}}" + "{\"v\":\"1\",\"event\":\"x.grp.inv\",\"params\":{\"groupInvitation\":{\"connRequest\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"invitedMember\":{\"memberRole\":\"member\",\"memberId\":\"BQYHCA==\"},\"groupProfile\":{\"fullName\":\"Team\",\"displayName\":\"team\",\"groupPreferences\":{\"reactions\":{\"enable\":\"on\"},\"voice\":{\"enable\":\"on\"}}},\"fromMember\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\"}, \"groupLinkId\":\"AQIDBA==\"}}}" #==# XGrpInv GroupInvitation {fromMember = MemberIdRole (MemberId "\1\2\3\4") GRAdmin, invitedMember = MemberIdRole (MemberId "\5\6\7\8") GRMember, connRequest = testConnReq, groupProfile = testGroupProfile, groupLinkId = Just $ GroupLinkId "\1\2\3\4"} it "x.grp.acpt without incognito profile" $ "{\"v\":\"1\",\"event\":\"x.grp.acpt\",\"params\":{\"memberId\":\"AQIDBA==\"}}" @@ -241,16 +241,16 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do "{\"v\":\"1\",\"event\":\"x.grp.mem.intro\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-2\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" #==# XGrpMemIntro MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Just $ ChatVersionRange supportedChatVRange, profile = testProfile} it "x.grp.mem.inv" $ - "{\"v\":\"1\",\"event\":\"x.grp.mem.inv\",\"params\":{\"memberId\":\"AQIDBA==\",\"memberIntro\":{\"directConnReq\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"groupConnReq\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}}" + "{\"v\":\"1\",\"event\":\"x.grp.mem.inv\",\"params\":{\"memberId\":\"AQIDBA==\",\"memberIntro\":{\"directConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}}" #==# XGrpMemInv (MemberId "\1\2\3\4") IntroInvitation {groupConnReq = testConnReq, directConnReq = Just testConnReq} it "x.grp.mem.inv w/t directConnReq" $ - "{\"v\":\"1\",\"event\":\"x.grp.mem.inv\",\"params\":{\"memberId\":\"AQIDBA==\",\"memberIntro\":{\"groupConnReq\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}}" + "{\"v\":\"1\",\"event\":\"x.grp.mem.inv\",\"params\":{\"memberId\":\"AQIDBA==\",\"memberIntro\":{\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}}" #==# XGrpMemInv (MemberId "\1\2\3\4") IntroInvitation {groupConnReq = testConnReq, directConnReq = Nothing} it "x.grp.mem.fwd" $ - "{\"v\":\"1\",\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"directConnReq\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"groupConnReq\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" + "{\"v\":\"1\",\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"directConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" #==# XGrpMemFwd MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Nothing, profile = testProfile} IntroInvitation {groupConnReq = testConnReq, directConnReq = Just testConnReq} it "x.grp.mem.fwd with member chat version range and w/t directConnReq" $ - "{\"v\":\"1\",\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"groupConnReq\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-2\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" + "{\"v\":\"1\",\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-2\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" #==# XGrpMemFwd MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Just $ ChatVersionRange supportedChatVRange, profile = testProfile} IntroInvitation {groupConnReq = testConnReq, directConnReq = Nothing} it "x.grp.mem.info" $ "{\"v\":\"1\",\"event\":\"x.grp.mem.info\",\"params\":{\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}" @@ -271,10 +271,10 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do "{\"v\":\"1\",\"event\":\"x.grp.del\",\"params\":{}}" ==# XGrpDel it "x.grp.direct.inv" $ - "{\"v\":\"1\",\"event\":\"x.grp.direct.inv\",\"params\":{\"connReq\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\", \"content\":{\"text\":\"hello\",\"type\":\"text\"}}}" + "{\"v\":\"1\",\"event\":\"x.grp.direct.inv\",\"params\":{\"connReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\", \"content\":{\"text\":\"hello\",\"type\":\"text\"}}}" #==# XGrpDirectInv testConnReq (Just $ MCText "hello") it "x.grp.direct.inv without content" $ - "{\"v\":\"1\",\"event\":\"x.grp.direct.inv\",\"params\":{\"connReq\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}" + "{\"v\":\"1\",\"event\":\"x.grp.direct.inv\",\"params\":{\"connReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}" #==# XGrpDirectInv testConnReq Nothing it "x.info.probe" $ "{\"v\":\"1\",\"event\":\"x.info.probe\",\"params\":{\"probe\":\"AQIDBA==\"}}" diff --git a/website/src/_includes/contact_page.html b/website/src/_includes/contact_page.html index a03a3aea4f..6beb148f8d 100644 --- a/website/src/_includes/contact_page.html +++ b/website/src/_includes/contact_page.html @@ -152,7 +152,7 @@ v1.0.0+, {{ "copy-the-command-below-text" | i18n({}, lang ) | safe }}

- /c https://simplex.chat/contact#/?v=1&smp=smp://u2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU=@smp4.simplex.im/KBCmxJ3-lEjpWLPPkI6OWPk-YJneU5uY%23MCowBQYDK2VuAyEAtixHJWDXvYWcoe-77vIfjvI6XWEuzUsapMS9nVHP_Go= +