chat: APIVerifySimplexName command + CEvtSimplexNameUnverified warning

Addresses the TOFU vulnerability where peer-claimed simplex_name was
accepted unverified. Adds:

- contacts.simplex_name_verified_at + groups.simplex_name_verified_at
  (M20260606_simplex_name_verified)
- APIVerifySimplexName ChatRef command: RSLV-resolves the claimed name
  and compares the resolved link to the peer's stored connection link;
  on match writes verified_at and emits CEvtSimplexNameVerified;
  on mismatch emits CEvtSimplexNameVerifyFailed
- CEvtSimplexNameUnverified passive warning emitted on incoming XInfo /
  XGrpInfo when a name claim arrives without a current verification
- updateContactProfileWithConflict / updateGroupProfileWithConflict
  clear simplex_name_verified_at whenever the peer's claim transitions
  (any value change including Nothing<->Just): the prior verification
  was bound to the prior claim.

UI can surface the unverified indicator next to a contact / group's
name, and prompt the user to invoke the verify command. This shifts
the security model from "TOFU + last-writer-wins" to "TOFU + on-demand
RSLV verification".
This commit is contained in:
shum
2026-06-06 08:33:25 +00:00
parent 3104291609
commit ebe90f7169
18 changed files with 322 additions and 32 deletions
+36
View File
@@ -486,6 +486,11 @@ data ChatCommand
| 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, connTarget_ :: Maybe ConnectTarget}
| -- Resolves the simplex_name claim on the chat row (contact or group) via
-- RSLV and compares the resolved link to the peer's stored connection link.
-- On match: writes simplex_name_verified_at and emits CEvtSimplexNameVerified.
-- On link mismatch or resolver failure: emits CEvtSimplexNameVerifyFailed.
APIVerifySimplexName {chatRef :: ChatRef}
| APIConnectContactViaAddress UserId IncognitoEnabled ContactId
| ConnectSimplex IncognitoEnabled -- UserId (not used in UI)
| DeleteContact ContactName ChatDeleteMode
@@ -952,11 +957,40 @@ data ChatEvent
-- (newer-claim-wins per RSLV); displacedFrom is the old row's local
-- display_name, claimedBy is the peer / group whose claim won.
CEvtSimplexNameConflict {user :: User, simplexName :: SimplexNameInfo, entity :: SimplexNameConflictEntity, claimedBy :: ContactName, displacedFrom :: ContactName}
| -- Emitted by APIVerifySimplexName when the RSLV-resolved link for the
-- claimed name matches the peer's stored connection link. simplex_name_verified_at
-- has been written; UI should clear the unverified indicator.
CEvtSimplexNameVerified {user :: User, chatRef :: ChatRef, simplexName :: SimplexNameInfo, verifiedAt :: UTCTime}
| -- Emitted by APIVerifySimplexName when verification did not succeed.
-- The simplex_name claim is NOT cleared; the user may still wish to keep
-- the contact/group. UI should surface the failure reason.
CEvtSimplexNameVerifyFailed {user :: User, chatRef :: ChatRef, simplexName :: SimplexNameInfo, reason :: SimplexNameVerifyFailReason}
| -- Passive warning emitted when an incoming XInfo / XGrpInfo carries a
-- simplex_name claim that the user has not (yet) verified — i.e.
-- simplex_name_verified_at is NULL. The UI is expected to show an
-- unverified indicator; the user can invoke APIVerifySimplexName to clear it.
CEvtSimplexNameUnverified {user :: User, chatRef :: ChatRef, simplexName :: SimplexNameInfo}
deriving (Show)
data SimplexNameConflictEntity = SNCEContact | SNCEGroup
deriving (Show)
-- | Why APIVerifySimplexName failed. The resolved record is not stashed: we
-- intentionally do not allow a "verified to point at a DIFFERENT contact"
-- state; the user must decide whether to keep the existing contact or start
-- a fresh connection with the resolved link.
data SimplexNameVerifyFailReason
= -- | Resolver returned a NameRecord but its link for this entity's type
-- (nrSimplexContact for contacts, nrSimplexChannel for groups) differs
-- from the link stored locally for the peer.
SNVFLinkMismatch
| -- | RSLV returned AUTH (NameNotRegistered): no on-chain record exists.
SNVFNameNotRegistered
| -- | Transport / proxy / other resolver-side failure. The agent error is
-- surfaced verbatim so the UI can reuse existing agent-error rendering.
SNVFResolverError {agentError :: AgentErrorType}
deriving (Eq, Show)
data TerminalEvent
= TEGroupLinkRejected {user :: User, groupInfo :: GroupInfo, groupRejectionReason :: GroupRejectionReason}
| TERelayRejected {user :: User, groupInfo :: GroupInfo, relayRejectionReason :: RelayRejectionReason}
@@ -1783,6 +1817,8 @@ $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CR") ''ChatResponse)
$(JQ.deriveJSON (enumJSON $ dropPrefix "SNCE") ''SimplexNameConflictEntity)
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "SNVF") ''SimplexNameVerifyFailReason)
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CEvt") ''ChatEvent)
$(JQ.deriveFromJSON defaultJSON ''ArchiveConfig)
+74
View File
@@ -2248,6 +2248,7 @@ processChatCommand vr nm = \case
(ccLink, plan) <- connectPlanName user ni
connectWithPlan user incognito ccLink plan
Connect _ Nothing -> throwChatError CEInvalidConnReq
APIVerifySimplexName chatRef -> withUser $ \user -> apiVerifySimplexName user chatRef
APIConnectContactViaAddress userId incognito contactId -> withUserId userId $ \user -> do
ct@Contact {profile = LocalProfile {contactLink}} <- withFastStore $ \db -> getContact db vr user contactId
ccLink <- case contactLink of
@@ -4749,6 +4750,78 @@ resolveErrorToChatError ni = \case
ResolverUnavailable -> ChatError $ CESimplexNameResolverUnavailable ni
ResolverTransport e -> chatErrorAgent e
-- | Best-effort comparison between an RSLV-resolved link (a 'Text' from the
-- name record) and the peer's stored connection link. Both are normalized via
-- 'strDecode' + 'strEncode' so scheme drift (simplex:/ vs https://simplex.chat)
-- doesn't cause a false negative. If the RSLV text fails to parse as a contact
-- link, we treat it as a mismatch — the resolver returned something we don't
-- understand, which is not a valid verification.
linksMatch :: Text -> ConnLinkContact -> Bool
linksMatch resolved stored = case strDecode (encodeUtf8 resolved) :: Either String AConnectionLink of
Right (ACL SCMContact resolvedLink) -> normalize resolvedLink == normalize stored
_ -> False
where
-- Mirror the inline 'serverShortLink' helper used elsewhere in this module:
-- the agent's simplex:/ scheme and the server-hostname scheme encode the
-- same link; verification must be scheme-insensitive.
normalize :: ConnLinkContact -> ByteString
normalize = \case
CLFull cReq -> strEncode cReq
CLShort (CSLContact _ ct srv linkKey) ->
strEncode (CSLContact SLSServer ct srv linkKey :: ConnShortLink 'CMContact)
-- | Resolves the chat row's simplex_name claim via RSLV and compares the
-- resolved per-type link to the peer's stored connection link. On match,
-- timestamps the contact/group row and emits CEvtSimplexNameVerified.
-- On mismatch / RSLV failure, emits CEvtSimplexNameVerifyFailed.
-- Throws CESimplexNameNotFound when the row has no claim to verify.
apiVerifySimplexName :: User -> ChatRef -> CM ChatResponse
apiVerifySimplexName user chatRef = do
vr <- chatVersionRange
(claim, storedLink, persistVerified) <- loadClaimAndLink vr
let domain = (\SimplexNameInfo {nameDomain} -> nameDomain) claim
nameType' = (\SimplexNameInfo {nameType} -> nameType) claim
resolveOnUserServers user domain >>= \case
Right NameRecord {nrSimplexContact, nrSimplexChannel} -> do
let resolvedLink = case nameType' of
NTContact -> nrSimplexContact
NTPublicGroup -> nrSimplexChannel
case resolvedLink of
Just lnk | linksMatch lnk storedLink -> do
ts <- liftIO getCurrentTime
withStore' $ \db -> persistVerified db ts
toView $ CEvtSimplexNameVerified user chatRef claim ts
_ ->
toView $ CEvtSimplexNameVerifyFailed user chatRef claim SNVFLinkMismatch
Left NameNotRegistered ->
toView $ CEvtSimplexNameVerifyFailed user chatRef claim SNVFNameNotRegistered
Left ResolverUnavailable ->
throwChatError $ CESimplexNameResolverUnavailable claim
Left (ResolverTransport e) ->
toView $ CEvtSimplexNameVerifyFailed user chatRef claim (SNVFResolverError e)
pure $ CRCmdOk (Just user)
where
-- Returns the claim to verify, the peer's stored link, and a callback that
-- persists the verified_at timestamp to the appropriate table. Throws a
-- command error when the row has no claim or no link (nothing to verify).
loadClaimAndLink :: VersionRangeChat -> CM (SimplexNameInfo, ConnLinkContact, DB.Connection -> UTCTime -> IO ())
loadClaimAndLink vr = case chatRef of
ChatRef CTDirect cId _ -> do
ct <- withFastStore $ \db -> getContact db vr user cId
let Contact {contactId, simplexName = ctSimplexName, profile = LocalProfile {contactLink}} = ct
claim <- maybe (throwCmdError "contact has no simplex_name to verify") pure ctSimplexName
lnk <- maybe (throwCmdError "contact has no stored link to verify against") pure contactLink
pure (claim, lnk, \db ts -> setContactSimplexNameVerifiedAt db user contactId ts)
ChatRef CTGroup gId _ -> do
g <- withFastStore $ \db -> getGroupInfo db vr user gId
let GroupInfo {groupId, simplexName = gSimplexName, preparedGroup} = g
claim <- maybe (throwCmdError "group has no simplex_name to verify") pure gSimplexName
PreparedGroup {connLinkToConnect = CCLink cReq shortLink_} <-
maybe (throwCmdError "group has no stored link to verify against") pure preparedGroup
let lnk = maybe (CLFull cReq) CLShort shortLink_
pure (claim, lnk, \db ts -> setGroupSimplexNameVerifiedAt db user groupId ts)
_ -> throwCmdError "APIVerifySimplexName supports only direct and group chat refs"
-- | Pure iteration logic for 'resolveOnUserServers'. Extracted so tests can
-- supply a stub resolver without standing up a real agent / proxy.
iterateResolvers ::
@@ -5395,6 +5468,7 @@ chatCommandP =
"/_set conn user :" *> (APIChangeConnectionUser <$> A.decimal <* A.space <*> A.decimal),
("/connect" <|> "/c") *> (AddContact <$> incognitoP),
("/connect" <|> "/c") *> (Connect <$> incognitoP <* A.space <*> ((Just <$> strP) <|> A.takeTill isSpace $> Nothing)),
"/_verify simplex name " *> (APIVerifySimplexName <$> chatRefP),
ForwardMessage <$> chatNameP <* " <- @" <*> displayNameP <* A.space <*> msgTextP,
ForwardGroupMessage <$> chatNameP <* " <- #" <*> displayNameP <* A.space <* A.char '@' <*> (Just <$> displayNameP) <* A.space <*> msgTextP,
ForwardGroupMessage <$> chatNameP <* " <- #" <*> displayNameP <*> pure Nothing <* A.space <*> msgTextP,
+13 -2
View File
@@ -2566,8 +2566,15 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
else do
c' <- liftIO $ updateContactUserPreferences db user c ctUserPrefs'
updateContactProfileWithConflict db user c' p'
let Contact {localDisplayName = newLDN} = c'
let Contact {contactId = ctId, localDisplayName = newLDN, simplexNameVerifiedAt = vAt} = c'
surfaceSimplexNameConflict user p'SimplexName displaced_ SNCEContact newLDN
-- Passive unverified warning: a non-empty incoming claim that the user
-- has not verified (or whose prior verification was cleared by the
-- claim transition in updateContactProfileWithConflict) should be
-- surfaced so the UI can prompt the user to invoke APIVerifySimplexName.
forM_ p'SimplexName $ \ni ->
when (isNothing vAt) $
toView $ CEvtSimplexNameUnverified user (ChatRef CTDirect ctId Nothing) ni
when (directOrUsed c' && createItems) $ do
createProfileUpdatedItem c'
lift $ createRcvFeatureItems user c c'
@@ -3307,8 +3314,12 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
case businessChat of
Nothing -> unless (p == p') $ do
(g', displaced_) <- withStore $ \db -> updateGroupProfileWithConflict db user g p'
let GroupInfo {localDisplayName = newLDN} = g'
let GroupInfo {groupId = gId, localDisplayName = newLDN, simplexNameVerifiedAt = vAt} = g'
surfaceSimplexNameConflict user p'GroupSimplexName displaced_ SNCEGroup newLDN
-- Passive unverified warning, mirrors processContactProfileUpdate.
forM_ p'GroupSimplexName $ \ni ->
when (isNothing vAt) $
toView $ CEvtSimplexNameUnverified user (ChatRef CTGroup gId Nothing) ni
(g'', m', scopeInfo) <- mkGroupChatScope g' m
toView $ CEvtGroupUpdated user g g'' (Just m') msgSigned
let cd = CDGroupRcv g'' scopeInfo m'
+2
View File
@@ -1461,6 +1461,8 @@ instance MsgDirectionI d => ToJSON (CIFile d) where
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "GCS") ''GroupChatScope)
$(JQ.deriveJSON defaultJSON ''ChatRef)
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "JCI") ''JSONCIDirection)
instance (ChatTypeI c, MsgDirectionI d) => FromJSON (CIDirection c d) where
+3 -3
View File
@@ -120,7 +120,7 @@ getConnectionEntity db vr user@User {userId, userContactId} agentConnId = do
|]
(userId, contactId, CSActive)
toContact' :: Int64 -> Connection -> [ChatTagId] -> ContactRow' -> Contact
toContact' contactId conn chatTags ((profileId, localDisplayName, displayName, fullName, shortDescr, image, contactLink, peerType, localAlias, BI contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, BI favorite, preferences, userPreferences, createdAt, updatedAt, chatTs) :. preparedContactRow :. (contactRequestId, contactGroupMemberId, BI contactGrpInvSent) :. groupDirectInvRow :. (uiThemes, BI chatDeleted, customData, chatItemTTL, ctSimplexNameRaw, cpSimplexNameRaw)) =
toContact' contactId conn chatTags ((profileId, localDisplayName, displayName, fullName, shortDescr, image, contactLink, peerType, localAlias, BI contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, BI favorite, preferences, userPreferences, createdAt, updatedAt, chatTs) :. preparedContactRow :. (contactRequestId, contactGroupMemberId, BI contactGrpInvSent) :. groupDirectInvRow :. (uiThemes, BI chatDeleted, customData, chatItemTTL, ctSimplexNameRaw, cpSimplexNameRaw, simplexNameVerifiedAt)) =
let simplexName = decodeSimplexName ctSimplexNameRaw
profile = LocalProfile {profileId, displayName, fullName, shortDescr, image, contactLink, simplexName = decodeSimplexName cpSimplexNameRaw, peerType, preferences, localAlias}
chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts = unBI <$> sendRcpts, favorite}
@@ -128,7 +128,7 @@ getConnectionEntity db vr user@User {userId, userContactId} agentConnId = do
activeConn = Just conn
preparedContact = toPreparedContact preparedContactRow
groupDirectInv = toGroupDirectInvitation groupDirectInvRow
in Contact {contactId, localDisplayName, profile, activeConn, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, preparedContact, contactRequestId, contactGroupMemberId, contactGrpInvSent, groupDirectInv, chatTags, chatItemTTL, uiThemes, chatDeleted, customData, simplexName}
in Contact {contactId, localDisplayName, profile, activeConn, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, preparedContact, contactRequestId, contactGroupMemberId, contactGrpInvSent, groupDirectInv, chatTags, chatItemTTL, uiThemes, chatDeleted, customData, simplexName, simplexNameVerifiedAt}
getGroupAndMember_ :: Int64 -> Connection -> ExceptT StoreError IO (GroupInfo, GroupMember)
getGroupAndMember_ groupMemberId c = do
gm <-
@@ -149,7 +149,7 @@ getConnectionEntity db vr user@User {userId, userContactId} agentConnId = do
g.use_relays, g.relay_own_status,
g.ui_themes, g.summary_current_members_count, g.public_member_count, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri,
g.root_priv_key, g.root_pub_key, g.member_priv_key,
g.simplex_name, gp.simplex_name,
g.simplex_name, gp.simplex_name, g.simplex_name_verified_at,
-- GroupInfo {membership}
mu.group_member_id, mu.group_id, mu.index_in_group, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category,
mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id,
+1 -1
View File
@@ -113,7 +113,7 @@ createOrUpdateContactRequest
ct.contact_id, ct.contact_profile_id, ct.local_display_name, cp.display_name, cp.full_name, cp.short_descr, cp.image, cp.contact_link, cp.chat_peer_type, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite,
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.request_shared_msg_id, ct.contact_request_id,
ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.grp_direct_inv_link, ct.grp_direct_inv_from_group_id, ct.grp_direct_inv_from_group_member_id, ct.grp_direct_inv_from_member_conn_id, ct.grp_direct_inv_started_connection,
ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, ct.simplex_name, cp.simplex_name,
ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, ct.simplex_name, cp.simplex_name, ct.simplex_name_verified_at,
-- Connection
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias,
c.contact_id, c.group_member_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter,
+29 -6
View File
@@ -54,6 +54,7 @@ module Simplex.Chat.Store.Direct
getContactIdBySimplexName,
updateContactProfile,
updateContactProfileWithConflict,
setContactSimplexNameVerifiedAt,
updateContactUserPreferences,
updateContactAlias,
updateContactConnectionAlias,
@@ -322,7 +323,7 @@ getContactByConnReqHash db vr user@User {userId} cReqHash1 cReqHash2 = do
ct.contact_id, ct.contact_profile_id, ct.local_display_name, cp.display_name, cp.full_name, cp.short_descr, cp.image, cp.contact_link, cp.chat_peer_type, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite,
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.request_shared_msg_id, ct.contact_request_id,
ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.grp_direct_inv_link, ct.grp_direct_inv_from_group_id, ct.grp_direct_inv_from_group_member_id, ct.grp_direct_inv_from_member_conn_id, ct.grp_direct_inv_started_connection,
ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, ct.simplex_name, cp.simplex_name,
ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, ct.simplex_name, cp.simplex_name, ct.simplex_name_verified_at,
-- Connection
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias,
c.contact_id, c.group_member_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter,
@@ -573,24 +574,45 @@ updateContactProfile db user c p' = fst <$> updateContactProfileWithConflict db
-- (user_id, simplex_name) — returning that row's display_name so the caller
-- can emit CEvtSimplexNameConflict. Used by the incoming-XInfo path; local
-- updates that don't expect conflicts can continue to use updateContactProfile.
--
-- Also clears contacts.simplex_name_verified_at when the peer's simplex_name
-- claim changes (any value transition, including Nothing<->Just): the prior
-- verification was tied to the prior claim and must be re-issued by the user.
updateContactProfileWithConflict :: DB.Connection -> User -> Contact -> Profile -> ExceptT StoreError IO (Contact, Maybe ContactName)
updateContactProfileWithConflict db user@User {userId} c p'
| displayName == newName = do
displaced <- liftIO $ clearConflictingContactProfileSimplexName_ db userId (Just profileId) profileSimplexName
liftIO $ updateContactProfile_ db userId profileId p'
pure (c {profile, mergedPreferences}, displaced)
liftIO clearVerifiedAtIfClaimChanged
pure (c' {profile, mergedPreferences}, displaced)
| otherwise =
ExceptT . withLocalDisplayName db userId newName $ \ldn -> do
currentTs <- getCurrentTime
displaced <- clearConflictingContactProfileSimplexName_ db userId (Just profileId) profileSimplexName
updateContactProfile_' db userId profileId p' currentTs
updateContactLDN_ db user contactId localDisplayName ldn currentTs
pure $ Right (c {localDisplayName = ldn, profile, mergedPreferences}, displaced)
clearVerifiedAtIfClaimChanged
pure $ Right (c' {localDisplayName = ldn, profile, mergedPreferences}, displaced)
where
Contact {contactId, localDisplayName, profile = LocalProfile {profileId, displayName, localAlias}, userPreferences} = c
Contact {contactId, localDisplayName, profile = LocalProfile {profileId, displayName, localAlias, simplexName = prevClaim}, userPreferences} = c
Profile {displayName = newName, simplexName = profileSimplexName, preferences} = p'
profile = toLocalProfile profileId p' localAlias
mergedPreferences = contactUserPreferences user userPreferences preferences $ contactConnIncognito c
claimChanged = prevClaim /= profileSimplexName
c' = if claimChanged then (c :: Contact) {simplexNameVerifiedAt = Nothing} else c
clearVerifiedAtIfClaimChanged =
when claimChanged $
DB.execute db "UPDATE contacts SET simplex_name_verified_at = NULL WHERE user_id = ? AND contact_id = ?" (userId, contactId)
-- | Records that the user successfully RSLV-verified the peer's simplex_name
-- claim against the contact's stored connection link. Cleared back to NULL by
-- updateContactProfileWithConflict whenever the peer's claim transitions.
setContactSimplexNameVerifiedAt :: DB.Connection -> User -> ContactId -> UTCTime -> IO ()
setContactSimplexNameVerifiedAt db User {userId} contactId ts =
DB.execute
db
"UPDATE contacts SET simplex_name_verified_at = ? WHERE user_id = ? AND contact_id = ?"
(ts, userId, contactId)
updateContactUserPreferences :: DB.Connection -> User -> Contact -> Preferences -> IO Contact
updateContactUserPreferences db user@User {userId} c@Contact {contactId} userPreferences = do
@@ -908,7 +930,8 @@ createContactFromRequest db user@User {userId, profile = LocalProfile {preferenc
uiThemes = Nothing,
chatDeleted = False,
customData = Nothing,
simplexName = Nothing
simplexName = Nothing,
simplexNameVerifiedAt = Nothing
}
pure (ct, conn)
@@ -955,7 +978,7 @@ getContact_ db vr user@User {userId} contactId deleted = do
ct.contact_id, ct.contact_profile_id, ct.local_display_name, cp.display_name, cp.full_name, cp.short_descr, cp.image, cp.contact_link, cp.chat_peer_type, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite,
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.request_shared_msg_id, ct.contact_request_id,
ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.grp_direct_inv_link, ct.grp_direct_inv_from_group_id, ct.grp_direct_inv_from_group_member_id, ct.grp_direct_inv_from_member_conn_id, ct.grp_direct_inv_started_connection,
ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, ct.simplex_name, cp.simplex_name,
ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, ct.simplex_name, cp.simplex_name, ct.simplex_name_verified_at,
-- Connection
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias,
c.contact_id, c.group_member_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter,
+27 -6
View File
@@ -47,6 +47,7 @@ module Simplex.Chat.Store.Groups
getGroupInfoByGroupLinkHash,
updateGroupProfile,
updateGroupProfileWithConflict,
setGroupSimplexNameVerifiedAt,
clearConflictingGroupProfileSimplexName_,
updateGroupPreferences,
updateGroupProfileFromMember,
@@ -425,7 +426,8 @@ createNewGroup db vr user@User {userId} groupProfile incognitoProfile useRelays
membersRequireAttention = 0,
viaGroupLinkUri = Nothing,
groupKeys,
simplexName = Nothing
simplexName = Nothing,
simplexNameVerifiedAt = Nothing
}
-- | creates a new group record for the group the current user was invited to, or returns an existing one
@@ -503,7 +505,8 @@ createGroupInvitation db vr user@User {userId} contact@Contact {contactId, activ
membersRequireAttention = 0,
viaGroupLinkUri = Nothing,
groupKeys = Nothing,
simplexName = Nothing
simplexName = Nothing,
simplexNameVerifiedAt = Nothing
},
groupMemberId
)
@@ -2393,13 +2396,14 @@ updateGroupProfile db user g p' = fst <$> updateGroupProfileWithConflict db user
-- (user_id, simplex_name) — returning that row's display_name so the caller
-- can emit CEvtSimplexNameConflict. Used by the XGrpInfo path.
updateGroupProfileWithConflict :: DB.Connection -> User -> GroupInfo -> GroupProfile -> ExceptT StoreError IO (GroupInfo, Maybe GroupName)
updateGroupProfileWithConflict db user@User {userId} g@GroupInfo {groupId, localDisplayName, groupProfile = GroupProfile {displayName}} p'@GroupProfile {displayName = newName, fullName, shortDescr, description, image, publicGroup, simplexName, groupPreferences, memberAdmission}
updateGroupProfileWithConflict db user@User {userId} g@GroupInfo {groupId, localDisplayName, groupProfile = GroupProfile {displayName, simplexName = prevClaim}} p'@GroupProfile {displayName = newName, fullName, shortDescr, description, image, publicGroup, simplexName, groupPreferences, memberAdmission}
| displayName == newName = liftIO $ do
currentTs <- getCurrentTime
profileId_ <- getGroupProfileId_
displaced <- clearConflictingGroupProfileSimplexName_ db userId profileId_ simplexName
updateGroupProfile_ currentTs
pure ((g :: GroupInfo) {groupProfile = p', fullGroupPreferences}, displaced)
clearVerifiedAtIfClaimChanged
pure ((g' :: GroupInfo) {groupProfile = p', fullGroupPreferences}, displaced)
| otherwise =
ExceptT . withLocalDisplayName db userId newName $ \ldn -> do
currentTs <- getCurrentTime
@@ -2407,9 +2411,18 @@ updateGroupProfileWithConflict db user@User {userId} g@GroupInfo {groupId, local
displaced <- clearConflictingGroupProfileSimplexName_ db userId profileId_ simplexName
updateGroupProfile_ currentTs
updateGroup_ ldn currentTs
pure $ Right ((g :: GroupInfo) {localDisplayName = ldn, groupProfile = p', fullGroupPreferences}, displaced)
clearVerifiedAtIfClaimChanged
pure $ Right ((g' :: GroupInfo) {localDisplayName = ldn, groupProfile = p', fullGroupPreferences}, displaced)
where
fullGroupPreferences = mergeGroupPreferences groupPreferences
claimChanged = prevClaim /= simplexName
g' = if claimChanged then (g :: GroupInfo) {simplexNameVerifiedAt = Nothing} else g
-- Mirrors updateContactProfileWithConflict: clear the verification when
-- the peer's claim transitions to/from/between values; prior verification
-- was bound to the prior claim.
clearVerifiedAtIfClaimChanged =
when claimChanged $
DB.execute db "UPDATE groups SET simplex_name_verified_at = NULL WHERE user_id = ? AND group_id = ?" (userId, groupId)
(groupType_, groupLink_) = case publicGroup of
Just PublicGroupProfile {groupType, groupLink} -> (Just groupType, Just groupLink)
Nothing -> (Nothing, Nothing)
@@ -2475,6 +2488,14 @@ clearConflictingGroupProfileSimplexName_ db userId (Just profileId) (Just simple
|]
(userId, simplexName, profileId)
-- | Mirror of setContactSimplexNameVerifiedAt for groups.
setGroupSimplexNameVerifiedAt :: DB.Connection -> User -> GroupId -> UTCTime -> IO ()
setGroupSimplexNameVerifiedAt db User {userId} groupId ts =
DB.execute
db
"UPDATE groups SET simplex_name_verified_at = ? WHERE user_id = ? AND group_id = ?"
(ts, userId, groupId)
updateGroupPreferences :: DB.Connection -> User -> GroupInfo -> GroupPreferences -> IO GroupInfo
updateGroupPreferences db User {userId} g@GroupInfo {groupId, groupProfile = p} ps = do
currentTs <- getCurrentTime
@@ -2962,7 +2983,7 @@ createMemberContact
simplexName = Nothing
}
mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito ctConn
pure Contact {contactId, localDisplayName, profile = memberProfile, activeConn = Just ctConn, contactUsed = True, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, preparedContact = Nothing, contactRequestId = Nothing, contactGroupMemberId = Just groupMemberId, contactGrpInvSent = False, groupDirectInv = Nothing, chatTags = [], chatItemTTL = Nothing, uiThemes = Nothing, chatDeleted = False, customData = Nothing, simplexName = Nothing}
pure Contact {contactId, localDisplayName, profile = memberProfile, activeConn = Just ctConn, contactUsed = True, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, preparedContact = Nothing, contactRequestId = Nothing, contactGroupMemberId = Just groupMemberId, contactGrpInvSent = False, groupDirectInv = Nothing, chatTags = [], chatItemTTL = Nothing, uiThemes = Nothing, chatDeleted = False, customData = Nothing, simplexName = Nothing, simplexNameVerifiedAt = Nothing}
getMemberContact :: DB.Connection -> VersionRangeChat -> User -> ContactId -> ExceptT StoreError IO (GroupInfo, GroupMember, Contact, ConnReqInvitation)
getMemberContact db vr user contactId = do
@@ -37,6 +37,7 @@ import Simplex.Chat.Store.Postgres.Migrations.M20260530_client_services
import Simplex.Chat.Store.Postgres.Migrations.M20260531_member_removed_at
import Simplex.Chat.Store.Postgres.Migrations.M20260603_simplex_name
import Simplex.Chat.Store.Postgres.Migrations.M20260604_simplex_name_profiles
import Simplex.Chat.Store.Postgres.Migrations.M20260606_simplex_name_verified
import Simplex.Messaging.Agent.Store.Shared (Migration (..))
schemaMigrations :: [(String, Text, Maybe Text)]
@@ -73,7 +74,8 @@ schemaMigrations =
("20260530_client_services", m20260530_client_services, Just down_m20260530_client_services),
("20260531_member_removed_at", m20260531_member_removed_at, Just down_m20260531_member_removed_at),
("20260603_simplex_name", m20260603_simplex_name, Just down_m20260603_simplex_name),
("20260604_simplex_name_profiles", m20260604_simplex_name_profiles, Just down_m20260604_simplex_name_profiles)
("20260604_simplex_name_profiles", m20260604_simplex_name_profiles, Just down_m20260604_simplex_name_profiles),
("20260606_simplex_name_verified", m20260606_simplex_name_verified, Just down_m20260606_simplex_name_verified)
]
-- | The list of migrations in ascending order by date
@@ -0,0 +1,21 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Chat.Store.Postgres.Migrations.M20260606_simplex_name_verified where
import Data.Text (Text)
import Text.RawString.QQ (r)
m20260606_simplex_name_verified :: Text
m20260606_simplex_name_verified =
[r|
ALTER TABLE contacts ADD COLUMN simplex_name_verified_at TIMESTAMPTZ;
ALTER TABLE groups ADD COLUMN simplex_name_verified_at TIMESTAMPTZ;
|]
down_m20260606_simplex_name_verified :: Text
down_m20260606_simplex_name_verified =
[r|
ALTER TABLE groups DROP COLUMN simplex_name_verified_at;
ALTER TABLE contacts DROP COLUMN simplex_name_verified_at;
|]
+3 -1
View File
@@ -160,6 +160,7 @@ import Simplex.Chat.Store.SQLite.Migrations.M20260530_client_services
import Simplex.Chat.Store.SQLite.Migrations.M20260531_member_removed_at
import Simplex.Chat.Store.SQLite.Migrations.M20260603_simplex_name
import Simplex.Chat.Store.SQLite.Migrations.M20260604_simplex_name_profiles
import Simplex.Chat.Store.SQLite.Migrations.M20260606_simplex_name_verified
import Simplex.Messaging.Agent.Store.Shared (Migration (..))
schemaMigrations :: [(String, Query, Maybe Query)]
@@ -319,7 +320,8 @@ schemaMigrations =
("20260530_client_services", m20260530_client_services, Just down_m20260530_client_services),
("20260531_member_removed_at", m20260531_member_removed_at, Just down_m20260531_member_removed_at),
("20260603_simplex_name", m20260603_simplex_name, Just down_m20260603_simplex_name),
("20260604_simplex_name_profiles", m20260604_simplex_name_profiles, Just down_m20260604_simplex_name_profiles)
("20260604_simplex_name_profiles", m20260604_simplex_name_profiles, Just down_m20260604_simplex_name_profiles),
("20260606_simplex_name_verified", m20260606_simplex_name_verified, Just down_m20260606_simplex_name_verified)
]
-- | The list of migrations in ascending order by date
@@ -0,0 +1,26 @@
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Chat.Store.SQLite.Migrations.M20260606_simplex_name_verified where
import Database.SQLite.Simple (Query)
import Database.SQLite.Simple.QQ (sql)
-- contacts.simplex_name_verified_at and groups.simplex_name_verified_at record
-- the timestamp when the user last verified that the peer's claimed simplex_name
-- resolves (via RSLV) to the link stored locally for the contact/group.
-- NULL means the claim is unverified and the UI should show an indicator.
-- The column is cleared back to NULL whenever the simplex_name claim changes
-- (updateContactProfileWithConflict / updateGroupProfileWithConflict).
m20260606_simplex_name_verified :: Query
m20260606_simplex_name_verified =
[sql|
ALTER TABLE contacts ADD COLUMN simplex_name_verified_at TEXT;
ALTER TABLE groups ADD COLUMN simplex_name_verified_at TEXT;
|]
down_m20260606_simplex_name_verified :: Query
down_m20260606_simplex_name_verified =
[sql|
ALTER TABLE groups DROP COLUMN simplex_name_verified_at;
ALTER TABLE contacts DROP COLUMN simplex_name_verified_at;
|]
@@ -94,6 +94,7 @@ CREATE TABLE contacts(
grp_direct_inv_from_member_conn_id INTEGER REFERENCES connections(connection_id) ON DELETE SET NULL,
grp_direct_inv_started_connection INTEGER NOT NULL DEFAULT 0,
simplex_name TEXT,
simplex_name_verified_at TEXT,
FOREIGN KEY(user_id, local_display_name)
REFERENCES display_names(user_id, local_display_name)
ON DELETE CASCADE
@@ -186,7 +187,8 @@ CREATE TABLE groups(
relay_request_delay INTEGER NOT NULL DEFAULT 0,
relay_request_execute_at TEXT NOT NULL DEFAULT '1970-01-01 00:00:00',
relay_inactive_at TEXT,
simplex_name TEXT, -- received
simplex_name TEXT,
simplex_name_verified_at TEXT, -- received
FOREIGN KEY(user_id, local_display_name)
REFERENCES display_names(user_id, local_display_name)
ON DELETE CASCADE
+7 -7
View File
@@ -544,14 +544,14 @@ type PreparedContactRow = (Maybe AConnectionRequestUri, Maybe AConnShortLink, Ma
type GroupDirectInvitationRow = (Maybe ConnReqInvitation, Maybe GroupId, Maybe GroupMemberId, Maybe Int64, BoolInt)
type ContactRow' = (ProfileId, ContactName, ContactName, Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, LocalAlias, BoolInt, ContactStatus) :. (Maybe MsgFilter, Maybe BoolInt, BoolInt, Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime) :. PreparedContactRow :. (Maybe Int64, Maybe GroupMemberId, BoolInt) :. GroupDirectInvitationRow :. (Maybe UIThemeEntityOverrides, BoolInt, Maybe CustomData, Maybe Int64, Maybe Text, Maybe Text)
type ContactRow' = (ProfileId, ContactName, ContactName, Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, LocalAlias, BoolInt, ContactStatus) :. (Maybe MsgFilter, Maybe BoolInt, BoolInt, Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime) :. PreparedContactRow :. (Maybe Int64, Maybe GroupMemberId, BoolInt) :. GroupDirectInvitationRow :. (Maybe UIThemeEntityOverrides, BoolInt, Maybe CustomData, Maybe Int64, Maybe Text, Maybe Text, Maybe UTCTime)
type ContactRow = Only ContactId :. ContactRow'
-- ct.simplex_name -> Contact.simplexName (user's locally-known label)
-- cp.simplex_name -> LocalProfile.simplexName (peer's broadcast claim)
toContact :: VersionRangeChat -> User -> [ChatTagId] -> ContactRow :. MaybeConnectionRow -> Contact
toContact vr user chatTags ((Only contactId :. (profileId, localDisplayName, displayName, fullName, shortDescr, image, contactLink, peerType, localAlias, BI contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, BI favorite, preferences, userPreferences, createdAt, updatedAt, chatTs) :. preparedContactRow :. (contactRequestId, contactGroupMemberId, BI contactGrpInvSent) :. groupDirectInvRow :. (uiThemes, BI chatDeleted, customData, chatItemTTL, ctSimplexNameRaw, cpSimplexNameRaw)) :. connRow) =
toContact vr user chatTags ((Only contactId :. (profileId, localDisplayName, displayName, fullName, shortDescr, image, contactLink, peerType, localAlias, BI contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, BI favorite, preferences, userPreferences, createdAt, updatedAt, chatTs) :. preparedContactRow :. (contactRequestId, contactGroupMemberId, BI contactGrpInvSent) :. groupDirectInvRow :. (uiThemes, BI chatDeleted, customData, chatItemTTL, ctSimplexNameRaw, cpSimplexNameRaw, simplexNameVerifiedAt)) :. connRow) =
let simplexName = decodeSimplexName ctSimplexNameRaw
profile = LocalProfile {profileId, displayName, fullName, shortDescr, image, contactLink, simplexName = decodeSimplexName cpSimplexNameRaw, peerType, preferences, localAlias}
activeConn = toMaybeConnection vr connRow
@@ -560,7 +560,7 @@ toContact vr user chatTags ((Only contactId :. (profileId, localDisplayName, dis
mergedPreferences = contactUserPreferences user userPreferences preferences incognito
preparedContact = toPreparedContact preparedContactRow
groupDirectInv = toGroupDirectInvitation groupDirectInvRow
in Contact {contactId, localDisplayName, profile, activeConn, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, preparedContact, contactRequestId, contactGroupMemberId, contactGrpInvSent, groupDirectInv, chatTags, chatItemTTL, uiThemes, chatDeleted, customData, simplexName}
in Contact {contactId, localDisplayName, profile, activeConn, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, preparedContact, contactRequestId, contactGroupMemberId, contactGrpInvSent, groupDirectInv, chatTags, chatItemTTL, uiThemes, chatDeleted, customData, simplexName, simplexNameVerifiedAt}
toPreparedContact :: PreparedContactRow -> Maybe PreparedContact
toPreparedContact (connFullLink, connShortLink, welcomeSharedMsgId, requestSharedMsgId) =
@@ -728,7 +728,7 @@ type BusinessChatInfoRow = (Maybe BusinessChatType, Maybe MemberId, Maybe Member
type GroupKeysRow = (Maybe C.PrivateKeyEd25519, Maybe C.PublicKeyEd25519, Maybe C.PrivateKeyEd25519)
type GroupInfoRow = (Int64, GroupName, GroupName, Text, Maybe Text, Text, Maybe Text, Maybe ImageData, Maybe GroupType, Maybe ShortLinkContact, Maybe B64UrlByteString) :. PublicGroupAccessRow :. (Maybe MsgFilter, Maybe BoolInt, BoolInt, Maybe GroupPreferences, Maybe GroupMemberAdmission) :. (UTCTime, UTCTime, Maybe UTCTime, Maybe UTCTime) :. PreparedGroupRow :. BusinessChatInfoRow :. (BoolInt, Maybe RelayStatus, Maybe UIThemeEntityOverrides, Int64, Maybe Int64, Maybe CustomData, Maybe Int64, Int, Maybe ConnReqContact) :. GroupKeysRow :. (Maybe Text, Maybe Text) :. GroupMemberRow
type GroupInfoRow = (Int64, GroupName, GroupName, Text, Maybe Text, Text, Maybe Text, Maybe ImageData, Maybe GroupType, Maybe ShortLinkContact, Maybe B64UrlByteString) :. PublicGroupAccessRow :. (Maybe MsgFilter, Maybe BoolInt, BoolInt, Maybe GroupPreferences, Maybe GroupMemberAdmission) :. (UTCTime, UTCTime, Maybe UTCTime, Maybe UTCTime) :. PreparedGroupRow :. BusinessChatInfoRow :. (BoolInt, Maybe RelayStatus, Maybe UIThemeEntityOverrides, Int64, Maybe Int64, Maybe CustomData, Maybe Int64, Int, Maybe ConnReqContact) :. GroupKeysRow :. (Maybe Text, Maybe Text, Maybe UTCTime) :. GroupMemberRow
type PublicGroupAccessRow = (Maybe Text, Maybe Text, Maybe BoolInt, Maybe BoolInt)
@@ -737,7 +737,7 @@ type GroupMemberRow = (GroupMemberId, GroupId, Int64, MemberId, VersionChat, Ver
type ProfileRow = (ProfileId, ContactName, Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, LocalAlias, Maybe Preferences, Maybe Text)
toGroupInfo :: VersionRangeChat -> Int64 -> [ChatTagId] -> GroupInfoRow -> GroupInfo
toGroupInfo vr userContactId chatTags ((groupId, localDisplayName, displayName, fullName, shortDescr, localAlias, description, image, groupType_, groupLink_, publicGroupId_) :. accessRow :. (enableNtfs_, sendRcpts, BI favorite, groupPreferences, memberAdmission) :. (createdAt, updatedAt, chatTs, userMemberProfileSentAt) :. preparedGroupRow :. businessRow :. (BI useRelays, relayOwnStatus, uiThemes, currentMembers, publicMemberCount, customData, chatItemTTL, membersRequireAttention, viaGroupLinkUri) :. groupKeysRow :. (gSimplexNameRaw, gpSimplexNameRaw) :. userMemberRow) =
toGroupInfo vr userContactId chatTags ((groupId, localDisplayName, displayName, fullName, shortDescr, localAlias, description, image, groupType_, groupLink_, publicGroupId_) :. accessRow :. (enableNtfs_, sendRcpts, BI favorite, groupPreferences, memberAdmission) :. (createdAt, updatedAt, chatTs, userMemberProfileSentAt) :. preparedGroupRow :. businessRow :. (BI useRelays, relayOwnStatus, uiThemes, currentMembers, publicMemberCount, customData, chatItemTTL, membersRequireAttention, viaGroupLinkUri) :. groupKeysRow :. (gSimplexNameRaw, gpSimplexNameRaw, simplexNameVerifiedAt) :. userMemberRow) =
let membership = (toGroupMember userContactId userMemberRow) {memberChatVRange = vr}
chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts = unBI <$> sendRcpts, favorite}
fullGroupPreferences = mergeGroupPreferences groupPreferences
@@ -752,7 +752,7 @@ toGroupInfo vr userContactId chatTags ((groupId, localDisplayName, displayName,
businessChat = toBusinessChatInfo businessRow
preparedGroup = toPreparedGroup preparedGroupRow
groupSummary = GroupSummary {currentMembers, publicMemberCount}
in GroupInfo {groupId, useRelays = BoolDef useRelays, relayOwnStatus, localDisplayName, groupProfile, localAlias, businessChat, fullGroupPreferences, membership, chatSettings, createdAt, updatedAt, chatTs, userMemberProfileSentAt, preparedGroup, chatTags, chatItemTTL, uiThemes, groupSummary, customData, membersRequireAttention, viaGroupLinkUri, groupKeys, simplexName}
in GroupInfo {groupId, useRelays = BoolDef useRelays, relayOwnStatus, localDisplayName, groupProfile, localAlias, businessChat, fullGroupPreferences, membership, chatSettings, createdAt, updatedAt, chatTs, userMemberProfileSentAt, preparedGroup, chatTags, chatItemTTL, uiThemes, groupSummary, customData, membersRequireAttention, viaGroupLinkUri, groupKeys, simplexName, simplexNameVerifiedAt}
toPreparedGroup :: PreparedGroupRow -> Maybe PreparedGroup
toPreparedGroup = \case
@@ -854,7 +854,7 @@ groupInfoQueryFields =
g.use_relays, g.relay_own_status,
g.ui_themes, g.summary_current_members_count, g.public_member_count, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri,
g.root_priv_key, g.root_pub_key, g.member_priv_key,
g.simplex_name, gp.simplex_name,
g.simplex_name, gp.simplex_name, g.simplex_name_verified_at,
-- GroupMember - membership
mu.group_member_id, mu.group_id, mu.index_in_group, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category,
mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id,
+10 -2
View File
@@ -210,7 +210,12 @@ data Contact = Contact
uiThemes :: Maybe UIThemeEntityOverrides,
chatDeleted :: Bool,
customData :: Maybe CustomData,
simplexName :: Maybe SimplexNameInfo
simplexName :: Maybe SimplexNameInfo,
-- | Timestamp of the most recent successful RSLV verification of the peer's
-- simplex_name claim against this contact's connection link. NULL means the
-- claim is unverified (UI should surface an indicator). Cleared back to NULL
-- whenever simplex_name changes in updateContactProfileWithConflict.
simplexNameVerifiedAt :: Maybe UTCTime
}
deriving (Eq, Show)
@@ -491,7 +496,10 @@ data GroupInfo = GroupInfo
membersRequireAttention :: Int,
viaGroupLinkUri :: Maybe ConnReqContact,
groupKeys :: Maybe GroupKeys,
simplexName :: Maybe SimplexNameInfo
simplexName :: Maybe SimplexNameInfo,
-- | See 'Contact.simplexNameVerifiedAt'. Verified against the channel link
-- stored for the group; cleared by updateGroupProfileWithConflict.
simplexNameVerifiedAt :: Maybe UTCTime
}
deriving (Eq, Show)
+10
View File
@@ -567,6 +567,16 @@ chatEventToView hu ChatConfig {logLevel, showReactions, showReceipts, testView}
where
entityLabel SNCEContact n = "@" <> n
entityLabel SNCEGroup n = "#" <> n
CEvtSimplexNameVerified u _chatRef ni _ts ->
ttyUser u ["simplex name " <> plain (shortNameInfoStr ni) <> " verified"]
CEvtSimplexNameVerifyFailed u _chatRef ni r ->
ttyUser u ["simplex name " <> plain (shortNameInfoStr ni) <> " verification failed: " <> reason r]
where
reason SNVFLinkMismatch = "link mismatch"
reason SNVFNameNotRegistered = "name not registered"
reason (SNVFResolverError e) = "resolver error: " <> sShow e
CEvtSimplexNameUnverified u _chatRef ni ->
ttyUser u ["simplex name " <> plain (shortNameInfoStr ni) <> " is unverified"]
where
ttyUser :: User -> [StyledString] -> [StyledString]
ttyUser user@User {showNtfs, activeUser, viewPwdHash} ss