core: allow to accept contact requests after address is deleted (#6032)

* core: allow to accept contact requests after address is deleted

* update

* update

* plans

* ios, postgres migration

* schema

* request lock, refactor

* update simplexmq

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
This commit is contained in:
spaced4ndy
2025-07-03 10:34:11 +00:00
committed by GitHub
parent 4f227518f1
commit 79041390f1
21 changed files with 243 additions and 107 deletions
@@ -996,7 +996,11 @@ func receivedMsgNtf(_ res: NSEChatEvent) async -> (String, NSENotificationData)?
// case let .contactConnecting(contact):
// TODO profile update
case let .receivedContactRequest(user, contactRequest):
return (UserContact(contactRequest: contactRequest).id, .contactRequest(user, contactRequest))
if let userContactLinkId = contactRequest.userContactLinkId_ {
return (UserContact(userContactLinkId: userContactLinkId).id, .contactRequest(user, contactRequest))
} else {
return nil
}
case let .newChatItems(user, chatItems):
// Received items are created one at a time
if let chatItem = chatItems.first {
+2 -6
View File
@@ -1908,10 +1908,6 @@ public struct UserContact: Decodable, Hashable {
self.userContactLinkId = userContactLinkId
}
public init(contactRequest: UserContactRequest) {
self.userContactLinkId = contactRequest.userContactLinkId
}
public var id: String {
"@>\(userContactLinkId)"
}
@@ -1919,7 +1915,7 @@ public struct UserContact: Decodable, Hashable {
public struct UserContactRequest: Decodable, NamedChat, Hashable {
var contactRequestId: Int64
public var userContactLinkId: Int64
public var userContactLinkId_: Int64?
public var cReqChatVRange: VersionRange
var localDisplayName: ContactName
var profile: Profile
@@ -1936,7 +1932,7 @@ public struct UserContactRequest: Decodable, NamedChat, Hashable {
public static let sampleData = UserContactRequest(
contactRequestId: 1,
userContactLinkId: 1,
userContactLinkId_: 1,
cReqChatVRange: VersionRange(1, 1),
localDisplayName: "alice",
profile: Profile.sampleData,
+1 -1
View File
@@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd
source-repository-package
type: git
location: https://github.com/simplex-chat/simplexmq.git
tag: a46edd60f0e2460ec4a7d6fb371412f32e988357
tag: c5eb66038bb9268671a353638606f6d0bd1de761
source-repository-package
type: git
+1 -1
View File
@@ -1,5 +1,5 @@
{
"https://github.com/simplex-chat/simplexmq.git"."a46edd60f0e2460ec4a7d6fb371412f32e988357" = "0vix4s9nfxrwiy56rc294s9ik7l9rar4zg2pvl1c4v914lsphybq";
"https://github.com/simplex-chat/simplexmq.git"."c5eb66038bb9268671a353638606f6d0bd1de761" = "08b1lqwpmcn139c09lr71gmh5kgxlcdfhvwlnxxafr26m4nc003w";
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
"https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d";
"https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl";
+2
View File
@@ -108,6 +108,7 @@ library
Simplex.Chat.Store.Postgres.Migrations.M20250512_member_admission
Simplex.Chat.Store.Postgres.Migrations.M20250513_group_scope
Simplex.Chat.Store.Postgres.Migrations.M20250526_short_links
Simplex.Chat.Store.Postgres.Migrations.M20250702_contact_requests_remove_cascade_delete
else
exposed-modules:
Simplex.Chat.Archive
@@ -241,6 +242,7 @@ library
Simplex.Chat.Store.SQLite.Migrations.M20250512_member_admission
Simplex.Chat.Store.SQLite.Migrations.M20250513_group_scope
Simplex.Chat.Store.SQLite.Migrations.M20250526_short_links
Simplex.Chat.Store.SQLite.Migrations.M20250702_contact_requests_remove_cascade_delete
other-modules:
Paths_simplex_chat
hs-source-dirs:
+56 -38
View File
@@ -1150,45 +1150,62 @@ processChatCommand' vr = \case
pure $ CRChatCleared user (AChatInfo SCTLocal $ LocalChat nf)
_ -> throwCmdError "not supported"
APIAcceptContact incognito connReqId -> withUser $ \user@User {userId} -> do
(uclId, (ucl, gLinkInfo_)) <- withFastStore $ \db -> do
uclId <- getUserContactLinkIdByCReq db connReqId
uclGLinkInfo <- getUserContactLinkById db userId uclId
pure (uclId, uclGLinkInfo)
let UserContactLink {shortLinkDataSet, addressSettings} = ucl
when (shortLinkDataSet && incognito) $ throwCmdError "incognito not allowed for address with short link data"
withUserContactLock "acceptContact" uclId $ do
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
updateContactAccepted db user ct contactUsed
conn' <-
if sqSecured
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'
uclData_ <- withFastStore $ \db -> do
uclId_ <- getUserContactLinkIdByCReq db connReqId
forM uclId_ $ \uclId -> do -- address may be deleted
uclGLinkInfo <- getUserContactLinkById db userId uclId
pure (uclId, uclGLinkInfo)
withContactRequestLock "acceptContact" connReqId $ case uclData_ of
Nothing -> do -- address was deleted
when incognito $ throwCmdError "incognito not allowed when address is not found"
cReq <- withFastStore $ \db -> getContactRequest db user connReqId
(ct, _sqSecured) <- acceptCReq user cReq True
pure $ CRAcceptingContactRequest user ct
Just (uclId, (ucl@UserContactLink {shortLinkDataSet}, gLinkInfo_)) -> do
when (shortLinkDataSet && incognito) $ throwCmdError "incognito not allowed for address with short link data"
withUserContactLock "acceptContact" uclId $ do
cReq <- withFastStore $ \db -> getContactRequest db user connReqId
let contactUsed = isNothing gLinkInfo_ -- for redundancy, as group link requests are auto-accepted
(ct, sqSecured) <- acceptCReq user cReq contactUsed
when sqSecured $ sendWelcomeMsg user ct ucl cReq
pure $ CRAcceptingContactRequest user ct
where
acceptCReq user cReq contactUsed = do
(ct, conn@Connection {connId}, sqSecured) <- acceptContactRequest user cReq incognito
ct' <- withStore' $ \db -> do
updateContactAccepted db user ct contactUsed
conn' <-
if sqSecured
then conn {connStatus = ConnSndReady} <$ updateConnectionStatusFromTo db connId ConnNew ConnSndReady
else pure conn
pure ct {contactUsed, activeConn = Just conn'}
pure (ct', sqSecured)
sendWelcomeMsg user ct ucl UserContactRequest {welcomeSharedMsgId} =
forM_ (autoReply $ addressSettings ucl) $ \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]
APIRejectContact connReqId -> withUser $ \user -> do
userContactLinkId <- withFastStore $ \db -> getUserContactLinkIdByCReq db connReqId
withUserContactLock "rejectContact" userContactLinkId $ do
(cReq@UserContactRequest {agentContactConnId = AgentConnId connId, agentInvitationId = AgentInvId invId}, ct_) <-
withFastStore $ \db -> do
cReq@UserContactRequest {contactId_} <- getContactRequest db user connReqId
ct_ <- forM contactId_ $ \contactId -> do
ct <- getContact db vr user contactId
deleteContact db user ct
pure ct
liftIO $ deleteContactRequest db user connReqId
pure (cReq, ct_)
withAgent $ \a -> rejectContact a connId invId
pure $ CRContactRequestRejected user cReq ct_
uclId_ <- withFastStore $ \db -> getUserContactLinkIdByCReq db connReqId
withContactRequestLock "rejectContact" connReqId $ case uclId_ of
Nothing -> rejectCReq user -- address was deleted
Just uclId -> withUserContactLock "rejectContact" uclId $ rejectCReq user
where
rejectCReq user = do
(cReq@UserContactRequest {agentInvitationId = AgentInvId invId}, ct_) <-
withFastStore $ \db -> do
cReq@UserContactRequest {contactId_} <- getContactRequest db user connReqId
ct_ <- forM contactId_ $ \contactId -> do
ct <- getContact db vr user contactId
deleteContact db user ct
pure ct
liftIO $ deleteContactRequest db user connReqId
pure (cReq, ct_)
withAgent (`rejectContact` invId)
pure $ CRContactRequestRejected user cReq ct_
APISendCallInvitation contactId callType -> withUser $ \user -> do
-- party initiating call
ct <- withFastStore $ \db -> getContact db vr user contactId
@@ -2785,6 +2802,7 @@ processChatCommand' vr = \case
CLContact ctId -> "Contact " <> tshow ctId
CLGroup gId -> "Group " <> tshow gId
CLUserContact ucId -> "UserContact " <> tshow ucId
CLContactRequest crId -> "ContactRequest " <> tshow crId
CLFile fId -> "File " <> tshow fId
DebugEvent event -> toView event >> ok_
GetAgentSubsTotal userId -> withUserId userId $ \user -> do
+12 -8
View File
@@ -142,6 +142,10 @@ withUserContactLock :: Text -> Int64 -> CM a -> CM a
withUserContactLock name = withEntityLock name . CLUserContact
{-# INLINE withUserContactLock #-}
withContactRequestLock :: Text -> Int64 -> CM a -> CM a
withContactRequestLock name = withEntityLock name . CLContactRequest
{-# INLINE withContactRequestLock #-}
withFileLock :: Text -> Int64 -> CM a -> CM a
withFileLock name = withEntityLock name . CLFile
{-# INLINE withFileLock #-}
@@ -878,7 +882,7 @@ getRcvFilePath fileId fPath_ fn keepHandle = case fPath_ of
-- It may be reasonable to set it when contact is first prepared, but then we can't use it to ignore requests after acceptance,
-- and it may lead to race conditions with XInfo events.
acceptContactRequest :: User -> UserContactRequest -> IncognitoEnabled -> CM (Contact, Connection, SndQueueSecured)
acceptContactRequest user@User {userId} UserContactRequest {agentInvitationId = AgentInvId invId, contactId_, cReqChatVRange, localDisplayName = cName, profileId, profile = cp, userContactLinkId, xContactId, pqSupport} incognito = do
acceptContactRequest user@User {userId} UserContactRequest {agentInvitationId = AgentInvId invId, contactId_, cReqChatVRange, localDisplayName = cName, profileId, profile = cp, userContactLinkId_, xContactId, pqSupport} incognito = do
subMode <- chatReadVar subscriptionMode
let pqSup = PQSupportOn
pqSup' = pqSup `CR.pqSupportAnd` pqSupport
@@ -887,20 +891,20 @@ acceptContactRequest user@User {userId} UserContactRequest {agentInvitationId =
(ct, conn, incognitoProfile) <- case contactId_ of
Nothing -> do
incognitoProfile <- if incognito then Just . NewIncognito <$> liftIO generateRandomProfile else pure Nothing
connId <- withAgent $ \a -> prepareConnectionToAccept a True invId pqSup'
connId <- withAgent $ \a -> prepareConnectionToAccept a (aUserId user) True invId pqSup'
(ct, conn) <- withStore' $ \db ->
createContactFromRequest db user userContactLinkId connId chatV cReqChatVRange cName profileId cp xContactId incognitoProfile subMode pqSup' False
createContactFromRequest db user userContactLinkId_ connId chatV cReqChatVRange cName profileId cp xContactId incognitoProfile subMode pqSup' False
pure (ct, conn, incognitoProfile)
Just contactId -> do
ct <- withFastStore $ \db -> getContact db vr user contactId
case contactConn ct of
Nothing -> do
incognitoProfile <- if incognito then Just . NewIncognito <$> liftIO generateRandomProfile else pure Nothing
connId <- withAgent $ \a -> prepareConnectionToAccept a True invId pqSup'
connId <- withAgent $ \a -> prepareConnectionToAccept a (aUserId user) True invId pqSup'
currentTs <- liftIO getCurrentTime
conn <- withStore' $ \db -> do
forM_ xContactId $ \xcId -> setContactAcceptedXContactId db ct xcId
createAcceptedContactConn db user userContactLinkId contactId connId chatV cReqChatVRange pqSup' incognitoProfile subMode currentTs
createAcceptedContactConn db user userContactLinkId_ contactId connId chatV cReqChatVRange pqSup' incognitoProfile subMode currentTs
pure (ct {activeConn = Just conn} :: Contact, conn, incognitoProfile)
Just conn@Connection {customUserProfileId} -> do
incognitoProfile <- forM customUserProfileId $ \pId -> withFastStore $ \db -> getProfileById db userId pId
@@ -908,7 +912,7 @@ acceptContactRequest user@User {userId} UserContactRequest {agentInvitationId =
let profileToSend = profileToSendOnAccept user incognitoProfile False
dm <- encodeConnInfoPQ pqSup' chatV $ XInfo profileToSend
-- TODO [certs rcv]
(ct,conn,) . fst <$> withAgent (\a -> acceptContact a (aConnId conn) True invId dm pqSup' subMode)
(ct,conn,) . fst <$> withAgent (\a -> acceptContact a (aUserId user) (aConnId conn) True invId dm pqSup' subMode)
acceptContactRequestAsync :: User -> Int64 -> Contact -> UserContactRequest -> Maybe IncognitoProfile -> CM Contact
acceptContactRequestAsync
@@ -925,7 +929,7 @@ acceptContactRequestAsync
currentTs <- liftIO getCurrentTime
withStore $ \db -> do
forM_ xContactId $ \xcId -> liftIO $ setContactAcceptedXContactId db ct xcId
Connection {connId} <- liftIO $ createAcceptedContactConn db user uclId contactId acId chatV cReqChatVRange cReqPQSup incognitoProfile subMode currentTs
Connection {connId} <- liftIO $ createAcceptedContactConn db user (Just uclId) contactId acId chatV cReqChatVRange cReqPQSup incognitoProfile subMode currentTs
liftIO $ setCommandConnId db user cmdId connId
getContact db vr user contactId
@@ -2194,7 +2198,7 @@ agentAcceptContactAsync :: MsgEncodingI e => User -> Bool -> InvitationId -> Cha
agentAcceptContactAsync user enableNtfs invId msg subMode pqSup chatV = do
cmdId <- withStore' $ \db -> createCommand db user Nothing CFAcceptContact
dm <- encodeConnInfoPQ pqSup chatV msg
connId <- withAgent $ \a -> acceptContactAsync a (aCorrId cmdId) enableNtfs invId dm pqSup subMode
connId <- withAgent $ \a -> acceptContactAsync a (aUserId user) (aCorrId cmdId) enableNtfs invId dm pqSup subMode
pure (cmdId, connId)
deleteAgentConnectionAsync :: ConnId -> CM ()
+1 -2
View File
@@ -143,12 +143,11 @@ createOrUpdateContactRequest
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.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.xcontact_id = ?
+8 -9
View File
@@ -708,7 +708,7 @@ getUserContacts db vr user@User {userId} = do
contacts <- rights <$> mapM (runExceptT . getContact db vr user) contactIds
pure $ filter (\Contact {activeConn} -> isJust activeConn) contacts
getUserContactLinkIdByCReq :: DB.Connection -> Int64 -> ExceptT StoreError IO Int64
getUserContactLinkIdByCReq :: DB.Connection -> Int64 -> ExceptT StoreError IO (Maybe Int64)
getUserContactLinkIdByCReq db contactRequestId =
ExceptT . firstRow fromOnly (SEContactRequestNotFound contactRequestId) $
DB.query db "SELECT user_contact_link_id FROM contact_requests WHERE contact_request_id = ?" (Only contactRequestId)
@@ -734,12 +734,11 @@ contactRequestQuery =
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.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)
|]
@@ -774,8 +773,8 @@ deleteContactRequest db User {userId} contactRequestId = do
(userId, userId, contactRequestId, userId)
DB.execute db "DELETE FROM contact_requests WHERE user_id = ? AND contact_request_id = ?" (userId, contactRequestId)
createContactFromRequest :: DB.Connection -> User -> Int64 -> ConnId -> VersionChat -> VersionRangeChat -> ContactName -> ProfileId -> Profile -> Maybe XContactId -> Maybe IncognitoProfile -> SubscriptionMode -> PQSupport -> Bool -> IO (Contact, Connection)
createContactFromRequest db user@User {userId, profile = LocalProfile {preferences}} uclId agentConnId connChatVersion cReqChatVRange localDisplayName profileId profile xContactId incognitoProfile subMode pqSup contactUsed = do
createContactFromRequest :: DB.Connection -> User -> Maybe Int64 -> ConnId -> VersionChat -> VersionRangeChat -> ContactName -> ProfileId -> Profile -> Maybe XContactId -> Maybe IncognitoProfile -> SubscriptionMode -> PQSupport -> Bool -> IO (Contact, Connection)
createContactFromRequest db user@User {userId, profile = LocalProfile {preferences}} uclId_ agentConnId connChatVersion cReqChatVRange localDisplayName profileId profile xContactId incognitoProfile subMode pqSup contactUsed = do
currentTs <- getCurrentTime
let userPreferences = fromMaybe emptyChatPrefs $ incognitoProfile >> preferences
DB.execute
@@ -784,7 +783,7 @@ createContactFromRequest db user@User {userId, profile = LocalProfile {preferenc
(userId, localDisplayName, profileId, BI True, userPreferences, currentTs, currentTs, currentTs, xContactId, BI contactUsed)
contactId <- insertedRowId db
DB.execute db "UPDATE contact_requests SET contact_id = ? WHERE user_id = ? AND local_display_name = ?" (contactId, userId, localDisplayName)
conn <- createAcceptedContactConn db user uclId contactId agentConnId connChatVersion cReqChatVRange pqSup incognitoProfile subMode currentTs
conn <- createAcceptedContactConn db user uclId_ contactId agentConnId connChatVersion cReqChatVRange pqSup incognitoProfile subMode currentTs
let mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito conn
ct =
Contact
@@ -813,12 +812,12 @@ createContactFromRequest db user@User {userId, profile = LocalProfile {preferenc
}
pure (ct, conn)
createAcceptedContactConn :: DB.Connection -> User -> Int64 -> ContactId -> ConnId -> VersionChat -> VersionRangeChat -> PQSupport -> Maybe IncognitoProfile -> SubscriptionMode -> UTCTime -> IO Connection
createAcceptedContactConn db User {userId} uclId contactId agentConnId connChatVersion cReqChatVRange pqSup incognitoProfile subMode currentTs = do
createAcceptedContactConn :: DB.Connection -> User -> Maybe Int64 -> ContactId -> ConnId -> VersionChat -> VersionRangeChat -> PQSupport -> Maybe IncognitoProfile -> SubscriptionMode -> UTCTime -> IO Connection
createAcceptedContactConn db User {userId} uclId_ contactId agentConnId connChatVersion cReqChatVRange pqSup incognitoProfile subMode currentTs = do
customUserProfileId <- forM incognitoProfile $ \case
NewIncognito p -> createIncognitoProfile_ db userId currentTs p
ExistingIncognito LocalProfile {profileId = pId} -> pure pId
createConnection_ db userId ConnContact (Just contactId) agentConnId ConnNew connChatVersion cReqChatVRange Nothing (Just uclId) customUserProfileId 0 currentTs subMode pqSup
createConnection_ db userId ConnContact (Just contactId) agentConnId ConnNew connChatVersion cReqChatVRange Nothing uclId_ customUserProfileId 0 currentTs subMode pqSup
updateContactAccepted :: DB.Connection -> User -> Contact -> Bool -> IO ()
updateContactAccepted db User {userId} Contact {contactId} contactUsed =
+1 -2
View File
@@ -1051,12 +1051,11 @@ getContactRequestChatPreviews_ db User {userId} pagination clq = case clq of
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.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 ON c.user_contact_link_id = cr.user_contact_link_id
JOIN contact_profiles p ON p.contact_profile_id = cr.contact_profile_id
JOIN user_contact_links uc ON uc.user_contact_link_id = cr.user_contact_link_id
WHERE cr.user_id = ?
@@ -9,6 +9,7 @@ import Simplex.Chat.Store.Postgres.Migrations.M20250402_short_links
import Simplex.Chat.Store.Postgres.Migrations.M20250512_member_admission
import Simplex.Chat.Store.Postgres.Migrations.M20250513_group_scope
import Simplex.Chat.Store.Postgres.Migrations.M20250526_short_links
import Simplex.Chat.Store.Postgres.Migrations.M20250702_contact_requests_remove_cascade_delete
import Simplex.Messaging.Agent.Store.Shared (Migration (..))
schemaMigrations :: [(String, Text, Maybe Text)]
@@ -17,7 +18,8 @@ schemaMigrations =
("20250402_short_links", m20250402_short_links, Just down_m20250402_short_links),
("20250512_member_admission", m20250512_member_admission, Just down_m20250512_member_admission),
("20250513_group_scope", m20250513_group_scope, Just down_m20250513_group_scope),
("20250526_short_links", m20250526_short_links, Just down_m20250526_short_links)
("20250526_short_links", m20250526_short_links, Just down_m20250526_short_links),
("20250702_contact_requests_remove_cascade_delete", m20250702_contact_requests_remove_cascade_delete, Just down_m20250702_contact_requests_remove_cascade_delete)
]
-- | The list of migrations in ascending order by date
@@ -0,0 +1,39 @@
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Chat.Store.Postgres.Migrations.M20250702_contact_requests_remove_cascade_delete where
import Data.Text (Text)
import qualified Data.Text as T
import Text.RawString.QQ (r)
m20250702_contact_requests_remove_cascade_delete :: Text
m20250702_contact_requests_remove_cascade_delete =
T.pack
[r|
ALTER TABLE contact_requests DROP CONSTRAINT contact_requests_user_contact_link_id_fkey;
ALTER TABLE contact_requests ALTER COLUMN user_contact_link_id DROP NOT NULL;
ALTER TABLE contact_requests
ADD CONSTRAINT contact_requests_user_contact_link_id_fkey
FOREIGN KEY (user_contact_link_id)
REFERENCES user_contact_links(user_contact_link_id)
ON UPDATE CASCADE
ON DELETE SET NULL;
|]
down_m20250702_contact_requests_remove_cascade_delete :: Text
down_m20250702_contact_requests_remove_cascade_delete =
T.pack
[r|
ALTER TABLE contact_requests DROP CONSTRAINT contact_requests_user_contact_link_id_fkey;
ALTER TABLE contact_requests ALTER COLUMN user_contact_link_id SET NOT NULL;
ALTER TABLE contact_requests
ADD CONSTRAINT contact_requests_user_contact_link_id_fkey
FOREIGN KEY (user_contact_link_id)
REFERENCES user_contact_links(user_contact_link_id)
ON UPDATE CASCADE
ON DELETE CASCADE;
|]
@@ -349,7 +349,7 @@ ALTER TABLE test_chat_schema.contact_profiles ALTER COLUMN contact_profile_id AD
CREATE TABLE test_chat_schema.contact_requests (
contact_request_id bigint NOT NULL,
user_contact_link_id bigint NOT NULL,
user_contact_link_id bigint,
agent_invitation_id bytea NOT NULL,
contact_profile_id bigint,
local_display_name text NOT NULL,
@@ -2313,7 +2313,7 @@ ALTER TABLE ONLY test_chat_schema.contact_requests
ALTER TABLE ONLY test_chat_schema.contact_requests
ADD CONSTRAINT contact_requests_user_contact_link_id_fkey FOREIGN KEY (user_contact_link_id) REFERENCES test_chat_schema.user_contact_links(user_contact_link_id) ON UPDATE CASCADE ON DELETE CASCADE;
ADD CONSTRAINT contact_requests_user_contact_link_id_fkey FOREIGN KEY (user_contact_link_id) REFERENCES test_chat_schema.user_contact_links(user_contact_link_id) ON UPDATE CASCADE ON DELETE SET NULL;
@@ -2689,3 +2689,6 @@ ALTER TABLE ONLY test_chat_schema.user_contact_links
ALTER TABLE ONLY test_chat_schema.xftp_file_descriptions
ADD CONSTRAINT xftp_file_descriptions_user_id_fkey FOREIGN KEY (user_id) REFERENCES test_chat_schema.users(user_id) ON DELETE CASCADE;
+3 -1
View File
@@ -132,6 +132,7 @@ import Simplex.Chat.Store.SQLite.Migrations.M20250402_short_links
import Simplex.Chat.Store.SQLite.Migrations.M20250512_member_admission
import Simplex.Chat.Store.SQLite.Migrations.M20250513_group_scope
import Simplex.Chat.Store.SQLite.Migrations.M20250526_short_links
import Simplex.Chat.Store.SQLite.Migrations.M20250702_contact_requests_remove_cascade_delete
import Simplex.Messaging.Agent.Store.Shared (Migration (..))
schemaMigrations :: [(String, Query, Maybe Query)]
@@ -263,7 +264,8 @@ schemaMigrations =
("20250402_short_links", m20250402_short_links, Just down_m20250402_short_links),
("20250512_member_admission", m20250512_member_admission, Just down_m20250512_member_admission),
("20250513_group_scope", m20250513_group_scope, Just down_m20250513_group_scope),
("20250526_short_links", m20250526_short_links, Just down_m20250526_short_links)
("20250526_short_links", m20250526_short_links, Just down_m20250526_short_links),
("20250702_contact_requests_remove_cascade_delete", m20250702_contact_requests_remove_cascade_delete, Just down_m20250702_contact_requests_remove_cascade_delete)
]
-- | The list of migrations in ascending order by date
@@ -0,0 +1,46 @@
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Chat.Store.SQLite.Migrations.M20250702_contact_requests_remove_cascade_delete where
import Database.SQLite.Simple (Query)
import Database.SQLite.Simple.QQ (sql)
m20250702_contact_requests_remove_cascade_delete :: Query
m20250702_contact_requests_remove_cascade_delete =
[sql|
PRAGMA writable_schema=1;
UPDATE sqlite_master
SET sql = replace(
replace(
sql,
'user_contact_link_id INTEGER NOT NULL REFERENCES user_contact_links',
'user_contact_link_id INTEGER REFERENCES user_contact_links'
),
'ON UPDATE CASCADE ON DELETE CASCADE,',
'ON UPDATE CASCADE ON DELETE SET NULL,'
)
WHERE name = 'contact_requests' AND type = 'table';
PRAGMA writable_schema=0;
|]
down_m20250702_contact_requests_remove_cascade_delete :: Query
down_m20250702_contact_requests_remove_cascade_delete =
[sql|
PRAGMA writable_schema=1;
UPDATE sqlite_master
SET sql = replace(
replace(
sql,
'ON UPDATE CASCADE ON DELETE SET NULL,',
'ON UPDATE CASCADE ON DELETE CASCADE,'
),
'user_contact_link_id INTEGER REFERENCES user_contact_links',
'user_contact_link_id INTEGER NOT NULL REFERENCES user_contact_links'
)
WHERE name = 'contact_requests' AND type = 'table';
PRAGMA writable_schema=0;
|]
@@ -274,7 +274,7 @@ Plan:
Query:
INSERT INTO conn_invitations
(invitation_id, contact_conn_id, cr_invitation, recipient_conn_info, accepted) VALUES (?, ?, ?, ?, 0);
(invitation_id, contact_conn_id, cr_invitation, recipient_conn_info, accepted) VALUES (?, ?, ?, ?, 0);
Plan:
@@ -822,7 +822,7 @@ Query: DELETE FROM commands WHERE command_id = ?
Plan:
SEARCH commands USING INTEGER PRIMARY KEY (rowid=?)
Query: DELETE FROM conn_invitations WHERE contact_conn_id = ? AND invitation_id = ?
Query: DELETE FROM conn_invitations WHERE invitation_id = ?
Plan:
SEARCH conn_invitations USING PRIMARY KEY (invitation_id=?)
@@ -398,12 +398,11 @@ 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.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.xcontact_id = ?
@@ -412,7 +411,6 @@ Query:
Plan:
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 COUNT(1)
@@ -1631,12 +1629,11 @@ 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.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 ON c.user_contact_link_id = cr.user_contact_link_id
JOIN contact_profiles p ON p.contact_profile_id = cr.contact_profile_id
JOIN user_contact_links uc ON uc.user_contact_link_id = cr.user_contact_link_id
WHERE cr.user_id = ?
@@ -1655,18 +1652,16 @@ Plan:
SEARCH uc USING INDEX sqlite_autoindex_user_contact_links_1 (user_id=? AND local_display_name=?)
SEARCH cr USING INDEX idx_contact_requests_updated_at (user_id=? AND updated_at<?)
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.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 ON c.user_contact_link_id = cr.user_contact_link_id
JOIN contact_profiles p ON p.contact_profile_id = cr.contact_profile_id
JOIN user_contact_links uc ON uc.user_contact_link_id = cr.user_contact_link_id
WHERE cr.user_id = ?
@@ -1685,18 +1680,16 @@ Plan:
SEARCH uc USING INDEX sqlite_autoindex_user_contact_links_1 (user_id=? AND local_display_name=?)
SEARCH cr USING INDEX idx_contact_requests_updated_at (user_id=? AND updated_at>?)
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.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 ON c.user_contact_link_id = cr.user_contact_link_id
JOIN contact_profiles p ON p.contact_profile_id = cr.contact_profile_id
JOIN user_contact_links uc ON uc.user_contact_link_id = cr.user_contact_link_id
WHERE cr.user_id = ?
@@ -1715,7 +1708,6 @@ Plan:
SEARCH uc USING INDEX sqlite_autoindex_user_contact_links_1 (user_id=? AND local_display_name=?)
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
@@ -4742,35 +4734,31 @@ 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.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.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
@@ -344,8 +344,8 @@ CREATE TABLE user_contact_links(
);
CREATE TABLE contact_requests(
contact_request_id INTEGER PRIMARY KEY,
user_contact_link_id INTEGER NOT NULL REFERENCES user_contact_links
ON UPDATE CASCADE ON DELETE CASCADE,
user_contact_link_id INTEGER REFERENCES user_contact_links
ON UPDATE CASCADE ON DELETE SET NULL,
agent_invitation_id BLOB NOT NULL,
contact_profile_id INTEGER REFERENCES contact_profiles
ON DELETE SET NULL
+4 -3
View File
@@ -64,6 +64,7 @@ data ChatLockEntity
| CLContact ContactId
| CLGroup GroupId
| CLUserContact Int64
| CLContactRequest Int64
| CLFile Int64
deriving (Eq, Ord)
@@ -488,13 +489,13 @@ getProfileById db userId profileId =
toProfile :: (ContactName, Text, Maybe ImageData, Maybe ConnLinkContact, LocalAlias, Maybe Preferences) -> LocalProfile
toProfile (displayName, fullName, image, contactLink, localAlias, preferences) = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences, localAlias}
type ContactRequestRow = (Int64, ContactName, AgentInvId, Maybe ContactId, Maybe GroupId, Int64) :. (AgentConnId, Int64, ContactName, Text, Maybe ImageData, Maybe ConnLinkContact) :. (Maybe XContactId, PQSupport, Maybe SharedMsgId, Maybe SharedMsgId, Maybe Preferences, UTCTime, UTCTime, VersionChat, VersionChat)
type ContactRequestRow = (Int64, ContactName, AgentInvId, Maybe ContactId, Maybe GroupId, Maybe Int64) :. (Int64, ContactName, Text, Maybe ImageData, Maybe ConnLinkContact) :. (Maybe XContactId, PQSupport, Maybe SharedMsgId, Maybe SharedMsgId, Maybe Preferences, UTCTime, UTCTime, VersionChat, VersionChat)
toContactRequest :: ContactRequestRow -> UserContactRequest
toContactRequest ((contactRequestId, localDisplayName, agentInvitationId, contactId_, businessGroupId_, userContactLinkId) :. (agentContactConnId, profileId, displayName, fullName, image, contactLink) :. (xContactId, pqSupport, welcomeSharedMsgId, requestSharedMsgId, preferences, createdAt, updatedAt, minVer, maxVer)) = do
toContactRequest ((contactRequestId, localDisplayName, agentInvitationId, contactId_, businessGroupId_, userContactLinkId_) :. (profileId, displayName, fullName, image, contactLink) :. (xContactId, pqSupport, welcomeSharedMsgId, requestSharedMsgId, preferences, createdAt, updatedAt, minVer, maxVer)) = do
let profile = Profile {displayName, fullName, image, contactLink, preferences}
cReqChatVRange = fromMaybe (versionToRange maxVer) $ safeVersionRange minVer maxVer
in UserContactRequest {contactRequestId, agentInvitationId, contactId_, businessGroupId_, userContactLinkId, agentContactConnId, cReqChatVRange, localDisplayName, profileId, profile, xContactId, pqSupport, welcomeSharedMsgId, requestSharedMsgId, createdAt, updatedAt}
in UserContactRequest {contactRequestId, agentInvitationId, contactId_, businessGroupId_, userContactLinkId_, cReqChatVRange, localDisplayName, profileId, profile, xContactId, pqSupport, welcomeSharedMsgId, requestSharedMsgId, createdAt, updatedAt}
userQuery :: Query
userQuery =
+1 -2
View File
@@ -347,8 +347,7 @@ data UserContactRequest = UserContactRequest
agentInvitationId :: AgentInvId,
contactId_ :: Maybe ContactId,
businessGroupId_ :: Maybe GroupId,
userContactLinkId :: Int64,
agentContactConnId :: AgentConnId, -- connection id of user contact
userContactLinkId_ :: Maybe Int64,
cReqChatVRange :: VersionRangeChat,
localDisplayName :: ContactName,
profileId :: Int64,
+43 -8
View File
@@ -48,10 +48,7 @@ chatProfileTests = do
it "deduplicate contact requests" testDeduplicateContactRequests
it "deduplicate contact requests with profile change" testDeduplicateContactRequestsProfileChange
it "reject contact and delete contact link" testRejectContactAndDeleteUserContact
-- TODO [short links] fix address deletion:
-- TODO - either alert user that N chats will be deleted and delete contact request contacts and business chats
-- TODO - or allow to accept contact requests for deleted address (remove cascade deletes, rework agent)
xit "delete connection requests when contact link deleted" testDeleteConnectionRequests
it "keep connection requests when contact link deleted" testKeepConnectionRequests
it "connected contact works when contact link deleted" testContactLinkDeletedConnectedContactWorks
-- TODO [short links] test auto-reply with current version, with connecting client not preparing contact
it "auto-reply message" testAutoReplyMessage
@@ -673,8 +670,8 @@ testRejectContactAndDeleteUserContact = testChat3 aliceProfile bobProfile cathPr
cath ##> ("/c " <> cLink)
cath <## "error: connection authorization failed - this could happen if connection was deleted, secured with different credentials, or due to a bug - please re-create the connection"
testDeleteConnectionRequests :: HasCallStack => TestParams -> IO ()
testDeleteConnectionRequests = testChat3 aliceProfile bobProfile cathProfile $
testKeepConnectionRequests :: HasCallStack => TestParams -> IO ()
testKeepConnectionRequests = testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
alice ##> "/ad"
cLink <- getContactLink alice True
@@ -687,14 +684,52 @@ testDeleteConnectionRequests = testChat3 aliceProfile bobProfile cathProfile $
alice <## "Your chat address is deleted - accepted contacts will remain connected."
alice <## "To create a new chat address use /ad"
-- can accept and reject requests after address deletion
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 ##> "/rc cath"
alice <## "cath: contact request rejected"
alice @@@ [("@bob", "hey")]
-- bob's request to new address uses different name
alice ##> "/ad"
cLink' <- getContactLink alice True
bob ##> ("/c " <> cLink')
-- same names are used here, as they were released at /da
alice <#? bob
bob <## "connection request sent!"
alice <## "bob_1 (Bob) wants to connect to you!"
alice <## "to accept: /ac bob_1"
alice <## "to reject: /rc bob_1 (the sender will NOT be notified)"
alice ##> "/ac bob_1"
alice <## "bob_1 (Bob): accepting contact request, you can send messages to contact"
concurrently_
(bob <## "alice_1 (Alice): contact is connected")
(alice <## "bob_1 (Bob): contact is connected")
alice #> "@bob_1 hi"
bob <# "alice_1> hi"
bob #> "@alice_1 hey"
alice <# "bob_1> hey"
cath ##> ("/c " <> cLink')
alice <#? cath
alice ##> "/ac cath"
alice <## "cath (Catherine): accepting contact request, you can send messages to contact"
concurrently_
(cath <## "alice (Alice): contact is connected")
(alice <## "cath (Catherine): contact is connected")
alice <##> cath
alice @@@ [("@cath", "hey"), ("@bob_1", "hey"), ("@bob", "hey")]
testContactLinkDeletedConnectedContactWorks :: HasCallStack => TestParams -> IO ()
testContactLinkDeletedConnectedContactWorks = testChat2 aliceProfile bobProfile $
\alice bob -> do