mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-07-02 15:41:44 +00:00
chat: RSLV-resolved NameRecord dispatched through prepared row
dispatchResolvedRecord now picks the first nrContactLinks (NTContact) or nrChannelLinks (NTPublicGroup) entry from the resolved record, decodes it as AConnShortLink, fetches the short-link data, and eagerly calls createPreparedContact / createPreparedGroup with the simplex_name set. Returning CPContactAddress (CAPKnown ct) / CPGroupLink (GLPKnown g ...) mirrors the local-store-hit branch of connectPlanName: hit and miss converge on the same plan shape, so the connectWithPlan caller cannot distinguish where the prepared row came from. Threading uses the existing Maybe SimplexNameInfo parameter added in c6f26150 for the local-prepare path -- no new write path or transient carrier. Pure helper firstNameLink is extracted and exported so the link-picker contract is testable without a DB / agent. ResolveNameTests gains five cases covering the per-type selection, the first-link policy, and the empty-list to CESimplexNameNotFound collapse.
This commit is contained in:
@@ -107,7 +107,7 @@ import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport (..), patt
|
||||
import Simplex.Messaging.Encoding
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Parsers (base64P)
|
||||
import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..), MsgFlags (..), NameRecord, NtfServer, ProtoServerWithAuth (..), ProtocolServer, ProtocolType (..), ProtocolTypeI (..), SMPServer, SProtocolType (..), SubscriptionMode (..), UserProtocol, userProtocol)
|
||||
import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..), MsgFlags (..), NameLink, NameRecord (..), NtfServer, ProtoServerWithAuth (..), ProtocolServer, ProtocolType (..), ProtocolTypeI (..), SMPServer, SProtocolType (..), SubscriptionMode (..), UserProtocol, unNameLink, userProtocol)
|
||||
import qualified Simplex.Messaging.Protocol as SMP
|
||||
import Simplex.Messaging.ServiceScheme (ServiceScheme (..))
|
||||
import qualified Simplex.Messaging.TMap as TM
|
||||
@@ -128,7 +128,7 @@ import qualified UnliftIO.Exception as E
|
||||
import UnliftIO.IO (hClose)
|
||||
import UnliftIO.STM
|
||||
#if defined(dbPostgres)
|
||||
import Data.Bifunctor (bimap, second)
|
||||
import Data.Bifunctor (bimap, first, second)
|
||||
import Simplex.Messaging.Agent.Client (SubInfo (..), getAgentQueuesInfo, getAgentWorkersDetails, getAgentWorkersSummary, temporaryOrHostError)
|
||||
#else
|
||||
import Data.Bifunctor (bimap, first, second)
|
||||
@@ -4205,7 +4205,7 @@ processChatCommand vr nm = \case
|
||||
resolveAndDispatch :: CM (ACreatedConnLink, ConnectionPlan)
|
||||
resolveAndDispatch =
|
||||
resolveOnUserServers user nameDomain >>= \case
|
||||
Right nr -> dispatchResolvedRecord user ni nr
|
||||
Right nr -> dispatchResolvedRecord vr nm user ni nr
|
||||
Left re -> throwError $ resolveErrorToChatError ni re
|
||||
connectWithPlan :: User -> IncognitoEnabled -> ACreatedConnLink -> ConnectionPlan -> CM ChatResponse
|
||||
connectWithPlan user@User {userId} incognito ccLink plan
|
||||
@@ -4663,10 +4663,76 @@ resolveOnUserServers user@User {userId} domain = do
|
||||
iterateResolvers srvs $ \srv ->
|
||||
liftIO . runExceptT $ resolveSimplexName a NRMInteractive userId srv domain
|
||||
|
||||
-- | Dispatch a resolved NameRecord through the existing link-based connect plan.
|
||||
-- Implemented in Task 6.
|
||||
dispatchResolvedRecord :: User -> SimplexNameInfo -> NameRecord -> CM (ACreatedConnLink, ConnectionPlan)
|
||||
dispatchResolvedRecord _ _ _ = throwChatError $ CEInternalError "dispatchResolvedRecord not yet implemented (Task 6)"
|
||||
-- | Dispatch a resolved NameRecord by eagerly preparing a contact/group row
|
||||
-- with @simplex_name@ set, then returning the same plan shape ('CAPKnown' /
|
||||
-- 'GLPKnown') the local-store-hit branch of 'connectPlanName' returns. The
|
||||
-- prepare-then-CAPKnown semantic threads the resolved name into persistence
|
||||
-- via the existing 'createPreparedContact' / 'createPreparedGroup' simplex_name
|
||||
-- parameter (introduced for the local-prepare path, see commit c6f26150), so
|
||||
-- the resolver hit reuses the same DB write path as a local-prepare hit.
|
||||
dispatchResolvedRecord :: VersionRangeChat -> NetworkRequestMode -> User -> SimplexNameInfo -> NameRecord -> CM (ACreatedConnLink, ConnectionPlan)
|
||||
dispatchResolvedRecord vr nm user ni@SimplexNameInfo {nameType} NameRecord {nrChannelLinks, nrContactLinks} = do
|
||||
lnk <- liftEither $ firstNameLink nameType nrChannelLinks nrContactLinks ni
|
||||
acl <- liftEither $ first (chatErrorAgent . AGENT . A_LINK) $ strDecode (encodeUtf8 lnk)
|
||||
prepareAndPlan acl
|
||||
where
|
||||
prepareAndPlan :: AConnShortLink -> CM (ACreatedConnLink, ConnectionPlan)
|
||||
prepareAndPlan (ACSL SCMInvitation _) =
|
||||
-- The resolver returns long-term contact/channel links; an invitation
|
||||
-- link in a NameRecord is malformed for this flow.
|
||||
throwError $ chatErrorAgent $ AGENT $ A_LINK "RSLV returned an invitation link"
|
||||
prepareAndPlan (ACSL SCMContact l) = case l of
|
||||
CSLContact _ CCTContact _ _ | nameType == NTContact -> prepareContact l
|
||||
CSLContact _ CCTChannel _ _ | nameType == NTPublicGroup -> prepareGroup l
|
||||
CSLContact _ CCTGroup _ _ | nameType == NTPublicGroup -> prepareGroup l
|
||||
_ -> throwError $ chatErrorAgent $ AGENT $ A_LINK "RSLV link kind does not match name type"
|
||||
prepareContact :: ConnShortLink 'CMContact -> CM (ACreatedConnLink, ConnectionPlan)
|
||||
prepareContact l = do
|
||||
let l' = serverShortLink' l
|
||||
(FixedLinkData {linkConnReq = cReq}, cData) <- getShortLinkConnReq nm user l'
|
||||
ContactShortLinkData {profile} <-
|
||||
liftIO (decodeLinkUserData cData) >>= maybe (throwError $ chatErrorAgent $ AGENT $ A_LINK "could not decode contact profile from RSLV link") pure
|
||||
let ccLink = CCLink cReq (Just l')
|
||||
accLink = ACCL SCMContact ccLink
|
||||
(ct, displaced_) <- withStore $ \db -> createPreparedContact db vr user profile accLink Nothing (Just ni)
|
||||
let Profile {simplexName = pSimplexName} = profile
|
||||
forM_ ((,) <$> pSimplexName <*> displaced_) $ \(claim, displaced) ->
|
||||
let Contact {localDisplayName = newLDN} = ct
|
||||
in toView $ CEvtSimplexNameConflict user claim SNCEContact newLDN displaced
|
||||
pure (accLink, CPContactAddress (CAPKnown ct))
|
||||
prepareGroup :: ConnShortLink 'CMContact -> CM (ACreatedConnLink, ConnectionPlan)
|
||||
prepareGroup l = do
|
||||
let l' = serverShortLink' l
|
||||
(FixedLinkData {linkConnReq = cReq}, cData@(ContactLinkData _ UserContactData {direct})) <- getShortLinkConnReq' nm user l'
|
||||
GroupShortLinkData {groupProfile, publicGroupData = publicGroupData_} <-
|
||||
liftIO (decodeLinkUserData cData) >>= maybe (throwError $ chatErrorAgent $ AGENT $ A_LINK "could not decode group profile from RSLV link") pure
|
||||
let publicMemberCount_ = (\PublicGroupData {publicMemberCount} -> publicMemberCount) <$> publicGroupData_
|
||||
useRelays = not direct
|
||||
subRole <- if useRelays then asks $ channelSubscriberRole . config else pure GRMember
|
||||
gVar <- asks random
|
||||
let ccLink = CCLink cReq (Just l')
|
||||
(g, _hostMember_) <- withStore $ \db -> createPreparedGroup db gVar vr user groupProfile False ccLink Nothing useRelays subRole publicMemberCount_ (Just ni)
|
||||
pure (ACCL SCMContact ccLink, CPGroupLink (GLPKnown g (BoolDef False) Nothing (ListDef [])))
|
||||
-- Mirror the inline 'serverShortLink' helper defined in 'processChatCommand'
|
||||
-- where this dispatch is invoked: RSLV-supplied short links may carry the
|
||||
-- agent's simplex:/ scheme, but the prepared row stores hostname-scheme,
|
||||
-- and the connect-plan / known-link lookups assume the hostname form.
|
||||
serverShortLink' :: ConnShortLink m -> ConnShortLink m
|
||||
serverShortLink' = \case
|
||||
CSLInvitation _ srv lnkId linkKey -> CSLInvitation SLSServer srv lnkId linkKey
|
||||
CSLContact _ ct srv linkKey -> CSLContact SLSServer ct srv linkKey
|
||||
|
||||
-- | Pick the first link from the @NameRecord@ matching the queried name type.
|
||||
-- An empty list (record exists but advertises no link of this kind) is
|
||||
-- treated as "not found" — same UX as a local-store miss.
|
||||
firstNameLink :: SimplexNameType -> [NameLink] -> [NameLink] -> SimplexNameInfo -> Either ChatError Text
|
||||
firstNameLink nameType channelLinks contactLinks ni = case links of
|
||||
l : _ -> Right $ unNameLink l
|
||||
[] -> Left $ ChatError $ CESimplexNameNotFound ni
|
||||
where
|
||||
links = case nameType of
|
||||
NTPublicGroup -> channelLinks
|
||||
NTContact -> contactLinks
|
||||
|
||||
-- | Map a resolver failure to the corresponding ChatError surfaced to the user.
|
||||
-- AUTH (NameNotRegistered) collapses to the same UX as a local-store miss, so
|
||||
|
||||
@@ -6,12 +6,13 @@ module ResolveNameTests (resolveNameTests) where
|
||||
import Data.Functor.Identity (Identity (..))
|
||||
import Data.IORef (IORef, modifyIORef', newIORef, readIORef)
|
||||
import qualified Data.Map.Strict as M
|
||||
import Data.Text (Text)
|
||||
import Simplex.Chat.Controller (ChatError (..), ChatErrorType (..))
|
||||
import Simplex.Chat.Library.Commands (ResolveError (..), iterateResolvers, resolveErrorToChatError)
|
||||
import Simplex.Chat.Library.Commands (ResolveError (..), firstNameLink, iterateResolvers, resolveErrorToChatError)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Agent.Protocol (AgentErrorType (..), SimplexNameDomain (..), SimplexNameInfo (..), SimplexNameType (..), SimplexTLD (..))
|
||||
import Simplex.Messaging.Client (ProxyClientError (..))
|
||||
import Simplex.Messaging.Protocol (NameRecord (..), SMPServer, mkNameOwner, pattern SMPServer)
|
||||
import Simplex.Messaging.Protocol (NameLink, NameRecord (..), SMPServer, mkNameLink, mkNameOwner, pattern SMPServer)
|
||||
import qualified Simplex.Messaging.Protocol as SMP
|
||||
import Test.Hspec
|
||||
|
||||
@@ -64,6 +65,34 @@ resolveNameTests = do
|
||||
case resolveErrorToChatError aliceNi (ResolverTransport timeoutErr) of
|
||||
ChatErrorAgent e _ _ -> e `shouldBe` timeoutErr
|
||||
other -> expectationFailure $ "expected ChatErrorAgent, got " <> show other
|
||||
-- firstNameLink is the pure link-picker used by dispatchResolvedRecord:
|
||||
-- it selects nrContactLinks for NTContact, nrChannelLinks for NTPublicGroup,
|
||||
-- returning the first entry. Empty lists collapse to CESimplexNameNotFound
|
||||
-- so the UX is identical to a local-store miss.
|
||||
describe "firstNameLink" $ do
|
||||
it "picks the first nrContactLinks entry for NTContact" $
|
||||
case firstNameLink NTContact [channelOne] [contactOne, contactTwo] aliceNi of
|
||||
Right lnk -> lnk `shouldBe` "simplex:/contact-alice"
|
||||
Left e -> expectationFailure $ "expected Right, got " <> show e
|
||||
it "picks the first nrChannelLinks entry for NTPublicGroup" $
|
||||
case firstNameLink NTPublicGroup [channelOne, channelTwo] [contactOne] groupNi of
|
||||
Right lnk -> lnk `shouldBe` "simplex:/channel-team"
|
||||
Left e -> expectationFailure $ "expected Right, got " <> show e
|
||||
it "returns CESimplexNameNotFound when nrContactLinks is empty for NTContact" $
|
||||
case firstNameLink NTContact [channelOne] [] aliceNi of
|
||||
Left (ChatError (CESimplexNameNotFound ni)) -> ni `shouldBe` aliceNi
|
||||
other -> expectationFailure $ "expected CESimplexNameNotFound, got " <> show other
|
||||
it "returns CESimplexNameNotFound when nrChannelLinks is empty for NTPublicGroup" $
|
||||
case firstNameLink NTPublicGroup [] [contactOne] groupNi of
|
||||
Left (ChatError (CESimplexNameNotFound ni)) -> ni `shouldBe` groupNi
|
||||
other -> expectationFailure $ "expected CESimplexNameNotFound, got " <> show other
|
||||
-- NTContact ignores nrChannelLinks even when nrContactLinks is empty.
|
||||
-- The resolver-side semantics say each name advertises a per-type link
|
||||
-- set; cross-type fallback would silently connect to the wrong target.
|
||||
it "does not fall back to nrChannelLinks for NTContact" $
|
||||
case firstNameLink NTContact [channelOne] [] aliceNi of
|
||||
Left (ChatError (CESimplexNameNotFound _)) -> pure ()
|
||||
other -> expectationFailure $ "expected CESimplexNameNotFound, got " <> show other
|
||||
|
||||
-- | Wrap a resolver to record which servers it was called for.
|
||||
recording :: IORef [SMPServer] -> (SMPServer -> IO (Either AgentErrorType NameRecord)) -> SMPServer -> IO (Either AgentErrorType NameRecord)
|
||||
@@ -78,6 +107,23 @@ stubAuthThenHit srv = pure $ M.findWithDefault (Right sampleRecord) srv $ M.from
|
||||
aliceNi :: SimplexNameInfo
|
||||
aliceNi = SimplexNameInfo NTContact (SimplexNameDomain TLDSimplex "alice" [])
|
||||
|
||||
groupNi :: SimplexNameInfo
|
||||
groupNi = SimplexNameInfo NTPublicGroup (SimplexNameDomain TLDSimplex "team" [])
|
||||
|
||||
-- Synthetic links to exercise firstNameLink's selection logic. mkNameLink only
|
||||
-- enforces the ≤1024-byte upper bound; the strings are deliberately not real
|
||||
-- AConnShortLink encodings — the link parser is exercised separately by the
|
||||
-- A_LINK error path inside dispatchResolvedRecord and by AConnShortLink's own
|
||||
-- round-trip tests in simplexmq.
|
||||
mkLink :: Text -> NameLink
|
||||
mkLink = either error id . mkNameLink
|
||||
|
||||
channelOne, channelTwo, contactOne, contactTwo :: NameLink
|
||||
channelOne = mkLink "simplex:/channel-team"
|
||||
channelTwo = mkLink "simplex:/channel-team-backup"
|
||||
contactOne = mkLink "simplex:/contact-alice"
|
||||
contactTwo = mkLink "simplex:/contact-alice-backup"
|
||||
|
||||
srv1 :: SMPServer
|
||||
srv1 = SMPServer "smp1.example" "5223" (C.KeyHash "\1\2\3\4")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user