diff --git a/apps/ios/Shared/Model/AppAPITypes.swift b/apps/ios/Shared/Model/AppAPITypes.swift index a0e93340d9..64fba806a9 100644 --- a/apps/ios/Shared/Model/AppAPITypes.swift +++ b/apps/ios/Shared/Model/AppAPITypes.swift @@ -1420,7 +1420,6 @@ struct UserContactLink: Decodable, Hashable { struct AddressSettings: Codable, Hashable { var businessAddress: Bool - var welcomeMessage: String? var autoAccept: AutoAccept? var autoReply: MsgContent? } diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift index 08fcf402e1..5894a9c923 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift @@ -701,6 +701,7 @@ struct ComposeView: View { private func sendMemberContactInvitation() { Task { do { + await MainActor.run { hideKeyboard() } if let mc = connectCheckLinkPreview() { await sending() let contact = try await apiSendMemberContactInvitation(chat.chatInfo.apiId, mc) @@ -733,13 +734,14 @@ struct ComposeView: View { ), secondaryButton: empty - ? .cancel(Text("Add message")) { keyboardVisible = true } + ? .cancel(Text("Add message"), action: hideKeyboard) : .cancel() )) } private func sendConnectPreparedContact() { Task { + await MainActor.run { hideKeyboard() } await sending() let mc = connectCheckLinkPreview() if let contact = await apiConnectPreparedContact(contactId: chat.chatInfo.apiId, incognito: incognitoGroupDefault.get(), msg: mc) { @@ -756,6 +758,7 @@ struct ComposeView: View { private func connectPreparedGroup() { Task { + await MainActor.run { hideKeyboard() } await sending() let mc = connectCheckLinkPreview() if let groupInfo = await apiConnectPreparedGroup(groupId: chat.chatInfo.apiId, incognito: incognitoGroupDefault.get(), msg: mc) { diff --git a/apps/ios/Shared/Views/UserSettings/UserAddressView.swift b/apps/ios/Shared/Views/UserSettings/UserAddressView.swift index 5d86c472ed..9f1377e4d5 100644 --- a/apps/ios/Shared/Views/UserSettings/UserAddressView.swift +++ b/apps/ios/Shared/Views/UserSettings/UserAddressView.swift @@ -371,7 +371,6 @@ struct ToggleShortLinkHeader: View { struct AddressSettingsState: Equatable { var businessAddress = false - var welcomeMessage = "" var autoAccept = false var autoAcceptIncognito = false var autoReply = "" @@ -380,7 +379,6 @@ struct AddressSettingsState: Equatable { init(settings: AddressSettings) { self.businessAddress = settings.businessAddress - self.welcomeMessage = settings.welcomeMessage ?? "" self.autoAccept = settings.autoAccept != nil self.autoAcceptIncognito = settings.autoAccept?.acceptIncognito == true self.autoReply = settings.autoReply?.text ?? "" @@ -389,7 +387,6 @@ struct AddressSettingsState: Equatable { var addressSettings: AddressSettings { AddressSettings( businessAddress: self.businessAddress, - welcomeMessage: self.welcomeMessage.isEmpty ? nil : self.welcomeMessage, autoAccept: self.autoAccept ? AutoAccept(acceptIncognito: self.autoAcceptIncognito) : nil, autoReply: self.autoReply.isEmpty ? nil : MsgContent.text(self.autoReply) ) @@ -463,14 +460,8 @@ struct UserAddressSettingsView: View { autoAcceptToggle().disabled(settings.businessAddress) } - Section { - messageEditor(placeholder: NSLocalizedString("Enter welcome message… (optional)", comment: "placeholder"), text: $settings.welcomeMessage) - } header: { - Text("Welcome message") - .foregroundColor(theme.colors.secondary) - } footer: { - Text("Shown to your contact before connection.") - } + // TODO v6.4.1 move auto-reply editor here + messageEditor(placeholder: NSLocalizedString("Enter welcome message… (optional)", comment: "placeholder"), text: $settings.autoReply) if settings.autoAccept { autoAcceptSection() @@ -554,7 +545,8 @@ struct UserAddressSettingsView: View { if !ChatModel.shared.addressShortLinkDataSet && !settings.businessAddress { acceptIncognitoToggle() } - messageEditor(placeholder: NSLocalizedString("Enter auto-reply message… (optional)", comment: "placeholder"), text: $settings.autoReply) + // TODO v6.4.1 show this message editor even with auto-accept disabled + messageEditor(placeholder: NSLocalizedString("Enter welcome message… (optional)", comment: "placeholder"), text: $settings.autoReply) } header: { Text("Auto-accept") .foregroundColor(theme.colors.secondary) diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 9858b92350..c1ea3946a8 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -2217,7 +2217,7 @@ public enum MemberCriteria: String, Codable, Identifiable, Hashable { public struct ContactShortLinkData: Codable, Hashable { public var profile: Profile - public var message: String? + public var message: MsgContent? public var business: Bool } 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 a893c50de1..8ead5c2292 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 @@ -521,7 +521,7 @@ object ChatModel { } } } - + private fun chatItemBelongsToScope(cInfo: ChatInfo, cItem: ChatItem): Boolean = when (secondaryContextFilter) { null -> @@ -2063,7 +2063,7 @@ enum class MemberCriteria { @Serializable data class ContactShortLinkData ( val profile: Profile, - val message: String?, + val message: MsgContent?, val business: Boolean ) diff --git a/src/Simplex/Chat/Bot.hs b/src/Simplex/Chat/Bot.hs index c2e2d0c3a3..ff14dae6db 100644 --- a/src/Simplex/Chat/Bot.hs +++ b/src/Simplex/Chat/Bot.hs @@ -64,7 +64,7 @@ initializeBotAddress' logAddress cc = do when logAddress $ do putStrLn $ "Bot's contact address is: " <> B.unpack (maybe (strEncode uri) strEncode shortUri) when (isJust shortUri) $ putStrLn $ "Full contact address for old clients: " <> B.unpack (strEncode uri) - let settings = AddressSettings {businessAddress = False, welcomeMessage = Nothing, autoAccept = Just AutoAccept {acceptIncognito = False}, autoReply = Nothing} + let settings = AddressSettings {businessAddress = False, autoAccept = Just AutoAccept {acceptIncognito = False}, autoReply = Nothing} void $ sendChatCmd cc $ SetAddressSettings settings sendMessage :: ChatController -> Contact -> Text -> IO () diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index c5dafe4a8c..334330192a 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -72,6 +72,7 @@ import Simplex.Chat.Library.Internal import Simplex.Chat.Stats import Simplex.Chat.Store import Simplex.Chat.Store.AppSettings +import Simplex.Chat.Store.ContactRequest import Simplex.Chat.Store.Connections import Simplex.Chat.Store.Direct import Simplex.Chat.Store.Files @@ -1153,10 +1154,10 @@ processChatCommand' vr = \case uclId <- getUserContactLinkIdByCReq db connReqId uclGLinkInfo <- getUserContactLinkById db userId uclId pure (uclId, uclGLinkInfo) - let UserContactLink {shortLinkDataSet} = ucl + let UserContactLink {shortLinkDataSet, addressSettings} = ucl when (shortLinkDataSet && incognito) $ throwCmdError "incognito not allowed for address with short link data" withUserContactLock "acceptContact" uclId $ do - cReq <- withFastStore $ \db -> getContactRequest db user connReqId + cReq@UserContactRequest {welcomeSharedMsgId} <- withFastStore $ \db -> getContactRequest db user connReqId (ct, conn@Connection {connId}, sqSecured) <- acceptContactRequest user cReq incognito let contactUsed = isNothing gLinkInfo_ ct' <- withStore' $ \db -> do @@ -1166,6 +1167,13 @@ processChatCommand' vr = \case then conn {connStatus = ConnSndReady} <$ updateConnectionStatusFromTo db connId ConnNew ConnSndReady else pure conn pure ct {contactUsed, activeConn = Just conn'} + when sqSecured $ forM_ (autoReply addressSettings) $ \mc -> case welcomeSharedMsgId of + Just smId -> + void $ sendDirectContactMessage user ct' $ XMsgUpdate smId mc M.empty Nothing Nothing Nothing + Nothing -> do + (msg, _) <- sendDirectContactMessage user ct' $ XMsgNew $ MCSimple $ extMsgContent mc Nothing + ci <- saveSndChatItem user (CDDirectSnd ct') msg (CISndMsgContent mc) + toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDSnd (DirectChat ct') ci] pure $ CRAcceptingContactRequest user ct' APIRejectContact connReqId -> withUser $ \user -> do userContactLinkId <- withFastStore $ \db -> getUserContactLinkIdByCReq db connReqId @@ -1748,7 +1756,7 @@ processChatCommand' vr = \case createItem sharedMsgId content = createChatItem user cd True content sharedMsgId Nothing cInfo = GroupChat gInfo Nothing void $ createGroupFeatureItems_ user cd True CIRcvGroupFeature gInfo - aci <- mapM (createItem welcomeSharedMsgId . CIRcvMsgContent . MCText) message + aci <- mapM (createItem welcomeSharedMsgId . CIRcvMsgContent) message let chat = case aci of Just (AChatItem SCTGroup dir _ ci) -> Chat cInfo [CChatItem dir ci] emptyChatStats {unreadCount = 1, minUnreadItemId = chatItemId' ci} _ -> Chat cInfo [] emptyChatStats @@ -1759,19 +1767,19 @@ processChatCommand' vr = \case cInfo = DirectChat ct void $ createItem Nothing $ CIRcvDirectE2EEInfo $ E2EInfo $ connRequestPQEncryption cReq void $ createFeatureEnabledItems_ user ct - aci <- mapM (createItem welcomeSharedMsgId . CIRcvMsgContent . MCText) message + aci <- mapM (createItem welcomeSharedMsgId . CIRcvMsgContent) message let chat = case aci of Just (AChatItem SCTDirect dir _ ci) -> Chat cInfo [CChatItem dir ci] emptyChatStats {unreadCount = 1, minUnreadItemId = chatItemId' ci} _ -> Chat cInfo [] emptyChatStats pure $ CRNewPreparedChat user $ AChat SCTDirect chat APIPrepareGroup userId ccLink groupSLinkData -> withUserId userId $ \user -> do let GroupShortLinkData {groupProfile = gp@GroupProfile {description}} = groupSLinkData - (gInfo, hostMember) <- withStore $ \db -> createPreparedGroup db vr user gp False ccLink Nothing + welcomeSharedMsgId <- forM description $ \_ -> getSharedMsgId + (gInfo, hostMember) <- withStore $ \db -> createPreparedGroup db vr user gp False ccLink welcomeSharedMsgId let cd = CDGroupRcv gInfo Nothing hostMember - createItem content = createChatItem user cd True content Nothing Nothing cInfo = GroupChat gInfo Nothing void $ createGroupFeatureItems_ user cd True CIRcvGroupFeature gInfo - aci <- mapM (createItem . CIRcvMsgContent . MCText) description + aci <- forM description $ \descr -> createChatItem user cd True (CIRcvMsgContent $ MCText descr) welcomeSharedMsgId Nothing let chat = case aci of Just (AChatItem SCTGroup dir _ ci) -> Chat cInfo [CChatItem dir ci] emptyChatStats {unreadCount = 1, minUnreadItemId = chatItemId' ci} _ -> Chat cInfo [] emptyChatStats @@ -1804,9 +1812,13 @@ processChatCommand' vr = \case ci <- saveSndChatItem user (CDDirectSnd ct') msg (CISndMsgContent mc) toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDSnd (DirectChat ct') ci] pure $ CRStartedConnectionToContact user ct' customUserProfile - Just PreparedContact {connLinkToConnect = ACCL SCMContact ccLink, welcomeSharedMsgId} -> do - -- TODO [short links] reuse welcomeSharedMsgId - msg_ <- forM msgContent_ $ \mc -> (,mc) <$> getSharedMsgId + Just PreparedContact {connLinkToConnect = ACCL SCMContact ccLink, welcomeSharedMsgId, requestSharedMsgId} -> do + msg_ <- forM msgContent_ $ \mc -> case requestSharedMsgId of + Just smId -> pure (smId, mc) + Nothing -> do + smId <- getSharedMsgId + withFastStore' $ \db -> setRequestSharedMsgIdForContact db contactId smId + pure (smId, mc) connectViaContact user incognito ccLink welcomeSharedMsgId msg_ (Just $ ACCGContact contactId) >>= \case CRSentInvitation {customUserProfile} -> do -- get updated contact with connection @@ -1820,12 +1832,22 @@ processChatCommand' vr = \case (gInfo, hostMember) <- withFastStore $ \db -> (,) <$> getGroupInfo db vr user groupId <*> getHostMember db vr user groupId case preparedGroup gInfo of Nothing -> throwCmdError "group doesn't have link to connect" - Just PreparedGroup {connLinkToConnect} -> - -- TODO [short links] store request message with shared message ID (for business chat) - connectViaContact user incognito connLinkToConnect Nothing Nothing (Just $ ACCGGroup gInfo (groupMemberId' hostMember)) >>= \case + Just PreparedGroup {connLinkToConnect, welcomeSharedMsgId, requestSharedMsgId} -> do + msg_ <- forM msgContent_ $ \mc -> case requestSharedMsgId of + Just smId -> pure (smId, mc) + Nothing -> do + smId <- getSharedMsgId + withFastStore' $ \db -> setRequestSharedMsgIdForGroup db groupId smId + pure (smId, mc) + connectViaContact user incognito connLinkToConnect welcomeSharedMsgId msg_ (Just $ ACCGGroup gInfo $ groupMemberId' hostMember) >>= \case CRSentInvitation {customUserProfile} -> do -- get updated group info (connLinkStartedConnection and incognito membership) - gInfo' <- withFastStore $ \db -> getGroupInfo db vr user groupId + gInfo' <- withFastStore $ \db -> do + liftIO $ setPreparedGroupStartedConnection db groupId + getGroupInfo db vr user groupId + forM_ msg_ $ \(sharedMsgId, mc) -> do + ci <- createChatItem user (CDGroupSnd gInfo' Nothing) False (CISndMsgContent mc) (Just sharedMsgId) Nothing + toView $ CEvtNewChatItems user [ci] pure $ CRStartedConnectionToGroup user gInfo' customUserProfile cr -> pure cr APIConnect userId incognito acl -> withUserId userId $ \user -> case acl of @@ -2872,6 +2894,10 @@ processChatCommand' vr = \case withInvitationLock "connect" (strEncode cReq) $ do subMode <- chatReadVar subscriptionMode -- [incognito] generate profile to send + -- TODO [short links] if incognito profile was prepared on the previous attempt, it should be used instead of creating a new one + -- TODO [short links] for connection via prepared contacts we need to: + -- - potentially use different flow or pass contact as parameter here, + -- - prohibit changing user/incognito on the second attempt in the UI incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing let profileToSend = userProfileToSend user incognitoProfile Nothing False lift (withAgent' $ \a -> connRequestPQSupport a PQSupportOn cReq) >>= \case @@ -2903,31 +2929,84 @@ processChatCommand' vr = \case CRInvitationUri crData {crScheme = simplexChat} e2e ) connectViaContact :: User -> IncognitoEnabled -> CreatedLinkContact -> Maybe SharedMsgId -> Maybe (SharedMsgId, MsgContent) -> Maybe AttachConnToContactOrGroup -> CM ChatResponse - connectViaContact user@User {userId} incognito (CCLink cReq@(CRContactUri ConnReqUriData {crClientData}) sLnk) welcomeSharedMsgId msg_ attachConnTo_ = withInvitationLock "connectViaContact" (strEncode cReq) $ do + connectViaContact user@User {userId} incognito (CCLink cReq@(CRContactUri crData@ConnReqUriData {crClientData}) sLnk) welcomeSharedMsgId msg_ attachConnTo_ = withInvitationLock "connectViaContact" (strEncode cReq) $ do let groupLinkId = crClientData >>= decodeJSON >>= \(CRDataGroup gli) -> Just gli - cReqHash = ConnReqUriHash . C.sha256Hash $ strEncode cReq + cReqHash = ConnReqUriHash . C.sha256Hash . strEncode + cReqHash1 = cReqHash $ CRContactUri crData {crScheme = SSSimplex} + cReqHash2 = cReqHash $ CRContactUri crData {crScheme = simplexChat} case groupLinkId of -- contact address Nothing -> - withFastStore' (\db -> getConnReqContactXContactId db vr user cReqHash) >>= \case + withFastStore' (\db -> getConnReqContactXContactId db vr user cReqHash1 cReqHash2) >>= \case + (Just Contact {activeConn = Just conn@Connection {connStatus = ConnPrepared}}, Just (xContactId, _)) -> do + incognitoProfile <- joinPreparedConn' xContactId conn + pure $ CRSentInvitation user (toPCC conn) incognitoProfile (Just contact, _) -> pure $ CRContactAlreadyExists user contact - (_, xContactId_) -> procCmd $ do - let randomXContactId = XContactId <$> drgRandomBytes 16 - xContactId <- maybe randomXContactId pure xContactId_ - connect' Nothing cReqHash xContactId False + (Nothing, Just (xContactId, Just conn@Connection {connId, connStatus = ConnPrepared})) -> do + incognitoProfile <- joinPreparedConn' xContactId conn + -- TODO [short links] align Connection and PendingContactConnection so it doesn't need to be read again + pcc <- withStore $ \db -> getPendingContactConnection db userId connId + pure $ CRSentInvitation user pcc incognitoProfile + (Nothing, xContactId_) -> procCmd $ do + -- TODO [short links] this is executed on repeat request after success + -- it probably should send the second message without creating the second connection? + xContactId <- mkXContactId $ fst <$> xContactId_ + connect' Nothing cReqHash1 xContactId False -- group link Just gLinkId -> do + -- TODO [short links] reset "connection started" column when (isJust msg_) $ throwChatError CEConnReqMessageProhibited - withFastStore' (\db -> getConnReqContactXContactId db vr user cReqHash) >>= \case + withFastStore' (\db -> getConnReqContactXContactId db vr user cReqHash1 cReqHash2) >>= \case (Just _contact, _) -> procCmd $ do -- allow repeat contact request + -- TODO [short links] is this branch needed? it probably remained from the time we created host contact newXContactId <- XContactId <$> drgRandomBytes 16 - connect' (Just gLinkId) cReqHash newXContactId True + connect' (Just gLinkId) cReqHash1 newXContactId True + (Nothing, Just (xContactId, Just conn@Connection {connId, connStatus = ConnPrepared})) -> do + incognitoProfile <- joinPreparedConn' xContactId conn + pure $ CRSentInvitation user (toPCC conn) incognitoProfile (_, xContactId_) -> procCmd $ do - let randomXContactId = XContactId <$> drgRandomBytes 16 - xContactId <- maybe randomXContactId pure xContactId_ - connect' (Just gLinkId) cReqHash xContactId True + -- TODO [short links] this is executed on repeat request after success + -- it probably should send the second message without creating the second connection? + xContactId <- mkXContactId $ fst <$> xContactId_ + connect' (Just gLinkId) cReqHash1 xContactId True where + joinPreparedConn' xContactId conn@Connection {connId = dbConnId, agentConnId = AgentConnId connId, connChatVersion = chatV, customUserProfileId = pId_} = do + incognitoProfile <- getOrCreateIncognitoProfile + joinContact user dbConnId connId cReq incognitoProfile xContactId welcomeSharedMsgId msg_ False PQSupportOn chatV + pure incognitoProfile + where + getOrCreateIncognitoProfile + | incognito = + withStore' $ \db -> case pId_ of + Nothing -> newIncognitoProfile db + Just pId -> + runExceptT (getProfileById db userId pId) + >>= either (\_ -> newIncognitoProfile db) (pure . Just . fromLocalProfile) + | otherwise = do + when (isJust pId_) $ withStore' $ \db -> + deleteIncognitoConnectionProfile db userId conn + pure Nothing + newIncognitoProfile db = do + p <- generateRandomProfile + createdAt <- liftIO getCurrentTime + createIncognitoProfile_ db userId createdAt p + pure $ Just p + toPCC Connection {connId, agentConnId, connStatus, viaUserContactLink, groupLinkId, customUserProfileId, createdAt} = + PendingContactConnection + { pccConnId = connId, + pccAgentConnId = agentConnId, + pccConnStatus = connStatus, + viaContactUri = True, + viaUserContactLink, + groupLinkId, + customUserProfileId, + connLinkInv = Nothing, + localAlias = "", + createdAt, + updatedAt = createdAt + } + mkXContactId = maybe (XContactId <$> drgRandomBytes 16) pure connect' groupLinkId cReqHash xContactId inGroup = do let pqSup = if inGroup then PQSupportOff else PQSupportOn (connId, chatV) <- prepareContact user cReq pqSup @@ -2965,17 +3044,10 @@ processChatCommand' vr = \case joinContact :: User -> Int64 -> ConnId -> ConnReqContact -> Maybe Profile -> XContactId -> Maybe SharedMsgId -> Maybe (SharedMsgId, MsgContent) -> Bool -> PQSupport -> VersionChat -> CM () joinContact user pccConnId connId cReq incognitoProfile xContactId welcomeSharedMsgId msg_ inGroup pqSup chatV = do let profileToSend = userProfileToSend user incognitoProfile Nothing inGroup - -- TODO [short links] send welcome and sent sharedMsg Ids dm <- encodeConnInfoPQ pqSup chatV (XContact profileToSend (Just xContactId) welcomeSharedMsgId msg_) subMode <- chatReadVar subscriptionMode - joinPreparedAgentConnection user pccConnId connId cReq dm pqSup subMode - joinPreparedAgentConnection :: User -> Int64 -> ConnId -> ConnectionRequestUri m -> ByteString -> PQSupport -> SubscriptionMode -> CM () - joinPreparedAgentConnection user pccConnId connId cReq connInfo pqSup subMode = do - void (withAgent $ \a -> joinConnection a (aUserId user) connId True cReq connInfo pqSup subMode) - `catchChatError` \e -> do - withFastStore' $ \db -> deleteConnectionRecord db user pccConnId - withAgent $ \a -> deleteConnectionAsync a False connId - throwError e + void $ withAgent $ \a -> joinConnection a (aUserId user) connId True cReq dm pqSup subMode + withFastStore' $ \db -> updateConnectionStatusFromTo db pccConnId ConnPrepared ConnJoined contactMember :: Contact -> [GroupMember] -> Maybe GroupMember contactMember Contact {contactId} = find $ \GroupMember {memberContactId = cId, memberStatus = s} -> @@ -3402,7 +3474,9 @@ processChatCommand' vr = \case withFastStore' (\db -> getContactWithoutConnViaAddress db vr user cReqSchemas) >>= \case Nothing -> pure $ CPContactAddress (CAPOk contactSLinkData_) Just ct -> pure $ CPContactAddress (CAPContactViaAddress ct) - Just (RcvDirectMsgConnection _conn Nothing) -> pure $ CPContactAddress CAPConnectingConfirmReconnect + Just (RcvDirectMsgConnection Connection {connStatus} Nothing) + | connStatus == ConnPrepared -> pure $ CPContactAddress (CAPOk contactSLinkData_) + | otherwise -> pure $ CPContactAddress CAPConnectingConfirmReconnect Just (RcvDirectMsgConnection _ (Just ct)) | not (contactReady ct) && contactActive ct -> pure $ CPContactAddress (CAPConnectingProhibit ct) | contactDeleted ct -> pure $ CPContactAddress (CAPOk contactSLinkData_) @@ -3461,7 +3535,7 @@ processChatCommand' vr = \case contactShortLinkData :: Profile -> Maybe AddressSettings -> CM UserLinkData contactShortLinkData p settings = do large <- chatReadVar useLargeLinkData - let msg = welcomeMessage =<< settings + let msg = autoReply =<< settings business = maybe False businessAddress settings contactData | large = ContactShortLinkData p msg business @@ -4769,10 +4843,10 @@ chatCommandP = dbEncryptionConfig currentKey newKey = DBEncryptionConfig {currentKey, newKey, keepKey = Just False} #endif -- TODO [short links] parser for address settings - autoAcceptP = ifM onOffP (businessAA <|> addressAA) (pure $ AddressSettings False Nothing Nothing Nothing) + autoAcceptP = ifM onOffP (businessAA <|> addressAA) (pure $ AddressSettings False Nothing Nothing) where - addressAA = AddressSettings False Nothing <$> (Just . AutoAccept <$> (" incognito=" *> onOffP <|> pure False)) <*> autoReply - businessAA = " business" *> (AddressSettings True Nothing (Just $ AutoAccept False) <$> autoReply) + addressAA = AddressSettings False <$> (Just . AutoAccept <$> (" incognito=" *> onOffP <|> pure False)) <*> autoReply + businessAA = " business" *> (AddressSettings True (Just $ AutoAccept False) <$> autoReply) autoReply = optional (A.space *> msgContentP) rcCtrlAddressP = RCCtrlAddress <$> ("addr=" *> strP) <*> (" iface=" *> (jsonP <|> text1P)) text1P = safeDecodeUtf8 <$> A.takeTill (== ' ') diff --git a/src/Simplex/Chat/Library/Internal.hs b/src/Simplex/Chat/Library/Internal.hs index d4c700ab5d..cef0f1bb8a 100644 --- a/src/Simplex/Chat/Library/Internal.hs +++ b/src/Simplex/Chat/Library/Internal.hs @@ -929,7 +929,7 @@ acceptContactRequestAsync liftIO $ setCommandConnId db user cmdId connId getContact db vr user contactId -acceptGroupJoinRequestAsync :: User -> Int64 -> GroupInfo -> InvitationId -> VersionRangeChat -> Profile -> Maybe XContactId -> GroupAcceptance -> GroupMemberRole -> Maybe IncognitoProfile -> CM GroupMember +acceptGroupJoinRequestAsync :: User -> Int64 -> GroupInfo -> InvitationId -> VersionRangeChat -> Profile -> Maybe XContactId -> Maybe SharedMsgId -> GroupAcceptance -> GroupMemberRole -> Maybe IncognitoProfile -> CM GroupMember acceptGroupJoinRequestAsync user uclId @@ -938,13 +938,14 @@ acceptGroupJoinRequestAsync cReqChatVRange cReqProfile cReqXContactId_ + welcomeMsgId_ gAccepted gLinkMemRole incognitoProfile = do gVar <- asks random let initialStatus = acceptanceToStatus (memberAdmission groupProfile) gAccepted (groupMemberId, memberId) <- withStore $ \db -> - createJoiningMember db gVar user gInfo cReqChatVRange cReqProfile cReqXContactId_ gLinkMemRole initialStatus + createJoiningMember db gVar user gInfo cReqChatVRange cReqProfile cReqXContactId_ welcomeMsgId_ gLinkMemRole initialStatus currentMemCount <- withStore' $ \db -> getGroupCurrentMembersCount db user gInfo let Profile {displayName} = profileToSendOnAccept user incognitoProfile True GroupMember {memberRole = userRole, memberId = userMemberId} = membership @@ -979,7 +980,7 @@ acceptGroupJoinSendRejectAsync rejectionReason = do gVar <- asks random (groupMemberId, memberId) <- withStore $ \db -> - createJoiningMember db gVar user gInfo cReqChatVRange cReqProfile cReqXContactId_ GRObserver GSMemRejected + createJoiningMember db gVar user gInfo cReqChatVRange cReqProfile cReqXContactId_ Nothing GRObserver GSMemRejected let GroupMember {memberRole = userRole, memberId = userMemberId} = membership msg = XGrpLinkReject $ @@ -1111,8 +1112,15 @@ sendHistory user gInfo@GroupInfo {groupId, membership} m@GroupMember {activeConn (errs', events) <- partitionEithers <$> mapM (tryChatError . itemForwardEvents) items let errors = map ChatErrorStore errs <> errs' unless (null errors) $ toView $ CEvtChatErrors errors - let events' = maybe (concat events) (\x -> concat events <> [x]) descrEvent_ - forM_ (L.nonEmpty events') $ \events'' -> + let events' = concat events + events_ <- case descrEvent_ of + Just descr -> mkEvents <$> withStore' (\db -> getMemberJoinRequest db user gInfo m) + where + mkEvents = \case + Just (_, Just _welcomeMsgId) -> events' + _ -> events' <> [descr] + Nothing -> pure events' + forM_ (L.nonEmpty events_) $ \events'' -> sendGroupMemberMessages user conn events'' groupId where descrEvent_ :: Maybe (ChatMsgEvent 'Json) diff --git a/src/Simplex/Chat/Library/Subscriber.hs b/src/Simplex/Chat/Library/Subscriber.hs index 810af2fe0e..4f82823118 100644 --- a/src/Simplex/Chat/Library/Subscriber.hs +++ b/src/Simplex/Chat/Library/Subscriber.hs @@ -36,7 +36,7 @@ import Data.Maybe (catMaybes, fromMaybe, isJust, isNothing, mapMaybe) import Data.Text (Text) import qualified Data.Text as T import Data.Text.Encoding (decodeLatin1) -import Data.Time.Clock (UTCTime, diffUTCTime) +import Data.Time.Clock (UTCTime, diffUTCTime, getCurrentTime) import qualified Data.UUID as UUID import qualified Data.UUID.V4 as V4 import Data.Word (Word32) @@ -580,9 +580,10 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = createFeatureEnabledItems user ct' (Just PreparedContact {connLinkToConnect = ACCL _ (CCLink cReq _)}, _) -> unless (Just pqEnc == connRequestPQEncryption cReq) createE2EItem - (_, Just connReqId) -> do - UserContactRequest {pqSupport} <- withStore $ \db -> getContactRequest db user connReqId - unless (CR.pqSupportToEnc pqSupport == pqEnc) createE2EItem + (_, Just connReqId) -> + withStore' (\db -> getContactRequest' db user connReqId) >>= \case + Just UserContactRequest {pqSupport} | CR.pqSupportToEnc pqSupport == pqEnc -> pure () + _ -> createE2EItem when (contactConnInitiated conn') $ do let Connection {groupLinkId} = conn' doProbeContacts = isJust groupLinkId @@ -590,7 +591,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = withStore' $ \db -> resetContactConnInitiated db user conn' forM_ viaUserContactLink $ \userContactLinkId -> do (ucl, gli_) <- withStore $ \db -> getUserContactLinkById db userId userContactLinkId - when (connChatVersion < batchSend2Version) $ sendAutoReply ucl ct' + -- let UserContactLink {addressSettings = AddressSettings {autoReply}} = ucl + when (connChatVersion < batchSend2Version) $ forM_ (autoReply $ addressSettings ucl) $ \mc -> sendAutoReply ct' mc Nothing -- old versions only -- TODO REMOVE LEGACY vvv forM_ gli_ $ \GroupLinkInfo {groupId, memberRole = gLinkMemRole} -> do groupInfo <- withStore $ \db -> getGroupInfo db vr user groupId @@ -656,9 +658,11 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = when (directOrUsed ct && sqSecured) $ do lift $ setContactNetworkStatus ct NSConnected toView $ CEvtContactSndReady user ct - forM_ viaUserContactLink $ \userContactLinkId -> do + when (connChatVersion >= batchSend2Version) $ forM_ viaUserContactLink $ \userContactLinkId -> do (ucl, _) <- withStore $ \db -> getUserContactLinkById db userId userContactLinkId - when (connChatVersion >= batchSend2Version) $ sendAutoReply ucl ct + forM_ (autoReply $ addressSettings ucl) $ \mc -> do + connReq_ <- pure (contactRequestId' ct) $>>= \connReqId -> withStore' (\db -> getContactRequest' db user connReqId) + sendAutoReply ct mc connReq_ QCONT -> void $ continueSending connEntity conn MWARN msgId err -> do @@ -678,9 +682,11 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = -- TODO add debugging output _ -> pure () where - sendAutoReply UserContactLink {addressSettings = AddressSettings {autoReply}} ct = - forM_ autoReply $ \mc -> do - (msg, _) <- sendDirectContactMessage user ct (XMsgNew $ MCSimple (extMsgContent mc Nothing)) + sendAutoReply ct mc = \case + Just UserContactRequest {welcomeSharedMsgId = Just smId} -> + void $ sendDirectContactMessage user ct $ XMsgUpdate smId mc M.empty Nothing Nothing Nothing + _ -> do + (msg, _) <- sendDirectContactMessage user ct $ XMsgNew $ MCSimple $ extMsgContent mc Nothing ci <- saveSndChatItem user (CDDirectSnd ct) msg (CISndMsgContent mc) toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDSnd (DirectChat ct) ci] @@ -808,9 +814,11 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = (gInfo'', m'', scopeInfo) <- mkGroupChatScope gInfo' m' let cd = CDGroupRcv gInfo'' scopeInfo m'' createInternalChatItem user cd (CIRcvGroupE2EEInfo E2EInfo {pqEnabled = Just PQEncOff}) Nothing - createGroupFeatureItems user cd CIRcvGroupFeature gInfo'' + let prepared = preparedGroup gInfo'' + unless (isJust prepared) $ createGroupFeatureItems user cd CIRcvGroupFeature gInfo'' memberConnectedChatItem gInfo'' scopeInfo m'' - unless (memberPending membership) $ maybeCreateGroupDescrLocal gInfo'' m'' + let welcomeMsgId_ = (\PreparedGroup {welcomeSharedMsgId = mId} -> mId) <$> prepared + unless (memberPending membership || isJust welcomeMsgId_) $ maybeCreateGroupDescrLocal gInfo'' m'' GCInviteeMember -> do (gInfo', mStatus) <- if not (memberPending m) @@ -829,13 +837,16 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = toView $ CEvtJoinedGroupMember user gInfo'' m' {memberStatus = mStatus} let Connection {viaUserContactLink} = conn when (isJust viaUserContactLink && isNothing (memberContactId m')) $ sendXGrpLinkMem gInfo'' - when (connChatVersion < batchSend2Version) sendGroupAutoReply + when (connChatVersion < batchSend2Version) $ getAutoReplyMsg >>= mapM_ (\mc -> sendGroupAutoReply mc Nothing) case mStatus of GSMemPendingApproval -> pure () GSMemPendingReview -> introduceToModerators vr user gInfo'' m' _ -> do introduceToAll vr user gInfo'' m' - when (groupFeatureAllowed SGFHistory gInfo'') $ sendHistory user gInfo'' m' + let memberIsCustomer = case businessChat gInfo'' of + Just BusinessChatInfo {chatType = BCCustomer, customerId} -> memberId' m' == customerId + _ -> False + when (groupFeatureAllowed SGFHistory gInfo'' && not memberIsCustomer) $ sendHistory user gInfo'' m' where sendXGrpLinkMem gInfo'' = do let profileMode = ExistingIncognito <$> incognitoMembershipProfile gInfo'' @@ -1011,7 +1022,11 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = JOINED sqSecured _serviceId -> -- [async agent commands] continuation on receiving JOINED when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> - when (sqSecured && connChatVersion >= batchSend2Version) sendGroupAutoReply + when (sqSecured && connChatVersion >= batchSend2Version) $ do + mc_ <- getAutoReplyMsg + forM_ mc_ $ \mc -> do + connReq_ <- withStore' $ \db -> getBusinessContactRequest db user groupId + sendGroupAutoReply mc connReq_ QCONT -> do continued <- continueSending connEntity conn when continued $ sendPendingGroupMessages user m conn @@ -1039,22 +1054,23 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = updateGroupItemsErrorStatus db msgId groupMemberId newStatus = do itemIds <- getChatItemIdsByAgentMsgId db connId msgId forM_ itemIds $ \itemId -> updateGroupMemSndStatus' db itemId groupMemberId newStatus - sendGroupAutoReply = autoReplyMC >>= mapM_ send - where - autoReplyMC = do - let GroupInfo {businessChat} = gInfo - GroupMember {memberId = joiningMemberId} = m - case businessChat of - Just BusinessChatInfo {customerId, chatType = BCCustomer} - | joiningMemberId == customerId -> useReply <$> withStore (`getUserAddress` user) - where - useReply UserContactLink {addressSettings = AddressSettings {autoReply}} = autoReply - _ -> pure Nothing - send mc = do - msg <- sendGroupMessage' user gInfo [m] (XMsgNew $ MCSimple (extMsgContent mc Nothing)) - ci <- saveSndChatItem user (CDGroupSnd gInfo Nothing) msg (CISndMsgContent mc) - withStore' $ \db -> createGroupSndStatus db (chatItemId' ci) (groupMemberId' m) GSSNew - toView $ CEvtNewChatItems user [AChatItem SCTGroup SMDSnd (GroupChat gInfo Nothing) ci] + getAutoReplyMsg = do + let GroupInfo {businessChat} = gInfo + GroupMember {memberId = joiningMemberId} = m + case businessChat of + Just BusinessChatInfo {customerId, chatType = BCCustomer} + | joiningMemberId == customerId -> useReply <$> withStore (`getUserAddress` user) + where + useReply UserContactLink {addressSettings = AddressSettings {autoReply}} = autoReply + _ -> pure Nothing + sendGroupAutoReply mc = \case + Just UserContactRequest {welcomeSharedMsgId = Just smId} -> + void $ sendGroupMessage' user gInfo [m] $ XMsgUpdate smId mc M.empty Nothing Nothing Nothing + _ -> do + msg <- sendGroupMessage' user gInfo [m] $ XMsgNew $ MCSimple $ extMsgContent mc Nothing + ci <- saveSndChatItem user (CDGroupSnd gInfo Nothing) msg (CISndMsgContent mc) + withStore' $ \db -> createGroupSndStatus db (chatItemId' ci) (groupMemberId' m) GSSNew + toView $ CEvtNewChatItems user [AChatItem SCTGroup SMDSnd (GroupChat gInfo Nothing) ci] agentMsgDecryptError :: AgentCryptoError -> (MsgDecryptError, Word32) agentMsgDecryptError = \case @@ -1255,56 +1271,121 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = REBusinessChat gInfo _clientMember -> -- TODO [short links] update request msg toView $ CEvtBusinessRequestAlreadyAccepted user gInfo - RSCurrentRequest ucr re_ repeatRequest -> case re_ of + RSCurrentRequest prevUcr_ ucr@UserContactRequest {welcomeSharedMsgId} re_ -> case re_ of Nothing -> toView $ CEvtReceivedContactRequest user ucr Nothing Just (REContact ct) -> do - -- TODO [short links] prevent duplicate items - -- update welcome message if changed (send update event to UI) and add updated feature items. - -- Do not created e2e item on repeat request - if repeatRequest - then do - -- TODO [short links] update request msg - -- .... - acceptOrShow Nothing -- pass item? - else do - -- TODO [short links] save sharedMsgId instead of the last Nothing - let createItem content = createChatItem user (CDDirectRcv ct) False content Nothing Nothing - void $ createItem $ CIRcvDirectE2EEInfo $ E2EInfo $ Just $ CR.pqSupportToEnc $ reqPQSup + let cd = CDDirectRcv ct + aci_ <- case prevUcr_ of + Just UserContactRequest {requestSharedMsgId = prevSharedMsgId_} -> + -- TODO [short links] this branch does not update feature items and e2e items, as they are highly unlikely to change + -- they will be updated after connection is accepted. + upsertDirectRequestItem cd (requestMsg_, prevSharedMsgId_) + Nothing -> do + let e2eContent = CIRcvDirectE2EEInfo $ E2EInfo $ Just $ CR.pqSupportToEnc $ reqPQSup + void $ createChatItem user cd False e2eContent Nothing Nothing void $ createFeatureEnabledItems_ user ct - -- TODO [short links] save sharedMsgId - aci <- forM requestMsg_ $ \(sharedMsgId, mc) -> do - aci <- createItem $ CIRcvMsgContent mc - unlessM (asks $ coreApi . config) $ toView $ CEvtNewChatItems user [aci] - pure aci - acceptOrShow aci - where - acceptOrShow aci_ = - case autoAccept of - Nothing -> do - let cInfo = DirectChat ct - chat = AChat SCTDirect $ case aci_ of - Just (AChatItem SCTDirect dir _ ci) -> Chat cInfo [CChatItem dir ci] emptyChatStats {unreadCount = 1, minUnreadItemId = chatItemId' ci} - _ -> Chat cInfo [] emptyChatStats - toView $ CEvtReceivedContactRequest user ucr (Just chat) - Just AutoAccept {acceptIncognito} -> do - incognitoProfile <- - if not shortLinkDataSet && acceptIncognito - then Just . NewIncognito <$> liftIO generateRandomProfile - else pure Nothing - ct' <- acceptContactRequestAsync user uclId ct ucr incognitoProfile - -- chat in event? - toView $ CEvtAcceptingContactRequest user ct' + forM_ (autoReply addressSettings) $ \mc -> forM_ welcomeSharedMsgId $ \sharedMsgId -> + createChatItem user (CDDirectSnd ct) False (CISndMsgContent mc) (Just sharedMsgId) Nothing + mapM (createRequestItem cd) requestMsg_ + case autoAccept of + Nothing -> do + let cInfo = DirectChat ct + chat = AChat SCTDirect $ case aci_ of + Just (AChatItem SCTDirect dir _ ci) -> Chat cInfo [CChatItem dir ci] emptyChatStats {unreadCount = 1, minUnreadItemId = chatItemId' ci} + _ -> Chat cInfo [] emptyChatStats + toView $ CEvtReceivedContactRequest user ucr $ Just chat + Just AutoAccept {acceptIncognito} -> do + incognitoProfile <- + if not shortLinkDataSet && acceptIncognito + then Just . NewIncognito <$> liftIO generateRandomProfile + else pure Nothing + ct' <- acceptContactRequestAsync user uclId ct ucr incognitoProfile + toView $ CEvtAcceptingContactRequest user ct' Just (REBusinessChat gInfo clientMember) -> do - -- TODO [short links] prevent duplicate items (use repeatRequest like for REContact) (_gInfo', _clientMember') <- acceptBusinessJoinRequestAsync user uclId gInfo clientMember ucr - -- TODO [short links] add welcome message if welcomeMsgId is present - -- forM_ autoReply $ \arMC -> - -- when (shortLinkDataSet && v >= shortLinkDataVersion) $ - -- createInternalChatItem user (CDGroupSnd gInfo Nothing) (CISndMsgContent arMC) Nothing - -- TODO [short links] save sharedMsgId - forM_ requestMsg_ $ \(sharedMsgId, mc) -> - createInternalChatItem user (CDGroupRcv gInfo Nothing clientMember) (CIRcvMsgContent mc) Nothing + let cd = CDGroupRcv gInfo Nothing clientMember + void $ case prevUcr_ of + Just UserContactRequest {requestSharedMsgId = prevSharedMsgId_} -> + -- TODO [short links] this branch does not update feature items and e2e items, as they are highly unlikely to change + -- they will be updated after connection is accepted. + upsertBusinessRequestItem cd (requestMsg_, prevSharedMsgId_) + Nothing -> do + -- TODO [short links] possibly, we can just keep them created where they are created on the business side due to auto-accept + -- let e2eContent = CIRcvGroupE2EEInfo $ E2EInfo $ Just False -- no PQ encryption in groups + -- void $ createChatItem user cd False e2eContent Nothing Nothing + -- void $ createFeatureEnabledItems_ user ct + forM_ (autoReply addressSettings) $ \arMC -> forM_ welcomeSharedMsgId $ \sharedMsgId -> + createChatItem user (CDGroupSnd gInfo Nothing) False (CISndMsgContent arMC) (Just sharedMsgId) Nothing + mapM (createRequestItem cd) requestMsg_ toView $ CEvtAcceptingBusinessRequest user gInfo + where + upsertDirectRequestItem :: ChatDirection 'CTDirect 'MDRcv -> (Maybe (SharedMsgId, MsgContent), Maybe SharedMsgId) -> CM (Maybe AChatItem) + upsertDirectRequestItem cd@(CDDirectRcv ct@Contact {contactId}) = upsertRequestItem cd updateRequestItem markRequestItemDeleted + where + updateRequestItem (sharedMsgId, mc) = + withStore (\db -> getDirectChatItemBySharedMsgId db user contactId sharedMsgId) >>= \case + CChatItem SMDRcv ci@ChatItem {content = CIRcvMsgContent oldMC} + | mc /= oldMC -> do + currentTs <- liftIO getCurrentTime + aci <- withStore' $ \db -> do + addInitialAndNewCIVersions db (chatItemId' ci) (chatItemTs' ci, oldMC) (currentTs, mc) + aChatItem <$> updateDirectChatItem' db user contactId ci (CIRcvMsgContent mc) True False Nothing Nothing + toView $ CEvtChatItemUpdated user aci + pure $ Just aci + | otherwise -> pure $ Just $ aChatItem ci + _ -> pure Nothing + where + aChatItem = AChatItem SCTDirect SMDRcv (DirectChat ct) + markRequestItemDeleted sharedMsgId = + withStore' (\db -> runExceptT $ getDirectChatItemBySharedMsgId db user contactId sharedMsgId) >>= \case + Right (cci@(CChatItem SMDRcv _)) -> do + currentTs <- liftIO getCurrentTime + deletions <- if featureAllowed SCFFullDelete forContact ct + then deleteDirectCIs user ct [cci] + else markDirectCIsDeleted user ct [cci] currentTs + toView $ CEvtChatItemsDeleted user deletions False False + _ -> pure () + upsertBusinessRequestItem :: ChatDirection 'CTGroup 'MDRcv -> (Maybe (SharedMsgId, MsgContent), Maybe SharedMsgId) -> CM (Maybe AChatItem) + upsertBusinessRequestItem cd@(CDGroupRcv gInfo@GroupInfo {groupId} _ clientMember) = upsertRequestItem cd updateRequestItem markRequestItemDeleted + where + updateRequestItem (sharedMsgId, mc) = + withStore (\db -> getGroupChatItemBySharedMsgId db user gInfo (groupMemberId' clientMember) sharedMsgId) >>= \case + CChatItem SMDRcv ci@ChatItem {chatDir = CIGroupRcv m', content = CIRcvMsgContent oldMC} + | sameMemberId (memberId' clientMember) m' -> + if mc /= oldMC + then do + currentTs <- liftIO getCurrentTime + aci <- withStore' $ \db -> do + addInitialAndNewCIVersions db (chatItemId' ci) (chatItemTs' ci, oldMC) (currentTs, mc) + aChatItem <$> updateGroupChatItem db user groupId ci (CIRcvMsgContent mc) True False Nothing + toView $ CEvtChatItemUpdated user aci + pure $ Just aci + else pure $ Just $ aChatItem ci + _ -> pure Nothing + where + aChatItem = AChatItem SCTGroup SMDRcv (GroupChat gInfo Nothing) + markRequestItemDeleted sharedMsgId = + withStore' (\db -> runExceptT $ getGroupMemberCIBySharedMsgId db user gInfo (memberId' clientMember) sharedMsgId) >>= \case + Right cci@(CChatItem SMDRcv ChatItem {chatDir = CIGroupRcv m'}) + | sameMemberId (memberId' clientMember) m' -> do + currentTs <- liftIO getCurrentTime + deletions <- if groupFeatureMemberAllowed SGFFullDelete clientMember gInfo + then deleteGroupCIs user gInfo Nothing [cci] Nothing currentTs + else markGroupCIsDeleted user gInfo Nothing [cci] Nothing currentTs + toView $ CEvtChatItemsDeleted user deletions False False + _ -> pure () + createRequestItem :: ChatTypeI c => ChatDirection c 'MDRcv -> (SharedMsgId, MsgContent) -> CM AChatItem + createRequestItem cd (sharedMsgId, mc) = do + aci <- createChatItem user cd False (CIRcvMsgContent mc) (Just sharedMsgId) Nothing + toView $ CEvtNewChatItems user [aci] + pure aci + upsertRequestItem :: ChatTypeI c => ChatDirection c 'MDRcv -> ((SharedMsgId, MsgContent) -> CM (Maybe AChatItem)) -> (SharedMsgId -> CM ()) -> (Maybe (SharedMsgId, MsgContent), Maybe SharedMsgId) -> CM (Maybe AChatItem) + upsertRequestItem cd update delete = \case + (Just msg, Nothing) -> Just <$> createRequestItem cd msg + (Just msg@(sharedMsgId, _), Just prevSharedMsgId) | sharedMsgId == prevSharedMsgId -> + update msg `catchCINotFound` \_ -> Just <$> createRequestItem cd msg + (Nothing, Just prevSharedMsgId) -> Nothing <$ delete prevSharedMsgId + _ -> pure Nothing -- ##### Group link join requests (don't create contact requests) ##### Just gli@GroupLinkInfo {groupId, memberRole = gLinkMemRole} -> do -- TODO [short links] deduplicate request by xContactId? @@ -1316,7 +1397,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = messageError "processUserContactRequest: chat version range incompatible for accepting group join request" | otherwise -> do let profileMode = ExistingIncognito <$> incognitoMembershipProfile gInfo - mem <- acceptGroupJoinRequestAsync user uclId gInfo invId chatVRange p xContactId_ acceptance useRole profileMode + mem <- acceptGroupJoinRequestAsync user uclId gInfo invId chatVRange p xContactId_ welcomeMsgId_ acceptance useRole profileMode (gInfo', mem', scopeInfo) <- mkGroupChatScope gInfo mem createInternalChatItem user (CDGroupRcv gInfo' scopeInfo mem') (CIRcvGroupEvent RGEInvitedViaGroupLink) Nothing toView $ CEvtAcceptingGroupJoinRequestMember user gInfo' mem' @@ -2201,7 +2282,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = cd = CDGroupRcv gInfo' Nothing m createInternalChatItem user cd (CIRcvGroupE2EEInfo E2EInfo {pqEnabled = Just PQEncOff}) Nothing createGroupFeatureItems user cd CIRcvGroupFeature gInfo' - maybeCreateGroupDescrLocal gInfo' m + let welcomeMsgId_ = (\PreparedGroup {welcomeSharedMsgId = mId} -> mId) <$> preparedGroup gInfo' + unless (isJust welcomeMsgId_) $ maybeCreateGroupDescrLocal gInfo' m createInternalChatItem user cd (CIRcvGroupEvent RGEUserAccepted) Nothing let scopeInfo = Just $ GCSIMemberSupport {groupMember_ = Nothing} createInternalChatItem user (CDGroupRcv gInfo' scopeInfo m) (CIRcvGroupEvent RGEUserAccepted) Nothing diff --git a/src/Simplex/Chat/Protocol.hs b/src/Simplex/Chat/Protocol.hs index 5a0518aae8..1fad623967 100644 --- a/src/Simplex/Chat/Protocol.hs +++ b/src/Simplex/Chat/Protocol.hs @@ -1224,3 +1224,19 @@ instance ToJSON (ChatMessage 'Json) where instance FromJSON (ChatMessage 'Json) where parseJSON v = appJsonToCM <$?> parseJSON v + +data ContactShortLinkData = ContactShortLinkData + { profile :: Profile, + message :: Maybe MsgContent, + business :: Bool + } + deriving (Show) + +data GroupShortLinkData = GroupShortLinkData + { groupProfile :: GroupProfile + } + deriving (Show) + +$(JQ.deriveJSON defaultJSON ''ContactShortLinkData) + +$(JQ.deriveJSON defaultJSON ''GroupShortLinkData) diff --git a/src/Simplex/Chat/Store/Connections.hs b/src/Simplex/Chat/Store/Connections.hs index 90df45357f..0a19daf751 100644 --- a/src/Simplex/Chat/Store/Connections.hs +++ b/src/Simplex/Chat/Store/Connections.hs @@ -112,7 +112,7 @@ getConnectionEntity db vr user@User {userId, userContactId} agentConnId = do [sql| SELECT c.contact_profile_id, c.local_display_name, c.via_group, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, c.contact_used, c.contact_status, c.enable_ntfs, c.send_rcpts, c.favorite, - p.preferences, c.user_preferences, c.created_at, c.updated_at, c.chat_ts, c.conn_full_link_to_connect, c.conn_short_link_to_connect, c.welcome_shared_msg_id, c.contact_request_id, + p.preferences, c.user_preferences, c.created_at, c.updated_at, c.chat_ts, c.conn_full_link_to_connect, c.conn_short_link_to_connect, c.welcome_shared_msg_id, c.request_shared_msg_id, c.contact_request_id, c.contact_group_member_id, c.contact_grp_inv_sent, c.ui_themes, c.chat_deleted, c.custom_data, c.chat_item_ttl FROM contacts c JOIN contact_profiles p ON c.contact_profile_id = p.contact_profile_id @@ -140,7 +140,7 @@ getConnectionEntity db vr user@User {userId, userContactId} agentConnId = do g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, - g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_started_connection, g.welcome_shared_msg_id, + g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id, g.business_chat, g.business_member_id, g.customer_member_id, g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention, -- GroupInfo {membership} diff --git a/src/Simplex/Chat/Store/ContactRequest.hs b/src/Simplex/Chat/Store/ContactRequest.hs index 9fae87eaa1..a56c89102b 100644 --- a/src/Simplex/Chat/Store/ContactRequest.hs +++ b/src/Simplex/Chat/Store/ContactRequest.hs @@ -12,7 +12,9 @@ module Simplex.Chat.Store.ContactRequest ( createOrUpdateContactRequest, setContactAcceptedXContactId, - setBusinessChatAcceptedXContactId + setBusinessChatAcceptedXContactId, + setRequestSharedMsgIdForContact, + setRequestSharedMsgIdForGroup ) where @@ -81,11 +83,14 @@ createOrUpdateContactRequest Just xContactId -> -- 1) first we try to find accepted contact or business chat by xContactId liftIO (getAcceptedContact xContactId) >>= \case - Just ct -> pure $ RSAcceptedRequest Nothing (REContact ct) + Just ct -> do + cr <- liftIO $ getContactRequestByXContactId xContactId + pure $ RSAcceptedRequest cr (REContact ct) Nothing -> liftIO (getAcceptedBusinessChat xContactId) >>= \case Just gInfo@GroupInfo {businessChat = Just BusinessChatInfo {customerId}} -> do clientMember <- getGroupMemberByMemberId db vr user gInfo customerId - pure $ RSAcceptedRequest Nothing (REBusinessChat gInfo clientMember) + cr <- liftIO $ getContactRequestByXContactId xContactId + pure $ RSAcceptedRequest cr (REBusinessChat gInfo clientMember) Just GroupInfo {businessChat = Nothing} -> throwError SEInvalidBusinessChatContactRequest -- 2) if no legacy accepted contact or business chat was found, next we try to find an existing request Nothing -> @@ -105,7 +110,7 @@ createOrUpdateContactRequest SELECT -- Contact ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, - cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.contact_request_id, + cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.request_shared_msg_id, ct.contact_request_id, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, -- Connection c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, @@ -196,7 +201,7 @@ createOrUpdateContactRequest (contactId, contactRequestId) ucr <- getContactRequest db user contactRequestId ct <- getContact db vr user contactId - pure $ RSCurrentRequest ucr (Just $ REContact ct) False + pure $ RSCurrentRequest Nothing ucr (Just $ REContact ct) createBusinessChat = do let Profile {preferences = userPreferences} = profileToSendOnAccept user Nothing True groupPreferences = maybe defaultBusinessGroupPrefs businessGroupPrefs userPreferences @@ -208,15 +213,15 @@ createOrUpdateContactRequest "UPDATE contact_requests SET business_group_id = ? WHERE contact_request_id = ?" (groupId, contactRequestId) ucr <- getContactRequest db user contactRequestId - pure $ RSCurrentRequest ucr (Just $ REBusinessChat gInfo clientMember) False + pure $ RSCurrentRequest Nothing ucr (Just $ REBusinessChat gInfo clientMember) updateContactRequest :: UserContactRequest -> ExceptT StoreError IO RequestStage - updateContactRequest UserContactRequest {contactRequestId, contactId_, localDisplayName = oldLdn, profile = Profile {displayName = oldDisplayName}} = do + updateContactRequest ucr@UserContactRequest {contactRequestId, contactId_, localDisplayName = oldLdn, profile = Profile {displayName = oldDisplayName}} = do currentTs <- liftIO getCurrentTime liftIO $ updateProfile currentTs updateRequest currentTs ucr' <- getContactRequest db user contactRequestId re_ <- getRequestEntity ucr' - pure $ RSCurrentRequest ucr' re_ True + pure $ RSCurrentRequest (Just ucr) ucr' re_ where updateProfile currentTs = DB.execute @@ -288,12 +293,16 @@ createOrUpdateContactRequest setContactAcceptedXContactId :: DB.Connection -> Contact -> XContactId -> IO () setContactAcceptedXContactId db Contact {contactId} xContactId = - DB.execute - db "UPDATE contacts SET xcontact_id = ? WHERE contact_id = ?" - (xContactId, contactId) + DB.execute db "UPDATE contacts SET xcontact_id = ? WHERE contact_id = ?" (xContactId, contactId) setBusinessChatAcceptedXContactId :: DB.Connection -> GroupInfo -> XContactId -> IO () setBusinessChatAcceptedXContactId db GroupInfo {groupId} xContactId = - DB.execute - db "UPDATE groups SET business_xcontact_id = ? WHERE group_id = ?" - (xContactId, groupId) + DB.execute db "UPDATE groups SET business_xcontact_id = ? WHERE group_id = ?" (xContactId, groupId) + +setRequestSharedMsgIdForContact :: DB.Connection -> ContactId -> SharedMsgId -> IO () +setRequestSharedMsgIdForContact db contactId sharedMsgId = do + DB.execute db "UPDATE contacts SET request_shared_msg_id = ? WHERE contact_id = ?" (sharedMsgId, contactId) + +setRequestSharedMsgIdForGroup :: DB.Connection -> GroupId -> SharedMsgId -> IO () +setRequestSharedMsgIdForGroup db groupId sharedMsgId = do + DB.execute db "UPDATE groups SET request_shared_msg_id = ? WHERE group_id = ?" (sharedMsgId, groupId) diff --git a/src/Simplex/Chat/Store/Direct.hs b/src/Simplex/Chat/Store/Direct.hs index 79012d29fb..c3a3051eb2 100644 --- a/src/Simplex/Chat/Store/Direct.hs +++ b/src/Simplex/Chat/Store/Direct.hs @@ -7,6 +7,7 @@ {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TupleSections #-} +{-# LANGUAGE TypeOperators #-} {-# OPTIONS_GHC -fno-warn-ambiguous-fields #-} module Simplex.Chat.Store.Direct @@ -27,10 +28,10 @@ module Simplex.Chat.Store.Direct createDirectConnection, createIncognitoProfile, createConnReqConnection, + setPreparedGroupStartedConnection, createAddressContactConnection, getProfileById, getConnReqContactXContactId, - getContactByConnReqHash, createPreparedContact, updatePreparedContactUser, createDirectContact, @@ -60,6 +61,8 @@ module Simplex.Chat.Store.Direct getUserContacts, getUserContactLinkIdByCReq, getContactRequest, + getContactRequest', + getBusinessContactRequest, getContactRequestIdByName, deleteContactRequest, createContactFromRequest, @@ -108,10 +111,10 @@ import qualified Simplex.Messaging.Agent.Store.DB as DB import Simplex.Messaging.Crypto.Ratchet (PQSupport) import Simplex.Messaging.Protocol (SubscriptionMode (..)) #if defined(dbPostgres) -import Database.PostgreSQL.Simple (Only (..), (:.) (..)) +import Database.PostgreSQL.Simple (Only (..), Query, (:.) (..)) import Database.PostgreSQL.Simple.SqlQQ (sql) #else -import Database.SQLite.Simple (Only (..), (:.) (..)) +import Database.SQLite.Simple (Only (..), Query, (:.) (..)) import Database.SQLite.Simple.QQ (sql) #endif @@ -156,7 +159,7 @@ createConnReqConnection :: DB.Connection -> UserId -> ConnId -> ConnReqUriHash - createConnReqConnection db userId acId cReqHash sLnk attachConnTo_ xContactId incognitoProfile groupLinkId subMode chatV pqSup = do currentTs <- getCurrentTime customUserProfileId <- mapM (createIncognitoProfile_ db userId currentTs) incognitoProfile - let pccConnStatus = ConnJoined + let pccConnStatus = ConnPrepared DB.execute db [sql| @@ -184,41 +187,57 @@ createConnReqConnection db userId acId cReqHash sLnk attachConnTo_ xContactId in Nothing -> (ConnContact, Nothing, Nothing) updatePreparedGroup GroupInfo {groupId, membership} pccConnId customUserProfileId currentTs = do setViaGroupLinkHash db groupId pccConnId - DB.execute - db - "UPDATE groups SET conn_link_started_connection = ?, updated_at = ? WHERE group_id = ?" - (BI True, currentTs, groupId) when (isJust customUserProfileId) $ DB.execute db "UPDATE group_members SET member_profile_id = ?, updated_at = ? WHERE group_member_id = ?" (customUserProfileId, currentTs, groupMemberId' membership) -getConnReqContactXContactId :: DB.Connection -> VersionRangeChat -> User -> ConnReqUriHash -> IO (Maybe Contact, Maybe XContactId) -getConnReqContactXContactId db vr user@User {userId} cReqHash = do - getContactByConnReqHash db vr user cReqHash >>= \case - c@(Just _) -> pure (c, Nothing) - Nothing -> (Nothing,) <$> getXContactId +setPreparedGroupStartedConnection :: DB.Connection -> GroupId -> IO () +setPreparedGroupStartedConnection db groupId = do + currentTs <- getCurrentTime + DB.execute + db + "UPDATE groups SET conn_link_started_connection = ?, updated_at = ? WHERE group_id = ?" + (BI True, currentTs, groupId) + +getConnReqContactXContactId :: DB.Connection -> VersionRangeChat -> User -> ConnReqUriHash -> ConnReqUriHash -> IO (Maybe Contact, Maybe (XContactId, Maybe Connection)) +getConnReqContactXContactId db vr user@User {userId} cReqHash1 cReqHash2 = do + getContactByConnReqHash db vr user cReqHash1 cReqHash2 >>= \case + Just (xContactId_, ct@Contact {activeConn}) -> pure (Just ct, (,activeConn) <$> xContactId_) + Nothing -> (Nothing,) <$> getConnectionXContactId where - getXContactId :: IO (Maybe XContactId) - getXContactId = - maybeFirstRow fromOnly $ + getConnectionXContactId :: IO (Maybe (XContactId, Maybe Connection)) + getConnectionXContactId = + maybeFirstRow toConnectionAndXContactId $ DB.query db - "SELECT xcontact_id FROM connections WHERE user_id = ? AND via_contact_uri_hash = ? LIMIT 1" - (userId, cReqHash) + [sql| + SELECT xcontact_id, + connection_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, via_group_link, group_link_id, custom_user_profile_id, conn_status, conn_type, contact_conn_initiated, local_alias, + contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at, security_code, security_code_verified_at, pq_support, pq_encryption, pq_snd_enabled, pq_rcv_enabled, auth_err_counter, quota_err_counter, + conn_chat_version, peer_chat_min_version, peer_chat_max_version + FROM connections + WHERE (user_id = ? AND via_contact_uri_hash = ?) + OR (user_id = ? AND via_contact_uri_hash = ?) + LIMIT 1 + |] + (userId, cReqHash1, userId, cReqHash2) + toConnectionAndXContactId :: Only XContactId :. ConnectionRow -> (XContactId, Maybe Connection) + toConnectionAndXContactId (Only xContactId_ :. connRow) = (xContactId_, Just $ toConnection vr connRow) -getContactByConnReqHash :: DB.Connection -> VersionRangeChat -> User -> ConnReqUriHash -> IO (Maybe Contact) -getContactByConnReqHash db vr user@User {userId} cReqHash = do - ct_ <- - maybeFirstRow (toContact vr user []) $ +getContactByConnReqHash :: DB.Connection -> VersionRangeChat -> User -> ConnReqUriHash -> ConnReqUriHash -> IO (Maybe (Maybe XContactId, Contact)) +getContactByConnReqHash db vr user@User {userId} cReqHash1 cReqHash2 = do + r <- + maybeFirstRow toContactAndXContactId $ DB.query db [sql| SELECT + c.xcontact_id, -- Contact ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, - cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.contact_request_id, + cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.request_shared_msg_id, ct.contact_request_id, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, -- Connection c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, @@ -227,12 +246,18 @@ getContactByConnReqHash db vr user@User {userId} cReqHash = do FROM contacts ct JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id JOIN connections c ON c.contact_id = ct.contact_id - WHERE c.user_id = ? AND c.via_contact_uri_hash = ? AND ct.contact_status = ? AND ct.deleted = 0 + WHERE + ( (c.user_id = ? AND c.via_contact_uri_hash = ?) OR + (c.user_id = ? AND c.via_contact_uri_hash = ?) + ) AND ct.contact_status = ? AND ct.deleted = 0 ORDER BY c.created_at DESC LIMIT 1 |] - (userId, cReqHash, CSActive) - mapM (addDirectChatTags db) ct_ + (userId, cReqHash1, userId, cReqHash2, CSActive) + mapM (traverse $ addDirectChatTags db) r + where + toContactAndXContactId :: Only (Maybe XContactId) :. (ContactRow :. MaybeConnectionRow) -> (Maybe XContactId, Contact) + toContactAndXContactId (Only xContactId_ :. ctRow) = (xContactId_, toContact vr user [] ctRow) createDirectConnection :: DB.Connection -> User -> ConnId -> CreatedLinkInvitation -> Maybe ContactId -> ConnStatus -> Maybe Profile -> SubscriptionMode -> VersionChat -> PQSupport -> IO PendingContactConnection createDirectConnection db User {userId} acId ccLink contactId_ pccConnStatus incognitoProfile subMode chatV pqSup = do @@ -286,7 +311,7 @@ createPreparedContact db user@User {userId} p@Profile {preferences} connLinkToCo createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, - preparedContact = Just $ PreparedContact connLinkToConnect (connMode m) welcomeSharedMsgId, + preparedContact = Just PreparedContact {connLinkToConnect, uiConnLinkType = connMode m, welcomeSharedMsgId, requestSharedMsgId = Nothing}, contactRequestId = Nothing, contactGroupMemberId = Nothing, contactGrpInvSent = False, @@ -687,23 +712,32 @@ getUserContactLinkIdByCReq db contactRequestId = getContactRequest :: DB.Connection -> User -> Int64 -> ExceptT StoreError IO UserContactRequest getContactRequest db User {userId} contactRequestId = ExceptT . firstRow toContactRequest (SEContactRequestNotFound contactRequestId) $ - DB.query - db - [sql| - SELECT - cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, - cr.contact_id, cr.business_group_id, cr.user_contact_link_id, - c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, - cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, - cr.created_at, cr.updated_at, - cr.peer_chat_min_version, cr.peer_chat_max_version - FROM contact_requests cr - JOIN connections c USING (user_contact_link_id) - JOIN contact_profiles p USING (contact_profile_id) - WHERE cr.user_id = ? - AND cr.contact_request_id = ? - |] - (userId, contactRequestId) + DB.query db (contactRequestQuery <> " WHERE cr.user_id = ? AND cr.contact_request_id = ?") (userId, contactRequestId) + +getContactRequest' :: DB.Connection -> User -> Int64 -> IO (Maybe UserContactRequest) +getContactRequest' db User {userId} contactRequestId = + maybeFirstRow toContactRequest $ + DB.query db (contactRequestQuery <> " WHERE cr.user_id = ? AND cr.contact_request_id = ?") (userId, contactRequestId) + +getBusinessContactRequest :: DB.Connection -> User -> GroupId -> IO (Maybe UserContactRequest) +getBusinessContactRequest db User {userId} groupId = + maybeFirstRow toContactRequest $ + DB.query db (contactRequestQuery <> " WHERE cr.user_id = ? AND cr.business_group_id = ?") (userId, groupId) + +contactRequestQuery :: Query +contactRequestQuery = + [sql| + SELECT + cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, + cr.contact_id, cr.business_group_id, cr.user_contact_link_id, + c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, + cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, + cr.created_at, cr.updated_at, + cr.peer_chat_min_version, cr.peer_chat_max_version + FROM contact_requests cr + JOIN connections c USING (user_contact_link_id) + JOIN contact_profiles p USING (contact_profile_id) + |] getContactRequestIdByName :: DB.Connection -> UserId -> ContactName -> ExceptT StoreError IO Int64 getContactRequestIdByName db userId cName = @@ -807,7 +841,7 @@ getContact_ db vr user@User {userId} contactId deleted = do SELECT -- Contact ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, - cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.contact_request_id, + cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.request_shared_msg_id, ct.contact_request_id, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, -- Connection c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs index e9462988df..e872bc3849 100644 --- a/src/Simplex/Chat/Store/Groups.hs +++ b/src/Simplex/Chat/Store/Groups.hs @@ -76,6 +76,7 @@ module Simplex.Chat.Store.Groups createNewContactMember, createNewContactMemberAsync, createJoiningMember, + getMemberJoinRequest, createJoiningMemberConnection, createBusinessRequestGroup, getContactViaMember, @@ -937,7 +938,7 @@ getUserGroupDetails db vr User {userId, userContactId} _contactId_ search_ = do g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, - g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_started_connection, g.welcome_shared_msg_id, + g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id, g.business_chat, g.business_member_id, g.customer_member_id, g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention, mu.group_member_id, g.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, mu.member_restriction, @@ -1230,7 +1231,7 @@ createNewContactMemberAsync db gVar user@User {userId, userContactId} GroupInfo :. (minV, maxV) ) -createJoiningMember :: DB.Connection -> TVar ChaChaDRG -> User -> GroupInfo -> VersionRangeChat -> Profile -> Maybe XContactId -> GroupMemberRole -> GroupMemberStatus -> ExceptT StoreError IO (GroupMemberId, MemberId) +createJoiningMember :: DB.Connection -> TVar ChaChaDRG -> User -> GroupInfo -> VersionRangeChat -> Profile -> Maybe XContactId -> Maybe SharedMsgId -> GroupMemberRole -> GroupMemberStatus -> ExceptT StoreError IO (GroupMemberId, MemberId) createJoiningMember db gVar @@ -1239,6 +1240,7 @@ createJoiningMember cReqChatVRange Profile {displayName, fullName, image, contactLink, preferences} cReqXContactId_ + welcomeMsgId_ memberRole memberStatus = do currentTs <- liftIO getCurrentTime @@ -1261,15 +1263,20 @@ createJoiningMember [sql| INSERT INTO group_members ( group_id, member_id, member_role, member_category, member_status, invited_by, invited_by_group_member_id, - user_id, local_display_name, contact_id, contact_profile_id, member_xcontact_id, created_at, updated_at, + user_id, local_display_name, contact_id, contact_profile_id, member_xcontact_id, member_welcome_shared_msg_id, created_at, updated_at, peer_chat_min_version, peer_chat_max_version) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) |] ( (groupId, memberId, memberRole, GCInviteeMember, memberStatus, fromInvitedBy userContactId IBUser, groupMemberId' membership) - :. (userId, ldn, Nothing :: (Maybe Int64), profileId, cReqXContactId_, currentTs, currentTs) + :. (userId, ldn, Nothing :: (Maybe Int64), profileId, cReqXContactId_, welcomeMsgId_, currentTs, currentTs) :. (minV, maxV) ) +getMemberJoinRequest :: DB.Connection -> User -> GroupInfo -> GroupMember -> IO (Maybe (Maybe XContactId, Maybe SharedMsgId)) +getMemberJoinRequest db User {userId} GroupInfo {groupId} GroupMember {groupMemberId = mId} = + maybeFirstRow id $ + DB.query db "SELECT member_xcontact_id, member_welcome_shared_msg_id FROM group_members WHERE user_id = ? AND group_id = ? AND group_member_id = ?" (userId, groupId, mId) + createJoiningMemberConnection :: DB.Connection -> User -> Int64 -> (CommandId, ConnId) -> VersionChat -> VersionRangeChat -> GroupMemberId -> SubscriptionMode -> IO () createJoiningMemberConnection db @@ -1291,7 +1298,7 @@ createBusinessRequestGroup gVar user@User {userId, userContactId} cReqChatVRange - Profile {displayName, fullName, image, contactLink, preferences} + Profile {displayName, fullName, image} profileId -- contact request profile id, to be used for member profile ldn -- contact request local display name, to be used for group local display name groupPreferences = do @@ -1813,7 +1820,7 @@ getViaGroupMember db vr User {userId, userContactId} Contact {contactId} = do g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, - g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_started_connection, g.welcome_shared_msg_id, + g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id, g.business_chat, g.business_member_id, g.customer_member_id, g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention, -- GroupInfo {membership} diff --git a/src/Simplex/Chat/Store/Profiles.hs b/src/Simplex/Chat/Store/Profiles.hs index bc18f5e7f3..cf60d32b0f 100644 --- a/src/Simplex/Chat/Store/Profiles.hs +++ b/src/Simplex/Chat/Store/Profiles.hs @@ -441,9 +441,8 @@ data GroupLinkInfo = GroupLinkInfo data AddressSettings = AddressSettings { businessAddress :: Bool, -- possibly, it can be wrapped together with acceptIncognito, or AutoAccept made sum type - welcomeMessage :: Maybe Text, -- included in short link information autoAccept :: Maybe AutoAccept, -- accept automatically - autoReply :: Maybe MsgContent -- sent on acceptance, can be supported with manual acceptance as well + autoReply :: Maybe MsgContent -- included in short link information, sent on acceptance in case wasn't shown during connection } deriving (Eq, Show) @@ -458,11 +457,11 @@ $(J.deriveJSON defaultJSON ''AddressSettings) $(J.deriveJSON defaultJSON ''UserContactLink) -toUserContactLink :: (Int64, ConnReqContact, Maybe ShortLinkContact, BoolInt, BoolInt, Maybe Text, BoolInt, BoolInt, Maybe MsgContent) -> UserContactLink -toUserContactLink (userContactLinkId, connReq, shortLink, BI shortLinkDataSet, BI businessAddress, welcomeMessage, BI autoAccept', BI acceptIncognito, autoReply) = +toUserContactLink :: (Int64, ConnReqContact, Maybe ShortLinkContact, BoolInt, BoolInt, BoolInt, BoolInt, Maybe MsgContent) -> UserContactLink +toUserContactLink (userContactLinkId, connReq, shortLink, BI shortLinkDataSet, BI businessAddress, BI autoAccept', BI acceptIncognito, autoReply) = UserContactLink userContactLinkId (CCLink connReq shortLink) shortLinkDataSet $ let autoAccept = if autoAccept' then Just AutoAccept {acceptIncognito} else Nothing - in AddressSettings {businessAddress, welcomeMessage, autoAccept, autoReply} + in AddressSettings {businessAddress, autoAccept, autoReply} getUserAddress :: DB.Connection -> User -> ExceptT StoreError IO UserContactLink getUserAddress db User {userId} = @@ -475,7 +474,7 @@ getUserContactLinkById db userId userContactLinkId = DB.query db [sql| - SELECT user_contact_link_id, conn_req_contact, short_link_contact, short_link_data_set, business_address, address_welcome_message, auto_accept, auto_accept_incognito, auto_reply_msg_content, group_id, group_link_member_role + SELECT user_contact_link_id, conn_req_contact, short_link_contact, short_link_data_set, business_address, auto_accept, auto_accept_incognito, auto_reply_msg_content, group_id, group_link_member_role FROM user_contact_links WHERE user_id = ? AND user_contact_link_id = ? |] @@ -511,7 +510,7 @@ getUserContactLinkViaShortLink db User {userId} shortLink = userContactLinkQuery :: Query userContactLinkQuery = [sql| - SELECT user_contact_link_id, conn_req_contact, short_link_contact, short_link_data_set, business_address, address_welcome_message, auto_accept, auto_accept_incognito, auto_reply_msg_content + SELECT user_contact_link_id, conn_req_contact, short_link_contact, short_link_data_set, business_address, auto_accept, auto_accept_incognito, auto_reply_msg_content FROM user_contact_links |] @@ -561,15 +560,15 @@ getContactWithoutConnViaShortAddress db vr user@User {userId} shortLink = do maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getContact db vr user) ctId_ updateUserAddressSettings :: DB.Connection -> Int64 -> AddressSettings -> IO () -updateUserAddressSettings db userContactLinkId AddressSettings {businessAddress, welcomeMessage, autoAccept, autoReply} = +updateUserAddressSettings db userContactLinkId AddressSettings {businessAddress, autoAccept, autoReply} = DB.execute db [sql| UPDATE user_contact_links - SET auto_accept = ?, auto_accept_incognito = ?, business_address = ?, address_welcome_message = ?, auto_reply_msg_content = ? + SET auto_accept = ?, auto_accept_incognito = ?, business_address = ?, auto_reply_msg_content = ? WHERE user_contact_link_id = ? |] - (autoAcceptValues :. (businessAddress, welcomeMessage, autoReply, userContactLinkId)) + (autoAcceptValues :. (businessAddress, autoReply, userContactLinkId)) where autoAcceptValues = case autoAccept of Just AutoAccept {acceptIncognito} -> (BI True, BI acceptIncognito) diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20250526_short_links.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20250526_short_links.hs index a2fa9c3d11..8109ca750d 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/M20250526_short_links.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20250526_short_links.hs @@ -11,6 +11,7 @@ m20250526_short_links = ALTER TABLE contacts ADD COLUMN conn_full_link_to_connect BLOB; ALTER TABLE contacts ADD COLUMN conn_short_link_to_connect BLOB; ALTER TABLE contacts ADD COLUMN welcome_shared_msg_id BLOB; +ALTER TABLE contacts ADD COLUMN request_shared_msg_id BLOB; ALTER TABLE contacts ADD COLUMN contact_request_id INTEGER REFERENCES contact_requests ON DELETE SET NULL; CREATE INDEX idx_contacts_contact_request_id ON contacts(contact_request_id); @@ -21,14 +22,15 @@ ALTER TABLE contact_requests ADD COLUMN welcome_shared_msg_id BLOB; ALTER TABLE contact_requests ADD COLUMN request_shared_msg_id BLOB; ALTER TABLE group_members ADD COLUMN member_xcontact_id BLOB; +ALTER TABLE group_members ADD COLUMN member_welcome_shared_msg_id BLOB; ALTER TABLE user_contact_links ADD COLUMN short_link_data_set INTEGER NOT NULL DEFAULT 0; -ALTER TABLE user_contact_links ADD COLUMN address_welcome_message TEXT; ALTER TABLE groups ADD COLUMN conn_full_link_to_connect BLOB; ALTER TABLE groups ADD COLUMN conn_short_link_to_connect BLOB; ALTER TABLE groups ADD COLUMN conn_link_started_connection INTEGER NOT NULL DEFAULT 0; ALTER TABLE groups ADD COLUMN welcome_shared_msg_id BLOB; +ALTER TABLE groups ADD COLUMN request_shared_msg_id BLOB; ALTER TABLE chat_items ADD COLUMN show_group_as_sender INTEGER NOT NULL DEFAULT 0; |] @@ -39,6 +41,7 @@ down_m20250526_short_links = ALTER TABLE contacts DROP COLUMN conn_full_link_to_connect; ALTER TABLE contacts DROP COLUMN conn_short_link_to_connect; ALTER TABLE contacts DROP COLUMN welcome_shared_msg_id; +ALTER TABLE contacts DROP COLUMN request_shared_msg_id; DROP INDEX idx_contacts_contact_request_id; ALTER TABLE contacts DROP COLUMN contact_request_id; @@ -49,14 +52,15 @@ ALTER TABLE contact_requests DROP COLUMN welcome_shared_msg_id; ALTER TABLE contact_requests DROP COLUMN request_shared_msg_id; ALTER TABLE group_members DROP COLUMN member_xcontact_id; +ALTER TABLE group_members DROP COLUMN member_welcome_shared_msg_id; ALTER TABLE user_contact_links DROP COLUMN short_link_data_set; -ALTER TABLE user_contact_links DROP COLUMN address_welcome_message; ALTER TABLE groups DROP COLUMN conn_full_link_to_connect; ALTER TABLE groups DROP COLUMN conn_short_link_to_connect; ALTER TABLE groups DROP COLUMN conn_link_started_connection; ALTER TABLE groups DROP COLUMN welcome_shared_msg_id; +ALTER TABLE groups DROP COLUMN request_shared_msg_id; ALTER TABLE chat_items DROP COLUMN show_group_as_sender; |] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt index a258eac2ab..3f46b68b1f 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt @@ -63,7 +63,7 @@ Query: g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, - g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_started_connection, g.welcome_shared_msg_id, + g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id, g.business_chat, g.business_member_id, g.customer_member_id, g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention, -- GroupInfo {membership} @@ -197,7 +197,7 @@ Query: SELECT -- Contact ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, - cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.contact_request_id, + cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.request_shared_msg_id, ct.contact_request_id, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, -- Connection c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, @@ -367,9 +367,9 @@ Plan: Query: INSERT INTO group_members ( group_id, member_id, member_role, member_category, member_status, invited_by, invited_by_group_member_id, - user_id, local_display_name, contact_id, contact_profile_id, member_xcontact_id, created_at, updated_at, + user_id, local_display_name, contact_id, contact_profile_id, member_xcontact_id, member_welcome_shared_msg_id, created_at, updated_at, peer_chat_min_version, peer_chat_max_version) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: @@ -384,7 +384,7 @@ Plan: Query: SELECT c.contact_profile_id, c.local_display_name, c.via_group, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, c.contact_used, c.contact_status, c.enable_ntfs, c.send_rcpts, c.favorite, - p.preferences, c.user_preferences, c.created_at, c.updated_at, c.chat_ts, c.conn_full_link_to_connect, c.conn_short_link_to_connect, c.welcome_shared_msg_id, c.contact_request_id, + p.preferences, c.user_preferences, c.created_at, c.updated_at, c.chat_ts, c.conn_full_link_to_connect, c.conn_short_link_to_connect, c.welcome_shared_msg_id, c.request_shared_msg_id, c.contact_request_id, c.contact_group_member_id, c.contact_grp_inv_sent, c.ui_themes, c.chat_deleted, c.custom_data, c.chat_item_ttl FROM contacts c JOIN contact_profiles p ON c.contact_profile_id = p.contact_profile_id @@ -712,6 +712,23 @@ SEARCH m USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN SEARCH g USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN SEARCH h USING INDEX idx_sent_probe_hashes_sent_probe_id (sent_probe_id=?) +Query: + SELECT xcontact_id, + connection_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, via_group_link, group_link_id, custom_user_profile_id, conn_status, conn_type, contact_conn_initiated, local_alias, + contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at, security_code, security_code_verified_at, pq_support, pq_encryption, pq_snd_enabled, pq_rcv_enabled, auth_err_counter, quota_err_counter, + conn_chat_version, peer_chat_min_version, peer_chat_max_version + FROM connections + WHERE (user_id = ? AND via_contact_uri_hash = ?) + OR (user_id = ? AND via_contact_uri_hash = ?) + LIMIT 1 + +Plan: +MULTI-INDEX OR +INDEX 1 +SEARCH connections USING INDEX idx_connections_via_contact_uri_hash (user_id=? AND via_contact_uri_hash=?) +INDEX 2 +SEARCH connections USING INDEX idx_connections_via_contact_uri_hash (user_id=? AND via_contact_uri_hash=?) + Query: UPDATE chat_items SET item_status = ?, updated_at = ? WHERE user_id = ? AND group_id = ? AND item_status = ? AND chat_item_id = ? @@ -916,36 +933,13 @@ SEARCH i USING INTEGER PRIMARY KEY (rowid=?) SEARCH f USING INDEX idx_files_chat_item_id (chat_item_id=?) LEFT-JOIN SEARCH ri USING COVERING INDEX idx_chat_items_direct_shared_msg_id (user_id=? AND contact_id=? AND shared_msg_id=?) LEFT-JOIN -Query: - SELECT - -- Contact - ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, - cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.contact_request_id, - ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, - -- Connection - c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, - c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, - c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version - FROM contacts ct - JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id - JOIN connections c ON c.contact_id = ct.contact_id - WHERE c.user_id = ? AND c.via_contact_uri_hash = ? AND ct.contact_status = ? AND ct.deleted = 0 - ORDER BY c.created_at DESC - LIMIT 1 - -Plan: -SEARCH c USING INDEX idx_connections_via_contact_uri_hash (user_id=? AND via_contact_uri_hash=?) -SEARCH ct USING INTEGER PRIMARY KEY (rowid=?) -SEARCH cp USING INTEGER PRIMARY KEY (rowid=?) -USE TEMP B-TREE FOR ORDER BY - Query: SELECT -- GroupInfo g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, - g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_started_connection, g.welcome_shared_msg_id, + g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id, g.business_chat, g.business_member_id, g.customer_member_id, g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention, -- GroupInfo {membership} @@ -990,12 +984,43 @@ SEARCH c USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN CORRELATED SCALAR SUBQUERY 1 SEARCH cc USING COVERING INDEX idx_connections_group_member (user_id=? AND group_member_id=?) +Query: + SELECT + c.xcontact_id, + -- Contact + ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, + cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.request_shared_msg_id, ct.contact_request_id, + ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, + -- Connection + c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, + c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, + c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version + FROM contacts ct + JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id + JOIN connections c ON c.contact_id = ct.contact_id + WHERE + ( (c.user_id = ? AND c.via_contact_uri_hash = ?) OR + (c.user_id = ? AND c.via_contact_uri_hash = ?) + ) AND ct.contact_status = ? AND ct.deleted = 0 + ORDER BY c.created_at DESC + LIMIT 1 + +Plan: +MULTI-INDEX OR +INDEX 1 +SEARCH c USING INDEX idx_connections_via_contact_uri_hash (user_id=? AND via_contact_uri_hash=?) +INDEX 2 +SEARCH c USING INDEX idx_connections_via_contact_uri_hash (user_id=? AND via_contact_uri_hash=?) +SEARCH ct USING INTEGER PRIMARY KEY (rowid=?) +SEARCH cp USING INTEGER PRIMARY KEY (rowid=?) +USE TEMP B-TREE FOR ORDER BY + Query: SELECT g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, - g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_started_connection, g.welcome_shared_msg_id, + g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id, g.business_chat, g.business_member_id, g.customer_member_id, g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention, mu.group_member_id, g.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, mu.member_restriction, @@ -1504,7 +1529,7 @@ Query: SELECT -- Contact ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, - cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.contact_request_id, + cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.request_shared_msg_id, ct.contact_request_id, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, -- Connection c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, @@ -1692,25 +1717,6 @@ SEARCH cr USING INDEX idx_contact_requests_updated_at (user_id=?) SEARCH p USING INTEGER PRIMARY KEY (rowid=?) SEARCH c USING INDEX idx_connections_user_contact_link_id (user_contact_link_id=?) -Query: - SELECT - cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, - cr.contact_id, cr.business_group_id, cr.user_contact_link_id, - c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, - cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, - cr.created_at, cr.updated_at, - cr.peer_chat_min_version, cr.peer_chat_max_version - FROM contact_requests cr - JOIN connections c USING (user_contact_link_id) - JOIN contact_profiles p USING (contact_profile_id) - WHERE cr.user_id = ? - AND cr.contact_request_id = ? - -Plan: -SEARCH cr USING INTEGER PRIMARY KEY (rowid=?) -SEARCH p USING INTEGER PRIMARY KEY (rowid=?) -SEARCH c USING INDEX idx_connections_user_contact_link_id (user_contact_link_id=?) - Query: SELECT created_at, updated_at, chat_ts, favorite, unread_chat @@ -3511,7 +3517,7 @@ Plan: SEARCH usage_conditions USING INTEGER PRIMARY KEY (rowid=?) Query: - SELECT user_contact_link_id, conn_req_contact, short_link_contact, short_link_data_set, business_address, address_welcome_message, auto_accept, auto_accept_incognito, auto_reply_msg_content, group_id, group_link_member_role + SELECT user_contact_link_id, conn_req_contact, short_link_contact, short_link_data_set, business_address, auto_accept, auto_accept_incognito, auto_reply_msg_content, group_id, group_link_member_role FROM user_contact_links WHERE user_id = ? AND user_contact_link_id = ? @@ -4620,7 +4626,7 @@ SEARCH server_operators USING INTEGER PRIMARY KEY (rowid=?) Query: UPDATE user_contact_links - SET auto_accept = ?, auto_accept_incognito = ?, business_address = ?, address_welcome_message = ?, auto_reply_msg_content = ? + SET auto_accept = ?, auto_accept_incognito = ?, business_address = ?, auto_reply_msg_content = ? WHERE user_contact_link_id = ? Plan: @@ -4686,7 +4692,7 @@ Query: g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, - g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_started_connection, g.welcome_shared_msg_id, + g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id, g.business_chat, g.business_member_id, g.customer_member_id, g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention, -- GroupMember - membership @@ -4712,7 +4718,7 @@ Query: g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, - g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_started_connection, g.welcome_shared_msg_id, + g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id, g.business_chat, g.business_member_id, g.customer_member_id, g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention, -- GroupMember - membership @@ -4732,6 +4738,40 @@ SEARCH gp USING INTEGER PRIMARY KEY (rowid=?) SEARCH mu USING INDEX idx_group_members_contact_id (contact_id=?) SEARCH pu USING INTEGER PRIMARY KEY (rowid=?) +Query: + SELECT + cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, + cr.contact_id, cr.business_group_id, cr.user_contact_link_id, + c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, + cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, + cr.created_at, cr.updated_at, + cr.peer_chat_min_version, cr.peer_chat_max_version + FROM contact_requests cr + JOIN connections c USING (user_contact_link_id) + JOIN contact_profiles p USING (contact_profile_id) + WHERE cr.user_id = ? AND cr.business_group_id = ? +Plan: +SEARCH cr USING INDEX idx_contact_requests_business_group_id (business_group_id=?) +SEARCH p USING INTEGER PRIMARY KEY (rowid=?) +SEARCH c USING INDEX idx_connections_user_contact_link_id (user_contact_link_id=?) + +Query: + SELECT + cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, + cr.contact_id, cr.business_group_id, cr.user_contact_link_id, + c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, + cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, + cr.created_at, cr.updated_at, + cr.peer_chat_min_version, cr.peer_chat_max_version + FROM contact_requests cr + JOIN connections c USING (user_contact_link_id) + JOIN contact_profiles p USING (contact_profile_id) + WHERE cr.user_id = ? AND cr.contact_request_id = ? +Plan: +SEARCH cr USING INTEGER PRIMARY KEY (rowid=?) +SEARCH p USING INTEGER PRIMARY KEY (rowid=?) +SEARCH c USING INDEX idx_connections_user_contact_link_id (user_contact_link_id=?) + Query: SELECT m.group_member_id, m.group_id, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, @@ -5171,21 +5211,21 @@ Plan: SCAN usage_conditions Query: - SELECT user_contact_link_id, conn_req_contact, short_link_contact, short_link_data_set, business_address, address_welcome_message, auto_accept, auto_accept_incognito, auto_reply_msg_content + SELECT user_contact_link_id, conn_req_contact, short_link_contact, short_link_data_set, business_address, auto_accept, auto_accept_incognito, auto_reply_msg_content FROM user_contact_links WHERE user_id = ? AND conn_req_contact IN (?,?) Plan: SEARCH user_contact_links USING INDEX sqlite_autoindex_user_contact_links_1 (user_id=?) Query: - SELECT user_contact_link_id, conn_req_contact, short_link_contact, short_link_data_set, business_address, address_welcome_message, auto_accept, auto_accept_incognito, auto_reply_msg_content + SELECT user_contact_link_id, conn_req_contact, short_link_contact, short_link_data_set, business_address, auto_accept, auto_accept_incognito, auto_reply_msg_content FROM user_contact_links WHERE user_id = ? AND local_display_name = '' AND group_id IS NULL Plan: SEARCH user_contact_links USING INDEX sqlite_autoindex_user_contact_links_1 (user_id=? AND local_display_name=?) Query: - SELECT user_contact_link_id, conn_req_contact, short_link_contact, short_link_data_set, business_address, address_welcome_message, auto_accept, auto_accept_incognito, auto_reply_msg_content + SELECT user_contact_link_id, conn_req_contact, short_link_contact, short_link_data_set, business_address, auto_accept, auto_accept_incognito, auto_reply_msg_content FROM user_contact_links WHERE user_id = ? AND short_link_contact = ? Plan: @@ -5902,6 +5942,10 @@ Query: SELECT max(active_order) FROM users Plan: SEARCH users +Query: SELECT member_xcontact_id, member_welcome_shared_msg_id FROM group_members WHERE user_id = ? AND group_id = ? AND group_member_id = ? +Plan: +SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) + Query: SELECT note_folder_id FROM note_folders WHERE user_id = ? Plan: SEARCH note_folders USING COVERING INDEX note_folders_user_id (user_id=?) @@ -5934,10 +5978,6 @@ Query: SELECT user_id FROM users WHERE local_display_name = ? Plan: SEARCH users USING COVERING INDEX sqlite_autoindex_users_2 (local_display_name=?) -Query: SELECT xcontact_id FROM connections WHERE user_id = ? AND via_contact_uri_hash = ? LIMIT 1 -Plan: -SEARCH connections USING INDEX idx_connections_via_contact_uri_hash (user_id=? AND via_contact_uri_hash=?) - Query: SELECT xgrplinkmem_received FROM group_members WHERE group_member_id = ? Plan: SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) @@ -6038,6 +6078,10 @@ Query: UPDATE contacts SET local_display_name = ?, updated_at = ? WHERE user_id Plan: SEARCH contacts USING INTEGER PRIMARY KEY (rowid=?) +Query: UPDATE contacts SET request_shared_msg_id = ? WHERE contact_id = ? +Plan: +SEARCH contacts USING INTEGER PRIMARY KEY (rowid=?) + Query: UPDATE contacts SET send_rcpts = NULL Plan: SCAN contacts @@ -6166,6 +6210,10 @@ Query: UPDATE groups SET local_display_name = ?, updated_at = ? WHERE user_id = Plan: SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) +Query: UPDATE groups SET request_shared_msg_id = ? WHERE group_id = ? +Plan: +SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) + Query: UPDATE groups SET send_rcpts = NULL Plan: SCAN groups diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql b/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql index 78207bc64f..dc5db54884 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql @@ -82,6 +82,7 @@ CREATE TABLE contacts( conn_full_link_to_connect BLOB, conn_short_link_to_connect BLOB, welcome_shared_msg_id BLOB, + request_shared_msg_id BLOB, contact_request_id INTEGER REFERENCES contact_requests ON DELETE SET NULL, FOREIGN KEY(user_id, local_display_name) REFERENCES display_names(user_id, local_display_name) @@ -143,7 +144,8 @@ CREATE TABLE groups( conn_full_link_to_connect BLOB, conn_short_link_to_connect BLOB, conn_link_started_connection INTEGER NOT NULL DEFAULT 0, - welcome_shared_msg_id BLOB, -- received + welcome_shared_msg_id BLOB, + request_shared_msg_id BLOB, -- received FOREIGN KEY(user_id, local_display_name) REFERENCES display_names(user_id, local_display_name) ON DELETE CASCADE @@ -182,6 +184,7 @@ CREATE TABLE group_members( support_chat_items_mentions INTEGER NOT NULL DEFAULT 0, support_chat_last_msg_from_member_ts TEXT, member_xcontact_id BLOB, + member_welcome_shared_msg_id BLOB, FOREIGN KEY(user_id, local_display_name) REFERENCES display_names(user_id, local_display_name) ON DELETE CASCADE @@ -336,7 +339,6 @@ CREATE TABLE user_contact_links( business_address INTEGER DEFAULT 0, short_link_contact BLOB, short_link_data_set INTEGER NOT NULL DEFAULT 0, - address_welcome_message TEXT, UNIQUE(user_id, local_display_name) ); CREATE TABLE contact_requests( diff --git a/src/Simplex/Chat/Store/Shared.hs b/src/Simplex/Chat/Store/Shared.hs index afd7915256..5ff809b33d 100644 --- a/src/Simplex/Chat/Store/Shared.hs +++ b/src/Simplex/Chat/Store/Shared.hs @@ -431,7 +431,22 @@ deleteUnusedIncognitoProfileById_ db User {userId} profileId = |] (userId, profileId, userId, profileId, userId, profileId) -type PreparedContactRow = (Maybe AConnectionRequestUri, Maybe AConnShortLink, Maybe SharedMsgId) +deleteIncognitoConnectionProfile :: DB.Connection -> UserId -> Connection -> IO () +deleteIncognitoConnectionProfile db userId Connection {connId, customUserProfileId} = + forM_ customUserProfileId $ \profileId -> do + DB.execute db "UPDATE connections SET custom_user_profile_id = NULL WHERE connection_id = ?" (Only connId) + DB.execute + db + [sql| + DELETE FROM contact_profiles + WHERE user_id = ? AND contact_profile_id = ? + AND NOT EXISTS (SELECT 1 FROM contacts WHERE contact_profile_id = ?) + AND NOT EXISTS (SELECT 1 FROM contact_requests WHERE contact_profile_id = ?) + AND NOT EXISTS (SELECT 1 FROM group_members WHERE contact_profile_id = ? OR member_profile_id = ?) + |] + (userId, profileId, profileId, profileId, profileId, profileId) + +type PreparedContactRow = (Maybe AConnectionRequestUri, Maybe AConnShortLink, Maybe SharedMsgId, Maybe SharedMsgId) type ContactRow' = (ProfileId, ContactName, Maybe Int64, ContactName, Text, Maybe ImageData, Maybe ConnLinkContact, LocalAlias, BoolInt, ContactStatus) :. (Maybe MsgFilter, Maybe BoolInt, BoolInt, Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime) :. PreparedContactRow :. (Maybe Int64, Maybe GroupMemberId, BoolInt, Maybe UIThemeEntityOverrides, BoolInt, Maybe CustomData, Maybe Int64) @@ -448,8 +463,9 @@ toContact vr user chatTags ((Only contactId :. (profileId, localDisplayName, via in Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, preparedContact, contactRequestId, contactGroupMemberId, contactGrpInvSent, chatTags, chatItemTTL, uiThemes, chatDeleted, customData} toPreparedContact :: PreparedContactRow -> Maybe PreparedContact -toPreparedContact (connFullLink, connShortLink, welcomeSharedMsgId) = - (\cl@(ACCL m _) -> PreparedContact cl (connMode m) welcomeSharedMsgId) <$> toACreatedConnLink_ connFullLink connShortLink +toPreparedContact (connFullLink, connShortLink, welcomeSharedMsgId, requestSharedMsgId) = + (\cl@(ACCL m _) -> PreparedContact {connLinkToConnect = cl, uiConnLinkType = connMode m, welcomeSharedMsgId, requestSharedMsgId}) + <$> toACreatedConnLink_ connFullLink connShortLink toACreatedConnLink_ :: Maybe AConnectionRequestUri -> Maybe AConnShortLink -> Maybe ACreatedConnLink toACreatedConnLink_ Nothing _ = Nothing @@ -604,7 +620,7 @@ safeDeleteLDN db User {userId} localDisplayName = do |] (userId, localDisplayName, userId) -type PreparedGroupRow = (Maybe ConnReqContact, Maybe ShortLinkContact, BoolInt, Maybe SharedMsgId) +type PreparedGroupRow = (Maybe ConnReqContact, Maybe ShortLinkContact, BoolInt, Maybe SharedMsgId, Maybe SharedMsgId) type BusinessChatInfoRow = (Maybe BusinessChatType, Maybe MemberId, Maybe MemberId) @@ -624,8 +640,8 @@ toGroupInfo vr userContactId chatTags ((groupId, localDisplayName, displayName, toPreparedGroup :: PreparedGroupRow -> Maybe PreparedGroup toPreparedGroup = \case - (Just fullLink, shortLink_, BI connLinkStartedConnection, welcomeSharedMsgId) -> - Just PreparedGroup {connLinkToConnect = CCLink fullLink shortLink_, connLinkStartedConnection, welcomeSharedMsgId} + (Just fullLink, shortLink_, BI connLinkStartedConnection, welcomeSharedMsgId, requestSharedMsgId) -> + Just PreparedGroup {connLinkToConnect = CCLink fullLink shortLink_, connLinkStartedConnection, welcomeSharedMsgId, requestSharedMsgId} _ -> Nothing toGroupMember :: Int64 -> GroupMemberRow -> GroupMember @@ -660,7 +676,7 @@ groupInfoQuery = g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, - g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_started_connection, g.welcome_shared_msg_id, + g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id, g.business_chat, g.business_member_id, g.customer_member_id, g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention, -- GroupMember - membership diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index c9059dace7..eb61988f8a 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -207,7 +207,8 @@ contactRequestId' Contact {contactRequestId} = contactRequestId data PreparedContact = PreparedContact { connLinkToConnect :: ACreatedConnLink, uiConnLinkType :: ConnectionMode, - welcomeSharedMsgId :: Maybe SharedMsgId + welcomeSharedMsgId :: Maybe SharedMsgId, + requestSharedMsgId :: Maybe SharedMsgId } deriving (Eq, Show) @@ -404,8 +405,15 @@ data RequestEntity type RepeatRequest = Bool data RequestStage - = RSAcceptedRequest (Maybe UserContactRequest) RequestEntity -- Optional request is for legacy deleted requests - | RSCurrentRequest UserContactRequest (Maybe RequestEntity) RepeatRequest -- Optional entity is for legacy requests without entity + = RSAcceptedRequest + { acceptedRequest :: Maybe UserContactRequest, -- Request is optional to support deleted legacy requests + requestEntity :: RequestEntity + } + | RSCurrentRequest + { previousRequest :: Maybe UserContactRequest, + currentRequest :: UserContactRequest, + requestEntity_ :: Maybe RequestEntity -- Entity is optional to support legacy requests without entity + } type UserName = Text @@ -488,7 +496,8 @@ instance ToField BusinessChatType where toField = toField . textEncode data PreparedGroup = PreparedGroup { connLinkToConnect :: CreatedLinkContact, connLinkStartedConnection :: Bool, - welcomeSharedMsgId :: Maybe SharedMsgId -- it is stored only for business chats, and only if welcome message is specified + welcomeSharedMsgId :: Maybe SharedMsgId, -- it is stored only for business chats, and only if welcome message is specified + requestSharedMsgId :: Maybe SharedMsgId } deriving (Eq, Show) @@ -715,18 +724,6 @@ instance ToField ImageData where toField (ImageData t) = toField t deriving newtype instance FromField ImageData -data ContactShortLinkData = ContactShortLinkData - { profile :: Profile, - message :: Maybe Text, - business :: Bool - } - deriving (Show) - -data GroupShortLinkData = GroupShortLinkData - { groupProfile :: GroupProfile - } - deriving (Show) - data CReqClientData = CRDataGroup {groupLinkId :: GroupLinkId} newtype GroupLinkId = GroupLinkId {unGroupLinkId :: ByteString} -- used to identify invitation via group link @@ -1998,10 +1995,6 @@ instance FromField MsgFilter where fromField = fromIntField_ msgFilterIntP instance ToField MsgFilter where toField = toField . msgFilterInt -$(JQ.deriveJSON defaultJSON ''ContactShortLinkData) - -$(JQ.deriveJSON defaultJSON ''GroupShortLinkData) - $(JQ.deriveJSON defaultJSON ''CReqClientData) $(JQ.deriveJSON defaultJSON ''MemberIdRole) diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 8602e4670c..0f5ec0345e 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -1084,7 +1084,7 @@ simplexChatContact' = \case -- TODO [short links] show all settings viewAddressSettings :: AddressSettings -> [StyledString] -viewAddressSettings AddressSettings {businessAddress, welcomeMessage = _, autoAccept, autoReply} = case autoAccept of +viewAddressSettings AddressSettings {businessAddress, autoAccept, autoReply} = case autoAccept of Just AutoAccept {acceptIncognito} -> ("auto_accept on" <> aaInfo) : maybe [] ((["auto reply:"] <>) . ttyMsgContent) autoReply diff --git a/tests/ChatTests/Profiles.hs b/tests/ChatTests/Profiles.hs index 1843124084..d41301f212 100644 --- a/tests/ChatTests/Profiles.hs +++ b/tests/ChatTests/Profiles.hs @@ -41,7 +41,7 @@ chatProfileTests = do it "use multiword profile names" testMultiWordProfileNames describe "user contact link" $ do it "create and connect via contact link" testUserContactLink - it "retry accepting connection via contact link" testRetryAcceptingViaContactLink + it "retry connecting via contact link" testRetryConnectingViaContactLink it "add contact link to profile" testProfileLink it "auto accept contact requests" testUserContactLinkAutoAccept it "deduplicate contact requests" testDeduplicateContactRequests @@ -120,9 +120,14 @@ chatProfileTests = do it "prepare contact using address short link data and connect" testShortLinkAddressPrepareContact it "prepare contact via invitation and connect after it is deleted" testShortLinkDeletedInvitation it "prepare contact via address and connect after it is deleted" testShortLinkDeletedAddress + it "prepare contact via address and connect with retry after error" testShortLinkAddressConnectRetry + it "prepare contact via address and connect incognito with retry" testShortLinkAddressConnectRetryIncognito it "prepare business chat using address short link data and connect" testShortLinkAddressPrepareBusiness + it "connect to business address with request message" testBusinessAddressRequestMessage it "prepare group using group short link data and connect" testShortLinkPrepareGroup it "prepare group using group short link data and connect, host rejects" testShortLinkPrepareGroupReject + it "connect to group with welcome message via short link" testGroupShortLinkWelcome + it "retry connecting to group via short link" testShortLinkGroupRetry it "connect to prepared contact incognito (via invitation)" testShortLinkInvitationConnectPreparedContactIncognito it "connect to prepared contact incognito (via address)" testShortLinkAddressConnectPreparedContactIncognito it "change prepared contact user" testShortLinkChangePreparedContactUser @@ -303,8 +308,8 @@ testUserContactLink = alice @@@ [("@cath", lastChatFeature), ("@bob", "hey")] alice <##> cath -testRetryAcceptingViaContactLink :: HasCallStack => TestParams -> IO () -testRetryAcceptingViaContactLink ps = testChatCfgOpts2 cfg' opts' aliceProfile bobProfile test ps +testRetryConnectingViaContactLink :: HasCallStack => TestParams -> IO () +testRetryConnectingViaContactLink ps = testChatCfgOpts2 cfg' opts' aliceProfile bobProfile test ps where tmp = tmpPath ps test alice bob = do @@ -3048,6 +3053,119 @@ testShortLinkDeletedAddress = bob ##> "/_connect contact @2" bob <##. "error: connection authorization failed" +testShortLinkAddressConnectRetry :: HasCallStack => TestParams -> IO () +testShortLinkAddressConnectRetry ps = + withNewTestChatOpts ps opts' "alice" aliceProfile $ \alice -> + withNewTestChatOpts ps opts' "bob" bobProfile $ \bob -> do + withSmpServer' serverCfg' $ do + alice ##> "/ad" + (shortLink, fullLink) <- getContactLinks alice True + bob ##> ("/_connect plan 1 " <> shortLink) + bob <## "contact address: ok to connect" + contactSLinkData <- getTermLine bob + bob ##> ("/_prepare contact 1 " <> fullLink <> " " <> shortLink <> " " <> contactSLinkData) + bob <## "alice: contact is prepared" + alice <## "server disconnected localhost ()" + bob ##> "/_connect contact @2 text hello" + bob <##. "smp agent error: BROKER" + withSmpServer' serverCfg' $ do + alice <## "server connected localhost ()" + threadDelay 250000 + bob ##> "/_connect contact @2 text hello" + bob + <### [ "alice: connection started", + WithTime "@alice hello" + ] + alice + <### [ "bob (Bob) wants to connect to you!", + WithTime "bob> hello" + ] + alice <## "to accept: /ac bob" + alice <## "to reject: /rc bob (the sender will NOT be notified)" + alice ##> "/ac bob" + alice <## "bob (Bob): accepting contact request, you can send messages to contact" + concurrently_ + (bob <## "alice (Alice): contact is connected") + (alice <## "bob (Bob): contact is connected") + alice <##> bob + alice <## "server disconnected localhost (@bob)" + bob <## "server disconnected localhost (@alice)" + where + tmp = tmpPath ps + serverCfg' = + smpServerCfg + { transports = [("7003", transport @TLS, False)], + serverStoreCfg = persistentServerStoreCfg tmp + } + opts' = + testOpts + { coreOptions = + testCoreOpts + { smpServers = ["smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7003"] + } + } + +testShortLinkAddressConnectRetryIncognito :: HasCallStack => TestParams -> IO () +testShortLinkAddressConnectRetryIncognito ps = + withNewTestChatOpts ps opts' "alice" aliceProfile $ \alice -> + withNewTestChatOpts ps opts' "bob" bobProfile $ \bob -> do + withSmpServer' serverCfg' $ do + alice ##> "/ad" + (shortLink, fullLink) <- getContactLinks alice True + bob ##> ("/_connect plan 1 " <> shortLink) + bob <## "contact address: ok to connect" + contactSLinkData <- getTermLine bob + bob ##> ("/_prepare contact 1 " <> fullLink <> " " <> shortLink <> " " <> contactSLinkData) + bob <## "alice: contact is prepared" + alice <## "server disconnected localhost ()" + bob ##> "/_connect contact @2 incognito=on text hello" + bob <##. "smp agent error: BROKER" + bobIncognito <- withSmpServer' serverCfg' $ do + alice <## "server connected localhost ()" + threadDelay 250000 + bob ##> "/_connect contact @2 incognito=on text hello" + bobIncognito <- getTermLine bob + bob + <### [ "alice: connection started incognito", + WithTime "i @alice hello" + ] + alice + <### [ ConsoleString (bobIncognito <> " wants to connect to you!"), + WithTime (bobIncognito <> "> hello") + ] + alice <## ("to accept: /ac " <> bobIncognito) + alice <## ("to reject: /rc " <> bobIncognito <> " (the sender will NOT be notified)") + alice ##> ("/ac " <> bobIncognito) + alice <## (bobIncognito <> ": accepting contact request, you can send messages to contact") + concurrentlyN_ + [ do + _ <- getTermLine bob + bob <## ("alice (Alice): contact is connected, your incognito profile for this contact is " <> bobIncognito) + bob <## "use /i alice to print out this incognito profile again", + alice <## (bobIncognito <> ": contact is connected") + ] + alice #> ("@" <> bobIncognito <> " hi") + bob ?<# "alice> hi" + bob ?#> "@alice hey" + alice <# (bobIncognito <> "> hey") + pure bobIncognito + alice <## ("server disconnected localhost (@" <> bobIncognito <> ")") + bob <## "server disconnected localhost (@alice)" + where + tmp = tmpPath ps + serverCfg' = + smpServerCfg + { transports = [("7003", transport @TLS, False)], + serverStoreCfg = persistentServerStoreCfg tmp + } + opts' = + testOpts + { coreOptions = + testCoreOpts + { smpServers = ["smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7003"] + } + } + testShortLinkAddressPrepareBusiness :: HasCallStack => TestParams -> IO () testShortLinkAddressPrepareBusiness = testChat3 businessProfile aliceProfile {fullName = "Alice @ Biz"} bobProfile $ @@ -3101,6 +3219,54 @@ testShortLinkAddressPrepareBusiness = (alice <# "#bob bob_1> hey there") (biz <# "#bob bob_1> hey there") +testBusinessAddressRequestMessage :: HasCallStack => TestParams -> IO () +testBusinessAddressRequestMessage = + testChat3 businessProfile aliceProfile {fullName = "Alice @ Biz"} bobProfile $ + \biz alice bob -> do + biz ##> "/ad" + (shortLink, fullLink) <- getContactLinks biz True + biz ##> "/auto_accept on business text Welcome!" + biz <## "auto_accept on, business" + biz <## "auto reply:" + biz <## "Welcome!" + bob ##> ("/_connect plan 1 " <> shortLink) + bob <## "business link: ok to connect" + contactSLinkData <- getTermLine bob + bob ##> ("/_prepare contact 1 " <> fullLink <> " " <> shortLink <> " " <> contactSLinkData) + bob <## "#biz: group is prepared" + bob #$> ("/_get chat #1 count=100", chat, businessGroupFeatures <> [(0, "Welcome!")]) + bob ##> "/_connect group #1 text Hello!" + bob + <### + [ "#biz: connection started", + WithTime "#biz Hello!" + ] + biz <# "#bob bob_1> Hello!" + biz <## "#bob (Bob): accepting business address request..." + bob <## "#biz: joining the group..." + biz <## "#bob: bob_1 joined the group" + bob <## "#biz: you joined the group" + -- Another member should receive history + connectUsers biz alice + biz ##> "/a bob alice" + biz <## "invitation to join the group #bob sent to alice" + alice <## "#bob (Bob): biz invites you to join the group as member" + alice <## "use /j bob to accept" + alice ##> "/j bob" + concurrentlyN_ + [ alice + <### + [ "#bob: you joined the group", + WithTime "#bob biz> Welcome! [>>]", + WithTime "#bob bob_1> Hello! [>>]", + "#bob: member bob_1 (Bob) is connected" + ], + biz <## "#bob: alice joined the group", + do + bob <## "#biz: biz_1 added alice (Alice @ Biz) to the group (connecting...)" + bob <## "#biz: new member alice is connected" + ] + testShortLinkPrepareGroup :: HasCallStack => TestParams -> IO () testShortLinkPrepareGroup = testChat3 aliceProfile bobProfile cathProfile $ @@ -3162,6 +3328,96 @@ testShortLinkPrepareGroupReject = where cfg = testCfg {chatHooks = defaultChatHooks {acceptMember = Just (\_ _ _ -> pure $ Left GRRBlockedName)}} +testGroupShortLinkWelcome :: HasCallStack => TestParams -> IO () +testGroupShortLinkWelcome = + testChat2 aliceProfile bobProfile $ \alice bob -> do + alice ##> "/g team" + alice <## "group #team is created" + alice <## "to add members use /a team or /create link #team" + alice ##> "/set welcome #team Welcome!" + alice <## "description changed to:" + alice <## "Welcome!" + alice ##> "/create link #team" + (shortLink, fullLink) <- getGroupLinks alice "team" GRMember True + bob ##> ("/_connect plan 1 " <> shortLink) + bob <## "group link: ok to connect" + groupSLinkData <- getTermLine bob + bob ##> ("/_prepare group 1 " <> fullLink <> " " <> shortLink <> " " <> groupSLinkData) + bob <## "#team: group is prepared" + bob #$> ("/_get chat #1 count=100", chat, groupFeaturesNoE2E <> [(0, "Welcome!")]) + threadDelay 1000000 -- TODO [short links] to compensate for rounding of timestamps of received messages + bob ##> "/_connect group #1" + bob <## "#team: connection started" + alice <## "bob (Bob): accepting request to join group #team..." + concurrentlyN_ + [ alice <## "#team: bob joined the group", + do + bob <## "#team: joining the group..." + bob <## "#team: you joined the group" + ] + bob #$> ("/_get chat #1 count=100", chat, groupFeaturesNoE2E <> [(0, "Welcome!"), (0, e2eeInfoNoPQStr), (0, "connected")]) + alice #> "#team 1" + bob <# "#team alice> 1" + bob #> "#team 2" + alice <# "#team bob> 2" + +testShortLinkGroupRetry :: HasCallStack => TestParams -> IO () +testShortLinkGroupRetry ps = testChatOpts2 opts' aliceProfile bobProfile test ps + where + test alice bob = do + withSmpServer' serverCfg' $ do + connectUsers alice bob + alice ##> "/g team" + alice <## "group #team is created" + alice <## "to add members use /a team or /create link #team" + alice ##> "/create link #team" + (shortLink, fullLink) <- getGroupLinks alice "team" GRMember True + bob ##> ("/_connect plan 1 " <> shortLink) + bob <## "group link: ok to connect" + groupSLinkData <- getTermLine bob + bob ##> ("/_prepare group 1 " <> fullLink <> " " <> shortLink <> " " <> groupSLinkData) + bob <## "#team: group is prepared" + alice <## "server disconnected localhost (@bob)" + bob <## "server disconnected localhost (@alice)" + bob ##> "/_connect group #1" + bob <##. "smp agent error: BROKER" + withSmpServer' serverCfg' $ do + alice <## "server connected localhost (@bob)" + bob <## "server connected localhost (@alice)" + threadDelay 250000 + bob ##> "/_connect group #1" + bob <## "#team: connection started" + alice <## "bob_1 (Bob): accepting request to join group #team..." + concurrentlyN_ + [ alice <## "#team: bob_1 joined the group", + do + bob <## "#team: joining the group..." + bob <## "#team: you joined the group" + ] + alice <## "contact and member are merged: bob, #team bob_1" + alice <## "use @bob to send messages" + bob <## "contact and member are merged: alice, #team alice_1" + bob <## "use @alice to send messages" + alice #> "#team 1" + bob <# "#team alice> 1" + bob #> "#team 2" + alice <# "#team bob> 2" + alice <## "server disconnected localhost (@bob)" + bob <## "server disconnected localhost (@alice)" + tmp = tmpPath ps + serverCfg' = + smpServerCfg + { transports = [("7003", transport @TLS, False)], + serverStoreCfg = persistentServerStoreCfg tmp + } + opts' = + testOpts + { coreOptions = + testCoreOpts + { smpServers = ["smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7003"] + } + } + testShortLinkInvitationConnectPreparedContactIncognito :: HasCallStack => TestParams -> IO () testShortLinkInvitationConnectPreparedContactIncognito = testChat2 aliceProfile bobProfile $ @@ -3662,8 +3918,9 @@ testShortLinkAddressChangeAutoReply = alice <# "bob> hello" alice <## "bob (Bob): accepting contact request..." alice <## "bob (Bob): you can send messages to contact" - alice <# "@bob welcome!" -- auto reply - bob <# "alice> welcome!" + -- welcome messages, not sent as events + -- alice <# "@bob welcome!" + -- bob <# "alice> welcome!" concurrently_ (bob <## "alice (Alice): contact is connected") (alice <## "bob (Bob): contact is connected") diff --git a/tests/ChatTests/Utils.hs b/tests/ChatTests/Utils.hs index c3856069ac..38b16379da 100644 --- a/tests/ChatTests/Utils.hs +++ b/tests/ChatTests/Utils.hs @@ -293,6 +293,9 @@ lastChatFeature = snd $ last chatFeatures groupFeatures :: [(Int, String)] groupFeatures = map (\(a, _, _) -> a) $ groupFeatures'' 0 +groupFeaturesNoE2E :: [(Int, String)] +groupFeaturesNoE2E = map (\(a, _, _) -> a) $ groupFeatures_ 0 + sndGroupFeatures :: [(Int, String)] sndGroupFeatures = map (\(a, _, _) -> a) $ groupFeatures'' 1 @@ -300,9 +303,11 @@ groupFeatureStrs :: [String] groupFeatureStrs = map (\(a, _, _) -> snd a) $ groupFeatures'' 0 groupFeatures'' :: Int -> [((Int, String), Maybe (Int, String), Maybe String)] -groupFeatures'' dir = - [ ((dir, e2eeInfoNoPQStr), Nothing, Nothing), - ((dir, "Disappearing messages: off"), Nothing, Nothing), +groupFeatures'' dir = ((dir, e2eeInfoNoPQStr), Nothing, Nothing) : groupFeatures_ dir + +groupFeatures_ :: Int -> [((Int, String), Maybe (Int, String), Maybe String)] +groupFeatures_ dir = + [ ((dir, "Disappearing messages: off"), Nothing, Nothing), ((dir, "Direct messages: on"), Nothing, Nothing), ((dir, "Full deletion: off"), Nothing, Nothing), ((dir, "Message reactions: on"), Nothing, Nothing), @@ -313,6 +318,23 @@ groupFeatures'' dir = ((dir, "Recent history: on"), Nothing, Nothing) ] +businessGroupFeatures :: [(Int, String)] +businessGroupFeatures = map (\(a, _, _) -> a) $ businessGroupFeatures'' 0 + +businessGroupFeatures'' :: Int -> [((Int, String), Maybe (Int, String), Maybe String)] +businessGroupFeatures'' dir = + -- [ ((dir, e2eeInfoNoPQStr), Nothing, Nothing), + [ ((dir, "Disappearing messages: on"), Nothing, Nothing), + ((dir, "Direct messages: off"), Nothing, Nothing), + ((dir, "Full deletion: off"), Nothing, Nothing), + ((dir, "Message reactions: on"), Nothing, Nothing), + ((dir, "Voice messages: on"), Nothing, Nothing), + ((dir, "Files and media: on"), Nothing, Nothing), + ((dir, "SimpleX links: on"), Nothing, Nothing), + ((dir, "Member reports: off"), Nothing, Nothing), + ((dir, "Recent history: on"), Nothing, Nothing) + ] + itemId :: Int -> String itemId i = show $ length chatFeatures + i