From f2394d121f58837a41e8af9bf57c4e117c99c529 Mon Sep 17 00:00:00 2001 From: shum Date: Wed, 3 Jun 2026 17:44:20 +0000 Subject: [PATCH] chat: APIConnectPlan accepts ConnectTarget; connectPlanName looks up by name APIConnectPlan/Connect flip from Maybe AConnectionLink to Maybe ConnectTarget. connectPlan dispatches CTLink -> connectPlanLink (the prior body, renamed) and CTName -> connectPlanName (new) which looks the name up against contacts.simplex_name and groups.simplex_name via the new getContactBySimplexName / getGroupInfoBySimplexName store helpers. The hit path returns the contact's / group's stored conn link from preparedContact / preparedGroup; missing prepared state or unknown names return CEInvalidConnReq. RSLV on-chain resolution is out of scope for this branch -- known-name lookup is enough for conversation display, search, and external-link share. connLinkP_ parser is unchanged: APIConnect's preparedLink_ stays ACreatedConnLink-shaped, and the Connect / APIConnectPlan parsers already use inline strP for ConnectTarget without going through the helper. Directory.Service call sites updated to wrap their AConnectionLink in CTLink when invoking APIConnectPlan. --- .../src/Directory/Service.hs | 8 ++-- src/Simplex/Chat/Controller.hs | 4 +- src/Simplex/Chat/Library/Commands.hs | 38 +++++++++++++++---- src/Simplex/Chat/Store/Direct.hs | 19 ++++++++++ src/Simplex/Chat/Store/Groups.hs | 13 +++++++ 5 files changed, 69 insertions(+), 13 deletions(-) diff --git a/apps/simplex-directory-service/src/Directory/Service.hs b/apps/simplex-directory-service/src/Directory/Service.hs index 577cc99752..3d57fb2b43 100644 --- a/apps/simplex-directory-service/src/Directory/Service.hs +++ b/apps/simplex-directory-service/src/Directory/Service.hs @@ -65,7 +65,7 @@ import Simplex.Chat.Types import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.Shared import Simplex.Chat.View (serializeChatError, serializeChatResponse, simplexChatContact, viewContactName, viewGroupName) -import Simplex.Messaging.Agent.Protocol (AConnectionLink (..), ACreatedConnLink (..), AgentErrorType (..), ConnectionLink (..), CreatedConnLink (..), SConnectionMode (..), sameConnReqContact, sameShortLinkContact) +import Simplex.Messaging.Agent.Protocol (AConnectionLink (..), ACreatedConnLink (..), AgentErrorType (..), ConnectTarget (..), ConnectionLink (..), CreatedConnLink (..), SConnectionMode (..), sameConnReqContact, sameShortLinkContact) import qualified Simplex.Messaging.Crypto.File as CF import Simplex.Messaging.Encoding.String import Simplex.Messaging.Protocol (ErrorType (..)) @@ -552,7 +552,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName <> ".\nIt is hidden from the directory until approved." notifyAdminUsers $ "The " <> gt <> " " <> groupRef <> " is updated" <> byMember <> "." sendToApprove g' gr' n' - sendChatCmd cc (APIConnectPlan userId (Just link) True Nothing) >>= \case + sendChatCmd cc (APIConnectPlan userId (Just (CTLink link)) True Nothing) >>= \case Right (CRConnectionPlan _ _ (CPGroupLink (GLPKnown {groupInfo = g'}))) -> case dbOwnerMemberId gr of Just ownerGMId -> @@ -797,7 +797,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName forM_ pg_ $ \pg@PublicGroupProfile {groupLink} -> when (groupRegStatus == GRSActive || pendingApproval groupRegStatus) $ do let link = ACL SCMContact $ CLShort groupLink - sendChatCmd cc (APIConnectPlan userId (Just link) True Nothing) >>= \case + sendChatCmd cc (APIConnectPlan userId (Just (CTLink link)) True Nothing) >>= \case Right (CRConnectionPlan _ _ (CPGroupLink (GLPKnown {groupInfo = g', groupUpdated = BoolDef updated, linkOwners = ListDef owners}))) -> checkValidOwner dbOwnerMemberId owners $ do when updated $ reapprove pg gr groupRegStatus g' @@ -933,7 +933,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName let link = ACL SCMContact $ CLShort connLink mId = MemberId oIdBytes gt' = groupTypeStr gt - sendChatCmd cc (APIConnectPlan userId (Just link) True (Just ownerSig)) >>= \case + sendChatCmd cc (APIConnectPlan userId (Just (CTLink link)) True (Just ownerSig)) >>= \case Right (CRConnectionPlan _ (ACCL SCMContact ccLink) plan) -> handleGroupLinkPlan ct ccLink mId ownerSig gt' plan _ -> sendMessage cc ct "Error: could not connect. Please report it to directory admins." diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 27b3fedae6..2145afbe1f 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -477,7 +477,7 @@ data ChatCommand | AddContact IncognitoEnabled | APISetConnectionIncognito Int64 IncognitoEnabled | APIChangeConnectionUser Int64 UserId -- new user id to switch connection to - | APIConnectPlan {userId :: UserId, connectionLink :: Maybe AConnectionLink, resolveKnown :: Bool, linkOwnerSig :: Maybe LinkOwnerSig} -- Maybe AConnectionLink is used to report link parsing failure as special error + | APIConnectPlan {userId :: UserId, connectTarget :: Maybe ConnectTarget, resolveKnown :: Bool, linkOwnerSig :: Maybe LinkOwnerSig} -- Maybe ConnectTarget is used to report parsing failure as special error | APIPrepareContact UserId ACreatedConnLink ContactShortLinkData | APIPrepareGroup UserId CreatedLinkContact DirectLink GroupShortLinkData | APIChangePreparedContactUser ContactId UserId @@ -485,7 +485,7 @@ data ChatCommand | APIConnectPreparedContact {contactId :: ContactId, incognito :: IncognitoEnabled, msgContent_ :: Maybe MsgContent} | APIConnectPreparedGroup {groupId :: GroupId, incognito :: IncognitoEnabled, ownerContact :: Maybe GroupOwnerContact, msgContent_ :: Maybe MsgContent} | APIConnect {userId :: UserId, incognito :: IncognitoEnabled, preparedLink_ :: Maybe ACreatedConnLink} -- Maybe is used to report link parsing failure as special error - | Connect {incognito :: IncognitoEnabled, connLink_ :: Maybe AConnectionLink} + | Connect {incognito :: IncognitoEnabled, connTarget_ :: Maybe ConnectTarget} | APIConnectContactViaAddress UserId IncognitoEnabled ContactId | ConnectSimplex IncognitoEnabled -- UserId (not used in UI) | DeleteContact ContactName ChatDeleteMode diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index 8e9c0f17e4..2c949a809c 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -2007,8 +2007,8 @@ processChatCommand vr nm = \case createDirectConnection db newUser agConnId ccLink' Nothing ConnNew Nothing subMode initialChatVersion PQSupportOn deleteAgentConnectionAsync (aConnId' conn) pure conn' - APIConnectPlan userId (Just cLink) resolveKnown linkOwnerSig_ -> withUserId userId $ \user -> - uncurry (CRConnectionPlan user) <$> connectPlan user cLink resolveKnown linkOwnerSig_ + APIConnectPlan userId (Just ct) resolveKnown linkOwnerSig_ -> withUserId userId $ \user -> + uncurry (CRConnectionPlan user) <$> connectPlan user ct resolveKnown linkOwnerSig_ APIConnectPlan _ Nothing _ _ -> throwChatError CEInvalidConnReq APIPrepareContact userId accLink contactSLinkData -> withUserId userId $ \user -> do let ContactShortLinkData {profile, message, business} = contactSLinkData @@ -2235,10 +2235,13 @@ processChatCommand vr nm = \case CVRConnectedContact ct -> pure $ CRContactAlreadyExists user ct CVRSentInvitation conn incognitoProfile -> pure $ CRSentInvitation user (mkPendingContactConnection conn Nothing) incognitoProfile APIConnect _ _ Nothing -> throwChatError CEInvalidConnReq - Connect incognito (Just cLink@(ACL m cLink')) -> withUser $ \user -> do + Connect incognito (Just (CTLink cLink@(ACL m cLink'))) -> withUser $ \user -> do -- TODO [relays] member: /c api to support groups with relays -- TODO - possibly by going through APIPrepareGroup -> APIConnectPreparedGroup - (ccLink, plan) <- connectPlan user cLink False Nothing `catchAllErrors` \e -> case cLink' of CLFull cReq -> pure (ACCL m (CCLink cReq Nothing), CPInvitationLink (ILPOk Nothing Nothing)); _ -> throwError e + (ccLink, plan) <- connectPlanLink user cLink False Nothing `catchAllErrors` \e -> case cLink' of CLFull cReq -> pure (ACCL m (CCLink cReq Nothing), CPInvitationLink (ILPOk Nothing Nothing)); _ -> throwError e + connectWithPlan user incognito ccLink plan + Connect incognito (Just (CTName ni)) -> withUser $ \user -> do + (ccLink, plan) <- connectPlanName user ni connectWithPlan user incognito ccLink plan Connect _ Nothing -> throwChatError CEInvalidConnReq APIConnectContactViaAddress userId incognito contactId -> withUserId userId $ \user -> do @@ -4068,8 +4071,12 @@ processChatCommand vr nm = \case pure (gId, chatSettings) _ -> throwCmdError "not supported" processChatCommand vr nm $ APISetChatSettings (ChatRef cType chatId Nothing) $ updateSettings chatSettings - connectPlan :: User -> AConnectionLink -> Bool -> Maybe LinkOwnerSig -> CM (ACreatedConnLink, ConnectionPlan) - connectPlan user (ACL SCMInvitation cLink) _ sig_ = case cLink of + connectPlan :: User -> ConnectTarget -> Bool -> Maybe LinkOwnerSig -> CM (ACreatedConnLink, ConnectionPlan) + connectPlan user ct resolveKnown sig_ = case ct of + CTLink l -> connectPlanLink user l resolveKnown sig_ + CTName ni -> connectPlanName user ni + connectPlanLink :: User -> AConnectionLink -> Bool -> Maybe LinkOwnerSig -> CM (ACreatedConnLink, ConnectionPlan) + connectPlanLink user (ACL SCMInvitation cLink) _ sig_ = case cLink of CLFull cReq -> invitationReqAndPlan cReq Nothing Nothing Nothing CLShort l -> do let l' = serverShortLink l @@ -4090,7 +4097,7 @@ processChatCommand vr nm = \case invitationReqAndPlan cReq sLnk_ cld ov = do plan <- invitationRequestPlan user cReq cld ov `catchAllErrors` (pure . CPError) pure (ACCL SCMInvitation (CCLink cReq sLnk_), plan) - connectPlan user (ACL SCMContact cLink) resolveKnown sig_ = case cLink of + connectPlanLink user (ACL SCMContact cLink) resolveKnown sig_ = case cLink of CLFull cReq -> do plan <- contactOrGroupRequestPlan user cReq `catchAllErrors` (pure . CPError) pure (ACCL SCMContact $ CCLink cReq Nothing, plan) @@ -4164,6 +4171,23 @@ processChatCommand vr nm = \case Just sLinkData -> updateGroupFromLinkData user g sLinkData _ -> pure (g, False) pure (con (linkConnReq fd), CPGroupLink (GLPKnown g' (BoolDef updated) ov (ListDef glOwners))) + connectPlanName :: User -> SimplexNameInfo -> CM (ACreatedConnLink, ConnectionPlan) + connectPlanName user ni@SimplexNameInfo {nameType} = do + ct_ <- withFastStore $ \db -> getContactBySimplexName db vr user ni + case ct_ of + Just ct -> case preparedContact ct of + Just PreparedContact {connLinkToConnect} -> pure (connLinkToConnect, CPContactAddress (CAPKnown ct)) + Nothing -> throwChatError CEInvalidConnReq + Nothing -> case nameType of + NTContact -> throwChatError CEInvalidConnReq + NTPublicGroup -> do + g_ <- withFastStore $ \db -> getGroupInfoBySimplexName db vr user ni + case g_ of + Just g -> case preparedGroup g of + Just PreparedGroup {connLinkToConnect = ccLink} -> + pure (ACCL SCMContact ccLink, CPGroupLink (GLPKnown g (BoolDef False) Nothing (ListDef []))) + Nothing -> throwChatError CEInvalidConnReq + Nothing -> throwChatError CEInvalidConnReq connectWithPlan :: User -> IncognitoEnabled -> ACreatedConnLink -> ConnectionPlan -> CM ChatResponse connectWithPlan user@User {userId} incognito ccLink plan | connectionPlanProceed plan = do diff --git a/src/Simplex/Chat/Store/Direct.hs b/src/Simplex/Chat/Store/Direct.hs index 4a4a37c865..d629a0dcf5 100644 --- a/src/Simplex/Chat/Store/Direct.hs +++ b/src/Simplex/Chat/Store/Direct.hs @@ -46,9 +46,11 @@ module Simplex.Chat.Store.Direct deleteContactWithoutGroups, getDeletedContacts, getContactByName, + getContactBySimplexName, getContact, getContactViaShortLinkToConnect, getContactIdByName, + getContactIdBySimplexName, updateContactProfile, updateContactUserPreferences, updateContactAlias, @@ -763,6 +765,23 @@ getContactByName db vr user localDisplayName = do cId <- getContactIdByName db user localDisplayName getContact db vr user cId +getContactBySimplexName :: DB.Connection -> VersionRangeChat -> User -> SimplexNameInfo -> ExceptT StoreError IO (Maybe Contact) +getContactBySimplexName db vr user ni = + liftIO (getContactIdBySimplexName db user ni) >>= \case + Nothing -> pure Nothing + Just cId -> Just <$> getContact db vr user cId + +getContactIdBySimplexName :: DB.Connection -> User -> SimplexNameInfo -> IO (Maybe Int64) +getContactIdBySimplexName db User {userId} ni = + maybeFirstRow fromOnly $ + DB.query + db + [sql| + SELECT contact_id FROM contacts + WHERE user_id = ? AND simplex_name = ? AND deleted = 0 + |] + (userId, ni) + getUserContacts :: DB.Connection -> VersionRangeChat -> User -> IO [Contact] getUserContacts db vr user@User {userId} = do contactIds <- map fromOnly <$> DB.query db "SELECT contact_id FROM contacts WHERE user_id = ? AND deleted = 0" (Only userId) diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs index b5e059e5e7..5698b0b04b 100644 --- a/src/Simplex/Chat/Store/Groups.hs +++ b/src/Simplex/Chat/Store/Groups.hs @@ -49,9 +49,11 @@ module Simplex.Chat.Store.Groups updateGroupPreferences, updateGroupProfileFromMember, getGroupIdByName, + getGroupIdBySimplexName, getGroupMemberIdByName, getActiveMembersByName, getGroupInfoByName, + getGroupInfoBySimplexName, getGroupMember, getHostMember, getMentionedGroupMember, @@ -1043,6 +1045,17 @@ getGroupInfoByName db vr user gName = do gId <- getGroupIdByName db user gName getGroupInfo db vr user gId +getGroupInfoBySimplexName :: DB.Connection -> VersionRangeChat -> User -> SimplexNameInfo -> ExceptT StoreError IO (Maybe GroupInfo) +getGroupInfoBySimplexName db vr user ni = + liftIO (getGroupIdBySimplexName db user ni) >>= \case + Nothing -> pure Nothing + Just gId -> Just <$> getGroupInfo db vr user gId + +getGroupIdBySimplexName :: DB.Connection -> User -> SimplexNameInfo -> IO (Maybe GroupId) +getGroupIdBySimplexName db User {userId} ni = + maybeFirstRow fromOnly $ + DB.query db "SELECT group_id FROM groups WHERE user_id = ? AND simplex_name = ?" (userId, ni) + getGroupMember :: DB.Connection -> VersionRangeChat -> User -> GroupId -> GroupMemberId -> ExceptT StoreError IO GroupMember getGroupMember db vr user@User {userId} groupId groupMemberId = ExceptT . firstRow (toContactMember vr user) (SEGroupMemberNotFound groupMemberId) $