Files
simplex-chat/tests/NameResolver.hs
T
7306f3aeef core: alerts with information about registering names (#7653)
* minimal api change

* embed status. reduce

* drop fake owner change

* no-valid-link fix

* add UX flow for name lookups

* diagram edits

* corrections

* update name lookup API

* manual renaming

* implement core + cli

* implement UI

* fix

* build iOS app blindly

* shrink diff

* fix review comments

* review iterations

* opuse 5.5 /goal iterations

* fixes EP review

* fixes

* fixes

* short goal iterations

* refactor core-UI API to NameWarning with logic in core

* diff cosmetics

* simplify and normalize diagram

* add excalidraw normalizer script and test git hook

* cosmetics

* commit wireframe collab doc to repo as README

* review round on UX coverage

* iterate on /goal

* iterate on /goal

* add tests

* iterate on /goal

* fix freshness bug with local chats: When core marks a chat verified for a name, it now first un-verifies the user's other chats of the same kind:
  - a contact name covers contacts and business chats;
  - a channel name covers channels.

* align app divergences

* docs fixes

* iterations on /goal

* align excalidraw

* align and simplify core/UI/UXcanvas

* 27 rounds of /goal

* review

* core: simplify connect plan with name lookup

* simplify

* cache names in a separate table

* cache names resolution

* wip

* all

* fix

* remove cache

* update plans

* simplify

* bot types

* change types

* simplify

* update bot types

* remove review

* update view, tests

* simplify "unverify"

* update query plan

* changes

* simplify

* test helpers

* simpler

* update query plans

* fix

* query plans

* query plans

* fixes

* query plans

---------

Co-authored-by: Alain Brenzikofer <alain@encointer.org>
Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
2026-10-13 17:05:15 +01:00

126 lines
5.2 KiB
Haskell

{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
-- | Local HTTP names resolver for chat tests, copied from simplexmq's
-- NamesResolverServer and made dynamic: it answers /v2/resolve/<query> from a
-- mutable name -> answer registry, so a test can resolve a name to the
-- address it just created.
module NameResolver
( NameRegistry,
withNameResolver,
registerName,
registerExpiredName,
registerReservedName,
unregisterName,
failNameResolution,
emptyRecord,
contactNameRecord,
channelNameRecord,
contactAndChannelNameRecord,
resolverNamesConfig,
)
where
import Control.Concurrent.STM
import qualified Data.Aeson as J
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as M
import Data.Text (Text)
import Data.Text.Encoding (decodeLatin1)
import Network.HTTP.Types (hContentType, internalServerError500, notFound404, ok200)
import Network.Wai (Application, pathInfo, responseLBS)
import qualified Network.Wai.Handler.Warp as Warp
import Simplex.Messaging.Encoding.String (strEncode)
import Simplex.Messaging.Names.Record (NamePricing (..), NameRecord (..), NameRegistration (..), NameReservedReason, NameResponse (..), USDCents (..))
import Simplex.Messaging.Server.Names (NamesConfig (..))
import Simplex.Messaging.SimplexName (SimplexDomain (..), SimplexNameInfo (..), labelHash)
import Simplex.Messaging.SystemTime (RoundedSystemTime (..), getSystemSeconds)
data TestNameAnswer = AnswerRegistration NameRegistration | AnswerFails
type NameRegistry = TVar (Map Text TestNameAnswer)
-- | Run an action with a local resolver on a free port and its registry (keyed
-- by the query the resolver looks the name up by).
withNameResolver :: (Int -> NameRegistry -> IO a) -> IO a
withNameResolver action = do
reg <- newTVarIO M.empty
Warp.withApplication (pure (app reg)) $ \port -> action port reg
where
app :: NameRegistry -> Application
app reg req send = do
(st, body) <- case pathInfo req of
["health"] -> pure (ok200, "{}")
["v2", "resolve", q] -> answer . M.lookup q <$> readTVarIO reg
_ -> pure (notFound404, "{}")
send $ responseLBS st [(hContentType, "application/json")] body
answer (Just AnswerFails) = (internalServerError500, "{}")
answer (Just (AnswerRegistration registration)) = (ok200, J.encode NameResponse {lastBlockTs = Nothing, registration})
answer Nothing = (ok200, J.encode NameResponse {lastBlockTs = Nothing, registration = NRAvailable {pricing = NamePricing {registrationPrices = M.empty, basePrice = USDCents 1000, minLabelLength = 1}}})
-- | Register a name's domain to resolve to the given record.
registerName :: NameRegistry -> SimplexNameInfo -> NameRecord -> IO ()
registerName reg ni nameRecord =
registerAnswer reg ni $ AnswerRegistration NRRegistered {expires = Nothing, graceUntil = Nothing, reservedReason_ = Nothing, nameRecord}
registerAnswer :: NameRegistry -> SimplexNameInfo -> TestNameAnswer -> IO ()
registerAnswer reg ni a = atomically $ modifyTVar' reg $ M.insert (registryKey ni) a
registerExpiredName :: NameRegistry -> SimplexNameInfo -> NameRecord -> IO ()
registerExpiredName reg ni nameRecord = do
RoundedSystemTime now <- getSystemSeconds
let expires = Just $ RoundedSystemTime (now - 86400)
graceUntil = Just $ RoundedSystemTime (now + 30 * 86400)
registerAnswer reg ni $ AnswerRegistration NRRegistered {expires, graceUntil, reservedReason_ = Nothing, nameRecord}
registerReservedName :: NameRegistry -> SimplexNameInfo -> NameReservedReason -> IO ()
registerReservedName reg ni reservedReason = registerAnswer reg ni $ AnswerRegistration NRReserved {reservedReason}
failNameResolution :: NameRegistry -> SimplexNameInfo -> IO ()
failNameResolution reg ni = registerAnswer reg ni AnswerFails
unregisterName :: NameRegistry -> SimplexNameInfo -> IO ()
unregisterName reg ni = atomically $ modifyTVar' reg $ M.delete (registryKey ni)
registryKey :: SimplexNameInfo -> Text
registryKey SimplexNameInfo {nameDomain = SimplexDomain {nameTLD, domain}} =
decodeLatin1 $ strEncode (labelHash domain) <> strEncode nameTLD
contactNameRecord :: Text -> Text -> NameRecord
contactNameRecord name link = (emptyRecord name) {nrSimplexContact = [link]}
channelNameRecord :: Text -> Text -> NameRecord
channelNameRecord name link = (emptyRecord name) {nrSimplexChannel = [link]}
-- | A record whose domain resolves to both a direct contact link and a channel link.
contactAndChannelNameRecord :: Text -> Text -> Text -> NameRecord
contactAndChannelNameRecord name contactLink channelLink =
(emptyRecord name) {nrSimplexContact = [contactLink], nrSimplexChannel = [channelLink]}
emptyRecord :: Text -> NameRecord
emptyRecord name =
NameRecord
{ nrName = name,
nrNickname = "",
nrWebsite = "",
nrLocation = "",
nrSimplexContact = [],
nrSimplexChannel = [],
nrEth = Nothing,
nrBtc = Nothing,
nrXmr = Nothing,
nrDot = Nothing,
nrOwner = "",
nrResolver = ""
}
-- | NamesConfig for a chat test SMP server pointing at this resolver.
resolverNamesConfig :: Int -> NamesConfig
resolverNamesConfig port =
NamesConfig
{ resolverEndpoint = "http://127.0.0.1:" <> show port,
resolverAuth = Nothing,
resolverTimeoutMs = 1000,
resolverMaxResponseBytes = 65536
}