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.
This commit is contained in:
shum
2026-06-03 18:26:35 +00:00
parent fb72ff6b1a
commit f2394d121f
5 changed files with 69 additions and 13 deletions
@@ -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."
+2 -2
View File
@@ -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
+31 -7
View File
@@ -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
+19
View File
@@ -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)
+13
View File
@@ -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) $