mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-08-28 14:14:09 +00:00
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:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|]
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user