core: short link connection plans wip; short links implementation comments (#5947)

This commit is contained in:
spaced4ndy
2025-05-27 13:00:52 +00:00
committed by GitHub
parent cbaab06975
commit ace7278190
16 changed files with 291 additions and 140 deletions

View File

@@ -238,6 +238,7 @@ library
Simplex.Chat.Store.SQLite.Migrations.M20250402_short_links
Simplex.Chat.Store.SQLite.Migrations.M20250512_member_admission
Simplex.Chat.Store.SQLite.Migrations.M20250513_group_scope
Simplex.Chat.Store.SQLite.Migrations.M20250526_short_links
other-modules:
Paths_simplex_chat
hs-source-dirs:

View File

@@ -448,7 +448,13 @@ data ChatCommand
| APISetConnectionIncognito Int64 IncognitoEnabled
| APIChangeConnectionUser Int64 UserId -- new user id to switch connection to
| APIConnectPlan UserId AConnectionLink
| APIConnect UserId IncognitoEnabled (Maybe ACreatedConnLink)
| APIPrepareContact UserId ContactShortLinkData ACreatedConnLink
| APIPrepareGroup UserId GroupShortLinkData ACreatedConnLink
| APIChangeContactUser ContactId UserId
| APIChangeGroupUser GroupId UserId
| APIConnectPreparedContact {contactId :: ContactId, msgContent_ :: Maybe MsgContent}
| APIConnectPreparedGroup GroupId
| APIConnect UserId IncognitoEnabled (Maybe ACreatedConnLink) (Maybe MsgContent)
| Connect IncognitoEnabled (Maybe AConnectionLink)
| APIConnectContactViaAddress UserId IncognitoEnabled ContactId
| ConnectSimplex IncognitoEnabled -- UserId (not used in UI)
@@ -961,14 +967,14 @@ data ConnectionPlan
deriving (Show)
data InvitationLinkPlan
= ILPOk
= ILPOk {contactSLinkData_ :: Maybe ContactShortLinkData}
| ILPOwnLink
| ILPConnecting {contact_ :: Maybe Contact}
| ILPKnown {contact :: Contact}
deriving (Show)
data ContactAddressPlan
= CAPOk
= CAPOk {contactSLinkData_ :: Maybe ContactShortLinkData}
| CAPOwnLink
| CAPConnectingConfirmReconnect
| CAPConnectingProhibit {contact :: Contact}
@@ -977,7 +983,7 @@ data ContactAddressPlan
deriving (Show)
data GroupLinkPlan
= GLPOk
= GLPOk {groupSLinkData_ :: Maybe GroupShortLinkData}
| GLPOwnLink {groupInfo :: GroupInfo}
| GLPConnectingConfirmReconnect
| GLPConnectingProhibit {groupInfo_ :: Maybe GroupInfo}
@@ -987,17 +993,17 @@ data GroupLinkPlan
connectionPlanProceed :: ConnectionPlan -> Bool
connectionPlanProceed = \case
CPInvitationLink ilp -> case ilp of
ILPOk -> True
ILPOk _ -> True
ILPOwnLink -> True
_ -> False
CPContactAddress cap -> case cap of
CAPOk -> True
CAPOk _ -> True
CAPOwnLink -> True
CAPConnectingConfirmReconnect -> True
CAPContactViaAddress _ -> True
_ -> False
CPGroupLink glp -> case glp of
GLPOk -> True
GLPOk _ -> True
GLPOwnLink _ -> True
GLPConnectingConfirmReconnect -> True
_ -> False
@@ -1281,6 +1287,7 @@ data ChatErrorType
| CEInvalidConnReq
| CEUnsupportedConnReq
| CEInvalidChatMessage {connection :: Connection, msgMeta :: Maybe MsgMetaJSON, messageData :: Text, message :: String}
| CEConnReqMessageProhibited
| CEContactNotFound {contactName :: ContactName, suspectedMember :: Maybe (GroupInfo, GroupMember)}
| CEContactNotReady {contact :: Contact}
| CEContactNotActive {contact :: Contact}

View File

@@ -1728,7 +1728,43 @@ processChatCommand' vr = \case
pure conn'
APIConnectPlan userId cLink -> withUserId userId $ \user ->
uncurry (CRConnectionPlan user) <$> connectPlan user cLink
APIConnect userId incognito (Just (ACCL SCMInvitation (CCLink cReq@(CRInvitationUri crData e2e) sLnk_))) -> withUserId userId $ \user -> withInvitationLock "connect" (strEncode cReq) . procCmd $ do
-- TODO [short links] prepare entity
-- TODO - UI would call these APIs after Ok connection plans with short link data
-- TODO - Persist ACreatedConnLink to be used for connection later on user action:
-- TODO - `link` to contacts.inv_conn_req_to_connect, contacts.addr_conn_req_to_connect, groups.conn_req_to_connect
-- TODO - prepared "invitation" and "address" contacts have to be differentiated,
-- TODO for example to warn user before deleting "invitation" contact, hence two fields
-- TODO - Alternatively, entity can be prepared without user action during Ok plans
-- TODO to avoid extra user action, then these APIs can be avoided altogether
APIPrepareContact userId contactSLinkData link -> withUserId userId $ \user -> do
ok_
APIPrepareGroup userId groupSLinkData link -> withUserId userId $ \user -> do
ok_
-- TODO [short links] change prepared entity user
-- TODO - UI would call these APIs before APIConnectPrepared... APIs
-- TODO - UI to transition to new user keeping chat opened
APIChangeContactUser contactId newUserId -> withUser $ \user -> do
ok_
APIChangeGroupUser groupId newUserId -> withUser $ \user -> do
ok_
-- TODO [short links] connect to prepared entity
-- TODO - UI would call these APIs from ChatView on user action after entity is prepared
-- TODO - APIs to call APIConnect
-- TODO - or new API for asynchronous connection? keep APIConnect for legacy links?
APIConnectPreparedContact contactId msgContent_ -> withUser $ \user -> do
-- TODO [short links] connect to prepared contact
-- TODO - for "invitation" contact:
-- TODO - optional message to be sent on successful "sender secure"?
-- TODO - call APIConnect, wait for synchronous (successful) response?
-- TODO - or persist message and queue it asynchronously?
-- TODO - rework agent to allow queueing messages for New connections?
-- TODO - for "address" contact:
-- TODO - optional message to be sent in contact request (pass to APIConnect)
ok_
APIConnectPreparedGroup groupId -> withUser $ \user -> do
ok_
APIConnect userId incognito (Just (ACCL SCMInvitation (CCLink cReq@(CRInvitationUri crData e2e) sLnk_))) mc_ -> withUserId userId $ \user -> withInvitationLock "connect" (strEncode cReq) . procCmd $ do
when (isJust mc_) $ throwChatError CEConnReqMessageProhibited
subMode <- chatReadVar subscriptionMode
-- [incognito] generate profile to send
incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing
@@ -1739,6 +1775,9 @@ processChatCommand' vr = \case
Just (agentV, pqSup') -> do
let chatV = agentToChatVersion agentV
dm <- encodeConnInfoPQ pqSup' chatV $ XInfo profileToSend
-- TODO [short links] use short link data on connection:
-- TODO - new connection (Nothing) is only for legacy links
-- TODO - existing contact is new normal (allow existing connection to have contact or change approach)
withFastStore' (\db -> getConnectionEntityByConnReq db vr user cReqs) >>= \case
Nothing -> joinNewConn chatV dm
Just (RcvDirectMsgConnection conn@Connection {connId, connStatus, contactConnInitiated} Nothing)
@@ -1762,10 +1801,10 @@ processChatCommand' vr = \case
( CRInvitationUri crData {crScheme = SSSimplex} e2e,
CRInvitationUri crData {crScheme = simplexChat} e2e
)
APIConnect userId incognito (Just (ACCL SCMContact ccLink)) -> withUserId userId $ \user -> connectViaContact user incognito ccLink
APIConnect _ _ Nothing -> throwChatError CEInvalidConnReq
APIConnect userId incognito (Just (ACCL SCMContact ccLink)) mc_ -> withUserId userId $ \user -> connectViaContact user incognito ccLink mc_
APIConnect _ _ Nothing _ -> throwChatError CEInvalidConnReq
Connect incognito (Just cLink@(ACL m cLink')) -> withUser $ \user -> do
(ccLink, plan) <- connectPlan user cLink `catchChatError` \e -> case cLink' of CLFull cReq -> pure (ACCL m (CCLink cReq Nothing), CPInvitationLink ILPOk); _ -> throwError e
(ccLink, plan) <- connectPlan user cLink `catchChatError` \e -> case cLink' of CLFull cReq -> pure (ACCL m (CCLink cReq Nothing), CPInvitationLink (ILPOk Nothing)); _ -> throwError e
connectWithPlan user incognito ccLink plan
Connect _ Nothing -> throwChatError CEInvalidConnReq
APIConnectContactViaAddress userId incognito contactId -> withUserId userId $ \user -> do
@@ -1774,12 +1813,12 @@ processChatCommand' vr = \case
ccLink <- case contactLink of
Just (CLFull cReq) -> pure $ CCLink cReq Nothing
Just (CLShort sLnk) -> do
cReq <- getShortLinkConnReq user sLnk
(cReq, _cData) <- getShortLinkConnReq user sLnk
pure $ CCLink cReq $ Just sLnk
Nothing -> throwCmdError "no address in contact profile"
connectContactViaAddress user incognito ct ccLink
ConnectSimplex incognito -> withUser $ \user -> do
plan <- contactRequestPlan user adminContactReq `catchChatError` const (pure $ CPContactAddress CAPOk)
plan <- contactRequestPlan user adminContactReq Nothing `catchChatError` const (pure $ CPContactAddress (CAPOk Nothing))
connectWithPlan user incognito (ACCL SCMContact (CCLink adminContactReq Nothing)) plan
DeleteContact cName cdm -> withContactName cName $ \ctId -> APIDeleteChat (ChatRef CTDirect ctId Nothing) cdm
ClearContact cName -> withContactName cName $ \chatId -> APIClearChat $ ChatRef CTDirect chatId Nothing
@@ -1817,6 +1856,7 @@ processChatCommand' vr = \case
(ucl@UserContactLink {connLinkContact = CCLink connFullLink sLnk_}, conn) <-
withFastStore $ \db -> (,) <$> getUserAddress db user <*> getUserAddressConnection db vr user
when (isJust sLnk_) $ throwCmdError "address already has short link"
-- TODO [short links] set ContactShortLinkData
sLnk <- shortenShortLink' =<< withAgent (\a -> setContactShortLink a (aConnId conn) "" Nothing)
case entityId conn of
Just uclId -> do
@@ -2423,6 +2463,7 @@ processChatCommand' vr = \case
pure (gInfo, gLink, conn)
when (isJust sLnk_) $ throwCmdError "group link already has short link"
let crClientData = encodeJSON $ CRDataGroup gLinkId
-- TODO [short links] set GroupShortLinkData
sLnk <- shortenShortLink' =<< toShortGroupLink <$> withAgent (\a -> setContactShortLink a (aConnId conn) "" (Just crClientData))
withFastStore' $ \db -> setUserContactLinkShortLink db uclId sLnk
let groupLink' = CCLink connFullLink (Just sLnk)
@@ -2798,8 +2839,8 @@ processChatCommand' vr = \case
CTGroup -> withFastStore $ \db -> getGroupChatItemIdByText' db user cId msg
CTLocal -> withFastStore $ \db -> getLocalChatItemIdByText' db user cId msg
_ -> throwCmdError "not supported"
connectViaContact :: User -> IncognitoEnabled -> CreatedLinkContact -> CM ChatResponse
connectViaContact user@User {userId} incognito (CCLink cReq@(CRContactUri ConnReqUriData {crClientData}) sLnk) = withInvitationLock "connectViaContact" (strEncode cReq) $ do
connectViaContact :: User -> IncognitoEnabled -> CreatedLinkContact -> Maybe MsgContent -> CM ChatResponse
connectViaContact user@User {userId} incognito (CCLink cReq@(CRContactUri ConnReqUriData {crClientData}) sLnk) mc_ = withInvitationLock "connectViaContact" (strEncode cReq) $ do
let groupLinkId = crClientData >>= decodeJSON >>= \(CRDataGroup gli) -> Just gli
cReqHash = ConnReqUriHash . C.sha256Hash $ strEncode cReq
case groupLinkId of
@@ -2812,7 +2853,8 @@ processChatCommand' vr = \case
xContactId <- maybe randomXContactId pure xContactId_
connect' Nothing cReqHash xContactId False
-- group link
Just gLinkId ->
Just gLinkId -> do
when (isJust mc_) $ throwChatError CEConnReqMessageProhibited
withFastStore' (\db -> getConnReqContactXContactId db vr user cReqHash) >>= \case
(Just _contact, _) -> procCmd $ do
-- allow repeat contact request
@@ -2831,7 +2873,7 @@ processChatCommand' vr = \case
subMode <- chatReadVar subscriptionMode
let sLnk' = serverShortLink <$> sLnk
conn@PendingContactConnection {pccConnId} <- withFastStore' $ \db -> createConnReqConnection db userId connId cReqHash sLnk' xContactId incognitoProfile groupLinkId subMode chatV pqSup
joinContact user pccConnId connId cReq incognitoProfile xContactId inGroup pqSup chatV
joinContact user pccConnId connId cReq incognitoProfile xContactId mc_ inGroup pqSup chatV
pure $ CRSentInvitation user conn incognitoProfile
connectContactViaAddress :: User -> IncognitoEnabled -> Contact -> CreatedLinkContact -> CM ChatResponse
connectContactViaAddress user incognito ct (CCLink cReq shortLink) =
@@ -2844,7 +2886,7 @@ processChatCommand' vr = \case
incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing
subMode <- chatReadVar subscriptionMode
(pccConnId, ct') <- withFastStore $ \db -> createAddressContactConnection db vr user ct connId cReqHash shortLink newXContactId incognitoProfile subMode chatV pqSup
joinContact user pccConnId connId cReq incognitoProfile newXContactId False pqSup chatV
joinContact user pccConnId connId cReq incognitoProfile newXContactId Nothing False pqSup chatV
pure $ CRSentInvitationToContact user ct' incognitoProfile
prepareContact :: User -> ConnReqContact -> PQSupport -> CM (ConnId, VersionChat)
prepareContact user cReq pqSup = do
@@ -2857,10 +2899,10 @@ processChatCommand' vr = \case
let chatV = agentToChatVersion agentV
connId <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True cReq pqSup
pure (connId, chatV)
joinContact :: User -> Int64 -> ConnId -> ConnReqContact -> Maybe Profile -> XContactId -> Bool -> PQSupport -> VersionChat -> CM ()
joinContact user pccConnId connId cReq incognitoProfile xContactId inGroup pqSup chatV = do
joinContact :: User -> Int64 -> ConnId -> ConnReqContact -> Maybe Profile -> XContactId -> Maybe MsgContent -> Bool -> PQSupport -> VersionChat -> CM ()
joinContact user pccConnId connId cReq incognitoProfile xContactId mc_ inGroup pqSup chatV = do
let profileToSend = userProfileToSend user incognitoProfile Nothing inGroup
dm <- encodeConnInfoPQ pqSup chatV (XContact profileToSend $ Just xContactId)
dm <- encodeConnInfoPQ pqSup chatV (XContact profileToSend (Just xContactId) mc_)
subMode <- chatReadVar subscriptionMode
joinPreparedAgentConnection user pccConnId connId cReq dm pqSup subMode
joinPreparedAgentConnection :: User -> Int64 -> ConnId -> ConnectionRequestUri m -> ByteString -> PQSupport -> SubscriptionMode -> CM ()
@@ -3165,35 +3207,44 @@ processChatCommand' vr = \case
processChatCommand $ APISetChatSettings (ChatRef cType chatId Nothing) $ updateSettings chatSettings
connectPlan :: User -> AConnectionLink -> CM (ACreatedConnLink, ConnectionPlan)
connectPlan user (ACL SCMInvitation cLink) = case cLink of
CLFull cReq -> invitationReqAndPlan cReq Nothing
CLFull cReq -> invitationReqAndPlan cReq Nothing Nothing
CLShort l -> do
let l' = serverShortLink l
withFastStore' (\db -> getConnectionEntityViaShortLink db vr user l') >>= \case
Just (cReq, ent) ->
(ACCL SCMInvitation (CCLink cReq (Just l')),) <$> (invitationEntityPlan ent `catchChatError` (pure . CPError))
Nothing -> getShortLinkConnReq user l' >>= (`invitationReqAndPlan` Just l')
Nothing -> do
(cReq, cData) <- getShortLinkConnReq user l'
let contactSLinkData_ = decodeJSON . safeDecodeUtf8 $ linkUserData cData
invitationReqAndPlan cReq (Just l') contactSLinkData_
where
invitationReqAndPlan cReq sLnk_ = do
plan <- inviationRequestPlan user cReq `catchChatError` (pure . CPError)
invitationReqAndPlan cReq sLnk_ contactSLinkData_ = do
plan <- invitationRequestPlan user cReq contactSLinkData_ `catchChatError` (pure . CPError)
pure (ACCL SCMInvitation (CCLink cReq sLnk_), plan)
connectPlan user (ACL SCMContact cLink) = case cLink of
CLFull cReq -> contactReqAndPlan cReq Nothing
CLFull cReq -> do
plan <- contactOrGroupRequestPlan user cReq `catchChatError` (pure . CPError)
pure (ACCL SCMContact $ CCLink cReq Nothing, plan)
CLShort l@(CSLContact _ ct _ _) -> do
let l' = serverShortLink l
case ct of
CCTContact ->
withFastStore' (\db -> getUserContactLinkViaShortLink db user l') >>= \case
Just (UserContactLink (CCLink cReq _) _) -> pure (ACCL SCMContact $ CCLink cReq (Just l'), CPContactAddress CAPOwnLink)
Nothing -> getShortLinkConnReq user l' >>= (`contactReqAndPlan` Just l')
Nothing -> do
(cReq, cData) <- getShortLinkConnReq user l'
let contactSLinkData_ = decodeJSON . safeDecodeUtf8 $ linkUserData cData
plan <- contactRequestPlan user cReq contactSLinkData_
pure (ACCL SCMContact $ CCLink cReq (Just l'), plan)
CCTGroup ->
withFastStore' (\db -> getGroupInfoViaUserShortLink db vr user l') >>= \case
Just (cReq, g) -> pure (ACCL SCMContact $ CCLink cReq (Just l'), CPGroupLink (GLPOwnLink g))
Nothing -> getShortLinkConnReq user l' >>= (`contactReqAndPlan` Just l')
Nothing -> do
(cReq, cData) <- getShortLinkConnReq user l'
let groupSLinkData_ = decodeJSON . safeDecodeUtf8 $ linkUserData cData
plan <- groupJoinRequestPlan user cReq groupSLinkData_
pure (ACCL SCMContact $ CCLink cReq (Just l'), plan)
CCTChannel -> throwCmdError "channel links are not supported in this version"
where
contactReqAndPlan cReq sLnk_ = do
plan <- contactRequestPlan user cReq `catchChatError` (pure . CPError)
pure (ACCL SCMContact $ CCLink cReq sLnk_, plan)
connectWithPlan :: User -> IncognitoEnabled -> ACreatedConnLink -> ConnectionPlan -> CM ChatResponse
connectWithPlan user@User {userId} incognito ccLink plan
| connectionPlanProceed plan = do
@@ -3201,94 +3252,104 @@ processChatCommand' vr = \case
case plan of
CPContactAddress (CAPContactViaAddress Contact {contactId}) ->
processChatCommand $ APIConnectContactViaAddress userId incognito contactId
_ -> processChatCommand $ APIConnect userId incognito (Just ccLink)
_ -> processChatCommand $ APIConnect userId incognito (Just ccLink) Nothing
| otherwise = pure $ CRConnectionPlan user ccLink plan
inviationRequestPlan :: User -> ConnReqInvitation -> CM ConnectionPlan
inviationRequestPlan user cReq = do
withFastStore' (\db -> getConnectionEntityByConnReq db vr user $ cReqSchemas cReq) >>= \case
Nothing -> pure $ CPInvitationLink ILPOk
invitationRequestPlan :: User -> ConnReqInvitation -> Maybe ContactShortLinkData -> CM ConnectionPlan
invitationRequestPlan user cReq contactSLinkData_ = do
withFastStore' (\db -> getConnectionEntityByConnReq db vr user $ invCReqSchemas cReq) >>= \case
Nothing -> pure $ CPInvitationLink (ILPOk contactSLinkData_)
Just ent -> invitationEntityPlan ent
where
cReqSchemas :: ConnReqInvitation -> (ConnReqInvitation, ConnReqInvitation)
cReqSchemas (CRInvitationUri crData e2e) =
invCReqSchemas :: ConnReqInvitation -> (ConnReqInvitation, ConnReqInvitation)
invCReqSchemas (CRInvitationUri crData e2e) =
( CRInvitationUri crData {crScheme = SSSimplex} e2e,
CRInvitationUri crData {crScheme = simplexChat} e2e
)
invitationEntityPlan :: ConnectionEntity -> CM ConnectionPlan
invitationEntityPlan = \case
RcvDirectMsgConnection Connection {connStatus = ConnPrepared} Nothing ->
pure $ CPInvitationLink ILPOk
-- TODO [short links] entity is already found - passing ContactShortLinkData doesn't make sense?
pure $ CPInvitationLink (ILPOk Nothing)
RcvDirectMsgConnection conn ct_ -> do
let Connection {connStatus, contactConnInitiated} = conn
if
| connStatus == ConnNew && contactConnInitiated ->
pure $ CPInvitationLink ILPOwnLink
-- TODO [short links] check status (now present contact may mean scanned, not only connecting)
| not (connReady conn) ->
pure $ CPInvitationLink (ILPConnecting ct_)
| otherwise -> case ct_ of
Just ct -> pure $ CPInvitationLink (ILPKnown ct)
Nothing -> throwChatError $ CEInternalError "ready RcvDirectMsgConnection connection should have associated contact"
_ -> throwCmdError "found connection entity is not RcvDirectMsgConnection"
contactRequestPlan :: User -> ConnReqContact -> CM ConnectionPlan
contactRequestPlan user (CRContactUri crData) = do
contactOrGroupRequestPlan :: User -> ConnReqContact -> CM ConnectionPlan
contactOrGroupRequestPlan user cReq@(CRContactUri crData) = do
let ConnReqUriData {crClientData} = crData
groupLinkId = crClientData >>= decodeJSON >>= \(CRDataGroup gli) -> Just gli
cReqHashes = bimap hash hash cReqSchemas
case groupLinkId of
-- contact address
Nothing -> contactRequestPlan user cReq Nothing
Just _ -> groupJoinRequestPlan user cReq Nothing
contactRequestPlan :: User -> ConnReqContact -> Maybe ContactShortLinkData -> CM ConnectionPlan
contactRequestPlan user (CRContactUri crData) contactSLinkData_ = do
let cReqSchemas = contactCReqSchemas crData
cReqHashes = bimap contactCReqHash contactCReqHash cReqSchemas
withFastStore' (\db -> getUserContactLinkByConnReq db user cReqSchemas) >>= \case
Just _ -> pure $ CPContactAddress CAPOwnLink
Nothing ->
withFastStore' (\db -> getUserContactLinkByConnReq db user cReqSchemas) >>= \case
Just _ -> pure $ CPContactAddress CAPOwnLink
withFastStore' (\db -> getContactConnEntityByConnReqHash db vr user cReqHashes) >>= \case
Nothing ->
withFastStore' (\db -> getContactConnEntityByConnReqHash db vr user cReqHashes) >>= \case
Nothing ->
withFastStore' (\db -> getContactWithoutConnViaAddress db vr user cReqSchemas) >>= \case
Nothing -> pure $ CPContactAddress CAPOk
Just ct -> pure $ CPContactAddress (CAPContactViaAddress ct)
Just (RcvDirectMsgConnection _conn Nothing) -> pure $ CPContactAddress CAPConnectingConfirmReconnect
Just (RcvDirectMsgConnection _ (Just ct))
| not (contactReady ct) && contactActive ct -> pure $ CPContactAddress (CAPConnectingProhibit ct)
| contactDeleted ct -> pure $ CPContactAddress CAPOk
| otherwise -> pure $ CPContactAddress (CAPKnown ct)
Just (RcvGroupMsgConnection _ gInfo _) -> groupPlan gInfo
Just _ -> throwCmdError "found connection entity is not RcvDirectMsgConnection or RcvGroupMsgConnection"
-- group link
Just _ ->
withFastStore' (\db -> getGroupInfoByUserContactLinkConnReq db vr user cReqSchemas) >>= \case
Just g -> pure $ CPGroupLink (GLPOwnLink g)
Nothing -> do
connEnt_ <- withFastStore' $ \db -> getContactConnEntityByConnReqHash db vr user cReqHashes
gInfo_ <- withFastStore' $ \db -> getGroupInfoByGroupLinkHash db vr user cReqHashes
case (gInfo_, connEnt_) of
(Nothing, Nothing) -> pure $ CPGroupLink GLPOk
(Nothing, Just (RcvDirectMsgConnection _conn Nothing)) -> pure $ CPGroupLink GLPConnectingConfirmReconnect
(Nothing, Just (RcvDirectMsgConnection _ (Just ct)))
| not (contactReady ct) && contactActive ct -> pure $ CPGroupLink (GLPConnectingProhibit gInfo_)
| otherwise -> pure $ CPGroupLink GLPOk
(Nothing, Just _) -> throwCmdError "found connection entity is not RcvDirectMsgConnection"
(Just gInfo, _) -> groupPlan gInfo
where
groupPlan gInfo@GroupInfo {membership}
| memberStatus membership == GSMemRejected = pure $ CPGroupLink (GLPKnown gInfo)
| not (memberActive membership) && not (memberRemoved membership) =
pure $ CPGroupLink (GLPConnectingProhibit $ Just gInfo)
| memberActive membership = pure $ CPGroupLink (GLPKnown gInfo)
| otherwise = pure $ CPGroupLink GLPOk
cReqSchemas :: (ConnReqContact, ConnReqContact)
cReqSchemas =
( CRContactUri crData {crScheme = SSSimplex},
CRContactUri crData {crScheme = simplexChat}
)
hash :: ConnReqContact -> ConnReqUriHash
hash = ConnReqUriHash . C.sha256Hash . strEncode
getShortLinkConnReq :: User -> ConnShortLink m -> CM (ConnectionRequestUri m)
withFastStore' (\db -> getContactWithoutConnViaAddress db vr user cReqSchemas) >>= \case
Nothing -> pure $ CPContactAddress (CAPOk contactSLinkData_)
Just ct -> pure $ CPContactAddress (CAPContactViaAddress ct)
Just (RcvDirectMsgConnection _conn Nothing) -> pure $ CPContactAddress CAPConnectingConfirmReconnect
Just (RcvDirectMsgConnection _ (Just ct))
| not (contactReady ct) && contactActive ct -> pure $ CPContactAddress (CAPConnectingProhibit ct)
| contactDeleted ct -> pure $ CPContactAddress (CAPOk contactSLinkData_)
| otherwise -> pure $ CPContactAddress (CAPKnown ct)
-- TODO [short links] RcvGroupMsgConnection branch is deprecated? (old group link protocol?)
Just (RcvGroupMsgConnection _ gInfo _) -> groupPlan gInfo
Just _ -> throwCmdError "found connection entity is not RcvDirectMsgConnection or RcvGroupMsgConnection"
groupJoinRequestPlan :: User -> ConnReqContact -> Maybe GroupShortLinkData -> CM ConnectionPlan
groupJoinRequestPlan user (CRContactUri crData) groupSLinkData_ = do
let cReqSchemas = contactCReqSchemas crData
cReqHashes = bimap contactCReqHash contactCReqHash cReqSchemas
withFastStore' (\db -> getGroupInfoByUserContactLinkConnReq db vr user cReqSchemas) >>= \case
Just g -> pure $ CPGroupLink (GLPOwnLink g)
Nothing -> do
connEnt_ <- withFastStore' $ \db -> getContactConnEntityByConnReqHash db vr user cReqHashes
gInfo_ <- withFastStore' $ \db -> getGroupInfoByGroupLinkHash db vr user cReqHashes
case (gInfo_, connEnt_) of
(Nothing, Nothing) -> pure $ CPGroupLink (GLPOk groupSLinkData_)
-- TODO [short links] RcvDirectMsgConnection branches are deprecated? (old group link protocol?)
(Nothing, Just (RcvDirectMsgConnection _conn Nothing)) -> pure $ CPGroupLink GLPConnectingConfirmReconnect
(Nothing, Just (RcvDirectMsgConnection _ (Just ct)))
| not (contactReady ct) && contactActive ct -> pure $ CPGroupLink (GLPConnectingProhibit gInfo_)
| otherwise -> pure $ CPGroupLink (GLPOk groupSLinkData_)
(Nothing, Just _) -> throwCmdError "found connection entity is not RcvDirectMsgConnection"
(Just gInfo, _) -> groupPlan gInfo
groupPlan :: GroupInfo -> CM ConnectionPlan
groupPlan gInfo@GroupInfo {membership}
| memberStatus membership == GSMemRejected = pure $ CPGroupLink (GLPKnown gInfo)
| not (memberActive membership) && not (memberRemoved membership) =
pure $ CPGroupLink (GLPConnectingProhibit $ Just gInfo)
| memberActive membership = pure $ CPGroupLink (GLPKnown gInfo)
-- TODO [short links] entity is already found - passing GroupShortLinkData doesn't make sense?
| otherwise = pure $ CPGroupLink (GLPOk Nothing)
contactCReqSchemas :: ConnReqUriData -> (ConnReqContact, ConnReqContact)
contactCReqSchemas crData =
( CRContactUri crData {crScheme = SSSimplex},
CRContactUri crData {crScheme = simplexChat}
)
contactCReqHash :: ConnReqContact -> ConnReqUriHash
contactCReqHash = ConnReqUriHash . C.sha256Hash . strEncode
getShortLinkConnReq :: User -> ConnShortLink m -> CM (ConnectionRequestUri m, ConnLinkData m)
getShortLinkConnReq user l = do
l' <- restoreShortLink' l
(cReq, cData) <- withAgent (\a -> getConnShortLink a (aUserId user) l')
case cData of
ContactLinkData {direct} | not direct -> throwChatError CEUnsupportedConnReq
_ -> pure ()
pure cReq
pure (cReq, cData)
-- This function is needed, as UI uses simplex:/ schema in message view, so that the links can be handled without browser,
-- and short links are stored with server hostname schema, so they wouldn't match without it.
serverShortLink :: ConnShortLink m -> ConnShortLink m
@@ -3296,6 +3357,7 @@ processChatCommand' vr = \case
CSLInvitation _ srv lnkId linkKey -> CSLInvitation SLSServer srv lnkId linkKey
CSLContact _ ct srv linkKey -> CSLContact SLSServer ct srv linkKey
restoreShortLink' l = (`restoreShortLink` l) <$> asks (shortLinkPresetServers . config)
-- TODO [short links] pass encoded ContactShortLinkData or GroupShortLinkData
shortLinkUserData short = if short then Just "" else Nothing
shortenShortLink' :: ConnShortLink m -> CM (ConnShortLink m)
shortenShortLink' l = (`shortenShortLink` l) <$> asks (shortLinkPresetServers . config)
@@ -4259,8 +4321,14 @@ chatCommandP =
"/_contacts " *> (APIListContacts <$> A.decimal),
"/contacts" $> ListContacts,
"/_connect plan " *> (APIConnectPlan <$> A.decimal <* A.space <*> strP),
"/_prepare contact" *> (APIPrepareContact <$> A.decimal <* A.space <*> jsonP <* A.space <*> connLinkP),
"/_prepare group" *> (APIPrepareGroup <$> A.decimal <* A.space <*> jsonP <* A.space <*> connLinkP),
"/_set contact user @" *> (APIChangeContactUser <$> A.decimal <* A.space <*> A.decimal),
"/_set group user #" *> (APIChangeGroupUser <$> A.decimal <* A.space <*> A.decimal),
"/_connect contact @" *> (APIConnectPreparedContact <$> A.decimal <*> optional (A.space *> msgContentP)),
"/_connect group $" *> (APIConnectPreparedGroup <$> A.decimal),
"/_connect " *> (APIAddContact <$> A.decimal <*> shortOnOffP <*> incognitoOnOffP),
"/_connect " *> (APIConnect <$> A.decimal <*> incognitoOnOffP <* A.space <*> connLinkP),
"/_connect " *> (APIConnect <$> A.decimal <*> incognitoOnOffP <* A.space <*> connLinkP_ <*> optional (A.space *> msgContentP)),
"/_set incognito :" *> (APISetConnectionIncognito <$> A.decimal <* A.space <*> onOffP),
"/_set conn user :" *> (APIChangeConnectionUser <$> A.decimal <* A.space <*> A.decimal),
("/connect" <|> "/c") *> (AddContact <$> shortP <*> incognitoP),
@@ -4371,8 +4439,11 @@ chatCommandP =
where
choice = A.choice . map (\p -> p <* A.takeWhile (== ' ') <* A.endOfInput)
connLinkP = do
((Just <$> strP) <|> A.takeTill (== ' ') $> Nothing)
>>= mapM (\(ACR m cReq) -> ACCL m . CCLink cReq <$> optional (A.space *> strP))
(ACR m cReq) <- strP
sLink_ <- optional (A.space *> strP)
pure $ ACCL m (CCLink cReq sLink_)
connLinkP_ =
((Just <$> connLinkP) <|> A.takeTill (== ' ') $> Nothing)
shortP = (A.space *> ("short" <|> "s")) $> True <|> pure False
incognitoP = (A.space *> ("incognito" <|> "i")) $> True <|> pure False
shortOnOffP = (A.space *> "short=" *> onOffP) <|> pure False

View File

@@ -1195,8 +1195,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
REQ invId pqSupport _ connInfo -> do
ChatMessage {chatVRange, chatMsgEvent} <- parseChatMessage conn connInfo
case chatMsgEvent of
XContact p xContactId_ -> profileContactRequest invId chatVRange p xContactId_ pqSupport
XInfo p -> profileContactRequest invId chatVRange p Nothing pqSupport
XContact p xContactId_ mc_ -> profileContactRequest invId chatVRange p xContactId_ mc_ pqSupport
XInfo p -> profileContactRequest invId chatVRange p Nothing Nothing pqSupport
-- TODO show/log error, other events in contact request
_ -> pure ()
MERR _ err -> do
@@ -1208,8 +1208,12 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
-- TODO add debugging output
_ -> pure ()
where
profileContactRequest :: InvitationId -> VersionRangeChat -> Profile -> Maybe XContactId -> PQSupport -> CM ()
profileContactRequest invId chatVRange p@Profile {displayName} xContactId_ reqPQSup = do
profileContactRequest :: InvitationId -> VersionRangeChat -> Profile -> Maybe XContactId -> Maybe MsgContent -> PQSupport -> CM ()
profileContactRequest invId chatVRange p@Profile {displayName} xContactId_ mc_ reqPQSup = do
-- TODO [short links] on contact request create contact with message
-- TODO - instead of creating a contact request, create a contact that can be accepted or rejected,
-- TODO and can be opened as a chat to view message
-- TODO - see schema comments on persistence
withStore (\db -> createOrUpdateContactRequest db vr user userContactLinkId invId chatVRange p xContactId_ reqPQSup) >>= \case
CORContact contact -> toView $ CEvtContactRequestAlreadyAccepted user contact
CORGroup gInfo -> toView $ CEvtBusinessRequestAlreadyAccepted user gInfo

View File

@@ -339,7 +339,7 @@ data ChatMsgEvent (e :: MsgEncoding) where
XFileAcptInv :: SharedMsgId -> Maybe ConnReqInvitation -> String -> ChatMsgEvent 'Json
XFileCancel :: SharedMsgId -> ChatMsgEvent 'Json
XInfo :: Profile -> ChatMsgEvent 'Json
XContact :: Profile -> Maybe XContactId -> ChatMsgEvent 'Json
XContact :: Profile -> Maybe XContactId -> Maybe MsgContent -> ChatMsgEvent 'Json
XDirectDel :: ChatMsgEvent 'Json
XGrpInv :: GroupInvitation -> ChatMsgEvent 'Json
XGrpAcpt :: MemberId -> ChatMsgEvent 'Json
@@ -989,7 +989,7 @@ toCMEventTag msg = case msg of
XFileAcptInv {} -> XFileAcptInv_
XFileCancel _ -> XFileCancel_
XInfo _ -> XInfo_
XContact _ _ -> XContact_
XContact {} -> XContact_
XDirectDel -> XDirectDel_
XGrpInv _ -> XGrpInv_
XGrpAcpt _ -> XGrpAcpt_
@@ -1099,7 +1099,7 @@ appJsonToCM AppMessageJson {v, msgId, event, params} = do
XFileAcptInv_ -> XFileAcptInv <$> p "msgId" <*> opt "fileConnReq" <*> p "fileName"
XFileCancel_ -> XFileCancel <$> p "msgId"
XInfo_ -> XInfo <$> p "profile"
XContact_ -> XContact <$> p "profile" <*> opt "contactReqId"
XContact_ -> XContact <$> p "profile" <*> opt "contactReqId" <*> opt "content"
XDirectDel_ -> pure XDirectDel
XGrpInv_ -> XGrpInv <$> p "groupInvitation"
XGrpAcpt_ -> XGrpAcpt <$> p "memberId"
@@ -1163,7 +1163,7 @@ chatToAppMessage ChatMessage {chatVRange, msgId, chatMsgEvent} = case encoding @
XFileAcptInv sharedMsgId fileConnReq fileName -> o $ ("fileConnReq" .=? fileConnReq) ["msgId" .= sharedMsgId, "fileName" .= fileName]
XFileCancel sharedMsgId -> o ["msgId" .= sharedMsgId]
XInfo profile -> o ["profile" .= profile]
XContact profile xContactId -> o $ ("contactReqId" .=? xContactId) ["profile" .= profile]
XContact profile xContactId content -> o $ ("contactReqId" .=? xContactId) $ ("content" .=? content) ["profile" .= profile]
XDirectDel -> JM.empty
XGrpInv groupInv -> o ["groupInvitation" .= groupInv]
XGrpAcpt memId -> o ["memberId" .= memId]

View File

@@ -112,19 +112,20 @@ getConnectionEntity db vr user@User {userId, userContactId} agentConnId = do
[sql|
SELECT
c.contact_profile_id, c.local_display_name, c.via_group, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, c.contact_used, c.contact_status, c.enable_ntfs, c.send_rcpts, c.favorite,
p.preferences, c.user_preferences, c.created_at, c.updated_at, c.chat_ts, c.contact_group_member_id, c.contact_grp_inv_sent, c.ui_themes, c.chat_deleted, c.custom_data, c.chat_item_ttl
p.preferences, c.user_preferences, c.created_at, c.updated_at, c.chat_ts, c.conn_req_to_connect,
c.contact_group_member_id, c.contact_grp_inv_sent, c.ui_themes, c.chat_deleted, c.custom_data, c.chat_item_ttl
FROM contacts c
JOIN contact_profiles p ON c.contact_profile_id = p.contact_profile_id
WHERE c.user_id = ? AND c.contact_id = ? AND c.deleted = 0
|]
(userId, contactId)
toContact' :: Int64 -> Connection -> [ChatTagId] -> ContactRow' -> Contact
toContact' contactId conn chatTags ((profileId, localDisplayName, viaGroup, displayName, fullName, image, contactLink, localAlias, BI contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, BI favorite, preferences, userPreferences, createdAt, updatedAt, chatTs) :. (contactGroupMemberId, BI contactGrpInvSent, uiThemes, BI chatDeleted, customData, chatItemTTL)) =
toContact' contactId conn chatTags ((profileId, localDisplayName, viaGroup, displayName, fullName, image, contactLink, localAlias, BI contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, BI favorite, preferences, userPreferences, createdAt, updatedAt, chatTs) :. (connReqToConnect, contactGroupMemberId, BI contactGrpInvSent, uiThemes, BI chatDeleted, customData, chatItemTTL)) =
let profile = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences, localAlias}
chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts = unBI <$> sendRcpts, favorite}
mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito conn
activeConn = Just conn
in Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent, chatTags, chatItemTTL, uiThemes, chatDeleted, customData}
in Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, connReqToConnect, contactGroupMemberId, contactGrpInvSent, chatTags, chatItemTTL, uiThemes, chatDeleted, customData}
getGroupAndMember_ :: Int64 -> Connection -> ExceptT StoreError IO (GroupInfo, GroupMember)
getGroupAndMember_ groupMemberId c = do
gm <-
@@ -137,7 +138,8 @@ getConnectionEntity db vr user@User {userId, userContactId} agentConnId = do
-- GroupInfo
g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image,
g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_chat, g.business_member_id, g.customer_member_id,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.conn_req_to_connect,
g.business_chat, g.business_member_id, g.customer_member_id,
g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention,
-- GroupInfo {membership}
mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category,

View File

@@ -199,7 +199,8 @@ getContactByConnReqHash db vr user@User {userId} cReqHash = do
SELECT
-- Contact
ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, 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.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl,
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_req_to_connect,
ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl,
-- 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.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.snd_file_id, c.rcv_file_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,
@@ -261,6 +262,7 @@ createDirectContact db user@User {userId} conn@Connection {connId, localAlias} p
createdAt = currentTs,
updatedAt = currentTs,
chatTs = Just currentTs,
connReqToConnect = Nothing,
contactGroupMemberId = Nothing,
contactGrpInvSent = False,
chatTags = [],
@@ -650,7 +652,8 @@ createOrUpdateContactRequest db vr user@User {userId, userContactId} userContact
SELECT
-- Contact
ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, 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.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl,
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_req_to_connect,
ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl,
-- 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.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.snd_file_id, c.rcv_file_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,
@@ -826,6 +829,7 @@ createAcceptedContact db user@User {userId, profile = LocalProfile {preferences}
createdAt,
updatedAt = createdAt,
chatTs = Just createdAt,
connReqToConnect = Nothing,
contactGroupMemberId = Nothing,
contactGrpInvSent = False,
chatTags = [],
@@ -865,7 +869,8 @@ getContact_ db vr user@User {userId} contactId deleted = do
SELECT
-- Contact
ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, 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.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl,
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_req_to_connect,
ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl,
-- 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.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.snd_file_id, c.rcv_file_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,

View File

@@ -283,7 +283,8 @@ getGroupAndMember db User {userId, userContactId} groupMemberId vr = do
-- GroupInfo
g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image,
g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_chat, g.business_member_id, g.customer_member_id,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.conn_req_to_connect,
g.business_chat, g.business_member_id, g.customer_member_id,
g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention,
-- GroupInfo {membership}
mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category,
@@ -364,6 +365,7 @@ createNewGroup db vr gVar user@User {userId} groupProfile incognitoProfile = Exc
updatedAt = currentTs,
chatTs = Just currentTs,
userMemberProfileSentAt = Just currentTs,
connReqToConnect = Nothing,
chatTags = [],
chatItemTTL = Nothing,
uiThemes = Nothing,
@@ -435,6 +437,7 @@ createGroupInvitation db vr user@User {userId} contact@Contact {contactId, activ
updatedAt = currentTs,
chatTs = Just currentTs,
userMemberProfileSentAt = Just currentTs,
connReqToConnect = Nothing,
chatTags = [],
chatItemTTL = Nothing,
uiThemes = Nothing,
@@ -775,7 +778,8 @@ getUserGroupDetails db vr User {userId, userContactId} _contactId_ search_ = do
SELECT
g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image,
g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_chat, g.business_member_id, g.customer_member_id,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.conn_req_to_connect,
g.business_chat, g.business_member_id, g.customer_member_id,
g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention,
mu.group_member_id, g.group_id, 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, pu.display_name, pu.full_name, pu.image, pu.contact_link, pu.local_alias, pu.preferences,
@@ -1634,7 +1638,8 @@ getViaGroupMember db vr User {userId, userContactId} Contact {contactId} = do
-- GroupInfo
g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image,
g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_chat, g.business_member_id, g.customer_member_id,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.conn_req_to_connect,
g.business_chat, g.business_member_id, g.customer_member_id,
g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention,
-- GroupInfo {membership}
mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category,
@@ -2309,7 +2314,7 @@ createMemberContact
quotaErrCounter = 0
}
mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito ctConn
pure Contact {contactId, localDisplayName, profile = memberProfile, activeConn = Just ctConn, viaGroup = Nothing, contactUsed = True, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, contactGroupMemberId = Just groupMemberId, contactGrpInvSent = False, chatTags = [], chatItemTTL = Nothing, uiThemes = Nothing, chatDeleted = False, customData = Nothing}
pure Contact {contactId, localDisplayName, profile = memberProfile, activeConn = Just ctConn, viaGroup = Nothing, contactUsed = True, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, connReqToConnect = Nothing, contactGroupMemberId = Just groupMemberId, contactGrpInvSent = False, chatTags = [], chatItemTTL = Nothing, uiThemes = Nothing, chatDeleted = False, customData = Nothing}
getMemberContact :: DB.Connection -> VersionRangeChat -> User -> ContactId -> ExceptT StoreError IO (GroupInfo, GroupMember, Contact, ConnReqInvitation)
getMemberContact db vr user contactId = do
@@ -2346,7 +2351,7 @@ createMemberContactInvited
contactId <- createContactUpdateMember currentTs userPreferences
ctConn <- createMemberContactConn_ db user connIds gInfo mConn contactId subMode
let mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito ctConn
mCt' = Contact {contactId, localDisplayName = memberLDN, profile = memberProfile, activeConn = Just ctConn, viaGroup = Nothing, contactUsed = True, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, contactGroupMemberId = Nothing, contactGrpInvSent = False, chatTags = [], chatItemTTL = Nothing, uiThemes = Nothing, chatDeleted = False, customData = Nothing}
mCt' = Contact {contactId, localDisplayName = memberLDN, profile = memberProfile, activeConn = Just ctConn, viaGroup = Nothing, contactUsed = True, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, connReqToConnect = Nothing, contactGroupMemberId = Nothing, contactGrpInvSent = False, chatTags = [], chatItemTTL = Nothing, uiThemes = Nothing, chatDeleted = False, customData = Nothing}
m' = m {memberContactId = Just contactId}
pure (mCt', m')
where

View File

@@ -131,6 +131,7 @@ import Simplex.Chat.Store.SQLite.Migrations.M20250130_indexes
import Simplex.Chat.Store.SQLite.Migrations.M20250402_short_links
import Simplex.Chat.Store.SQLite.Migrations.M20250512_member_admission
import Simplex.Chat.Store.SQLite.Migrations.M20250513_group_scope
import Simplex.Chat.Store.SQLite.Migrations.M20250526_short_links
import Simplex.Messaging.Agent.Store.Shared (Migration (..))
schemaMigrations :: [(String, Query, Maybe Query)]
@@ -261,7 +262,8 @@ schemaMigrations =
("20250130_indexes", m20250130_indexes, Just down_m20250130_indexes),
("20250402_short_links", m20250402_short_links, Just down_m20250402_short_links),
("20250512_member_admission", m20250512_member_admission, Just down_m20250512_member_admission),
("20250513_group_scope", m20250513_group_scope, Just down_m20250513_group_scope)
("20250513_group_scope", m20250513_group_scope, Just down_m20250513_group_scope),
("20250526_short_links", m20250526_short_links, Just down_m20250526_short_links)
]
-- | The list of migrations in ascending order by date

View File

@@ -0,0 +1,23 @@
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Chat.Store.SQLite.Migrations.M20250526_short_links where
import Database.SQLite.Simple (Query)
import Database.SQLite.Simple.QQ (sql)
-- TODO [short links] contacts with contact requests
-- TODO - contacts.is_contact_request flag?
-- TODO - link contact_requests and contacts?
m20250526_short_links :: Query
m20250526_short_links =
[sql|
ALTER TABLE contacts ADD COLUMN conn_req_to_connect BLOB;
ALTER TABLE groups ADD COLUMN conn_req_to_connect BLOB;
|]
down_m20250526_short_links :: Query
down_m20250526_short_links =
[sql|
ALTER TABLE contacts DROP COLUMN conn_req_to_connect;
ALTER TABLE groups DROP COLUMN conn_req_to_connect;
|]

View File

@@ -54,7 +54,8 @@ Query:
-- GroupInfo
g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image,
g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_chat, g.business_member_id, g.customer_member_id,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.conn_req_to_connect,
g.business_chat, g.business_member_id, g.customer_member_id,
g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention,
-- GroupInfo {membership}
mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category,
@@ -179,7 +180,8 @@ Query:
SELECT
-- Contact
ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, 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.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl,
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_req_to_connect,
ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl,
-- 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.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.snd_file_id, c.rcv_file_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,
@@ -329,7 +331,8 @@ Plan:
Query:
SELECT
c.contact_profile_id, c.local_display_name, c.via_group, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, c.contact_used, c.contact_status, c.enable_ntfs, c.send_rcpts, c.favorite,
p.preferences, c.user_preferences, c.created_at, c.updated_at, c.chat_ts, c.contact_group_member_id, c.contact_grp_inv_sent, c.ui_themes, c.chat_deleted, c.custom_data, c.chat_item_ttl
p.preferences, c.user_preferences, c.created_at, c.updated_at, c.chat_ts, c.conn_req_to_connect,
c.contact_group_member_id, c.contact_grp_inv_sent, c.ui_themes, c.chat_deleted, c.custom_data, c.chat_item_ttl
FROM contacts c
JOIN contact_profiles p ON c.contact_profile_id = p.contact_profile_id
WHERE c.user_id = ? AND c.contact_id = ? AND c.deleted = 0
@@ -825,7 +828,8 @@ Query:
SELECT
-- Contact
ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, 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.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl,
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_req_to_connect,
ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl,
-- 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.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.snd_file_id, c.rcv_file_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,
@@ -848,7 +852,8 @@ Query:
-- GroupInfo
g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image,
g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_chat, g.business_member_id, g.customer_member_id,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.conn_req_to_connect,
g.business_chat, g.business_member_id, g.customer_member_id,
g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention,
-- GroupInfo {membership}
mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category,
@@ -896,7 +901,8 @@ Query:
SELECT
g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image,
g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_chat, g.business_member_id, g.customer_member_id,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.conn_req_to_connect,
g.business_chat, g.business_member_id, g.customer_member_id,
g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention,
mu.group_member_id, g.group_id, 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, pu.display_name, pu.full_name, pu.image, pu.contact_link, pu.local_alias, pu.preferences,
@@ -1392,7 +1398,8 @@ Query:
SELECT
-- Contact
ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, 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.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl,
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_req_to_connect,
ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl,
-- 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.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.snd_file_id, c.rcv_file_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,
@@ -4571,7 +4578,8 @@ Query:
-- GroupInfo
g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image,
g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_chat, g.business_member_id, g.customer_member_id,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.conn_req_to_connect,
g.business_chat, g.business_member_id, g.customer_member_id,
g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention,
-- GroupMember - membership
mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category,
@@ -4595,7 +4603,8 @@ Query:
-- GroupInfo
g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image,
g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_chat, g.business_member_id, g.customer_member_id,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.conn_req_to_connect,
g.business_chat, g.business_member_id, g.customer_member_id,
g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention,
-- GroupMember - membership
mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category,

View File

@@ -79,6 +79,7 @@ CREATE TABLE contacts(
ui_themes TEXT,
chat_deleted INTEGER NOT NULL DEFAULT 0,
chat_item_ttl INTEGER,
conn_req_to_connect BLOB,
FOREIGN KEY(user_id, local_display_name)
REFERENCES display_names(user_id, local_display_name)
ON DELETE CASCADE
@@ -135,7 +136,8 @@ CREATE TABLE groups(
customer_member_id BLOB NULL,
chat_item_ttl INTEGER,
local_alias TEXT DEFAULT '',
members_require_attention INTEGER NOT NULL DEFAULT 0, -- received
members_require_attention INTEGER NOT NULL DEFAULT 0,
conn_req_to_connect BLOB, -- received
FOREIGN KEY(user_id, local_display_name)
REFERENCES display_names(user_id, local_display_name)
ON DELETE CASCADE

View File

@@ -36,7 +36,7 @@ import Simplex.Chat.Types
import Simplex.Chat.Types.Preferences
import Simplex.Chat.Types.Shared
import Simplex.Chat.Types.UITheme
import Simplex.Messaging.Agent.Protocol (ConnId, ConnShortLink, ConnectionMode (..), CreatedConnLink (..), UserId)
import Simplex.Messaging.Agent.Protocol (AConnectionRequestUri, ConnId, ConnShortLink, ConnectionMode (..), CreatedConnLink (..), UserId)
import Simplex.Messaging.Agent.Store.AgentStore (firstRow, maybeFirstRow)
import Simplex.Messaging.Agent.Store.DB (BoolInt (..))
import qualified Simplex.Messaging.Agent.Store.DB as DB
@@ -417,18 +417,18 @@ deleteUnusedIncognitoProfileById_ db User {userId} profileId =
|]
(userId, profileId, userId, profileId, userId, profileId)
type ContactRow' = (ProfileId, ContactName, Maybe Int64, ContactName, Text, Maybe ImageData, Maybe ConnLinkContact, LocalAlias, BoolInt, ContactStatus) :. (Maybe MsgFilter, Maybe BoolInt, BoolInt, Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime) :. (Maybe GroupMemberId, BoolInt, Maybe UIThemeEntityOverrides, BoolInt, Maybe CustomData, Maybe Int64)
type ContactRow' = (ProfileId, ContactName, Maybe Int64, ContactName, Text, Maybe ImageData, Maybe ConnLinkContact, LocalAlias, BoolInt, ContactStatus) :. (Maybe MsgFilter, Maybe BoolInt, BoolInt, Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime) :. (Maybe AConnectionRequestUri, Maybe GroupMemberId, BoolInt, Maybe UIThemeEntityOverrides, BoolInt, Maybe CustomData, Maybe Int64)
type ContactRow = Only ContactId :. ContactRow'
toContact :: VersionRangeChat -> User -> [ChatTagId] -> ContactRow :. MaybeConnectionRow -> Contact
toContact vr user chatTags ((Only contactId :. (profileId, localDisplayName, viaGroup, displayName, fullName, image, contactLink, localAlias, BI contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, BI favorite, preferences, userPreferences, createdAt, updatedAt, chatTs) :. (contactGroupMemberId, BI contactGrpInvSent, uiThemes, BI chatDeleted, customData, chatItemTTL)) :. connRow) =
toContact vr user chatTags ((Only contactId :. (profileId, localDisplayName, viaGroup, displayName, fullName, image, contactLink, localAlias, BI contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, BI favorite, preferences, userPreferences, createdAt, updatedAt, chatTs) :. (connReqToConnect, contactGroupMemberId, BI contactGrpInvSent, uiThemes, BI chatDeleted, customData, chatItemTTL)) :. connRow) =
let profile = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences, localAlias}
activeConn = toMaybeConnection vr connRow
chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts = unBI <$> sendRcpts, favorite}
incognito = maybe False connIncognito activeConn
mergedPreferences = contactUserPreferences user userPreferences preferences incognito
in Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent, chatTags, chatItemTTL, uiThemes, chatDeleted, customData}
in Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, connReqToConnect, contactGroupMemberId, contactGrpInvSent, chatTags, chatItemTTL, uiThemes, chatDeleted, customData}
getProfileById :: DB.Connection -> UserId -> Int64 -> ExceptT StoreError IO LocalProfile
getProfileById db userId profileId =
@@ -579,18 +579,18 @@ safeDeleteLDN db User {userId} localDisplayName = do
type BusinessChatInfoRow = (Maybe BusinessChatType, Maybe MemberId, Maybe MemberId)
type GroupInfoRow = (Int64, GroupName, GroupName, Text, Text, Maybe Text, Maybe ImageData, Maybe MsgFilter, Maybe BoolInt, BoolInt, Maybe GroupPreferences, Maybe GroupMemberAdmission) :. (UTCTime, UTCTime, Maybe UTCTime, Maybe UTCTime) :. BusinessChatInfoRow :. (Maybe UIThemeEntityOverrides, Maybe CustomData, Maybe Int64, Int) :. GroupMemberRow
type GroupInfoRow = (Int64, GroupName, GroupName, Text, Text, Maybe Text, Maybe ImageData, Maybe MsgFilter, Maybe BoolInt, BoolInt, Maybe GroupPreferences, Maybe GroupMemberAdmission) :. (UTCTime, UTCTime, Maybe UTCTime, Maybe UTCTime, Maybe ConnReqContact) :. BusinessChatInfoRow :. (Maybe UIThemeEntityOverrides, Maybe CustomData, Maybe Int64, Int) :. GroupMemberRow
type GroupMemberRow = (Int64, Int64, MemberId, VersionChat, VersionChat, GroupMemberRole, GroupMemberCategory, GroupMemberStatus, BoolInt, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, ContactName, Maybe ContactId, ProfileId, ProfileId, ContactName, Text, Maybe ImageData, Maybe ConnLinkContact, LocalAlias, Maybe Preferences) :. (UTCTime, UTCTime) :. (Maybe UTCTime, Int64, Int64, Int64, Maybe UTCTime)
toGroupInfo :: VersionRangeChat -> Int64 -> [ChatTagId] -> GroupInfoRow -> GroupInfo
toGroupInfo vr userContactId chatTags ((groupId, localDisplayName, displayName, fullName, localAlias, description, image, enableNtfs_, sendRcpts, BI favorite, groupPreferences, memberAdmission) :. (createdAt, updatedAt, chatTs, userMemberProfileSentAt) :. businessRow :. (uiThemes, customData, chatItemTTL, membersRequireAttention) :. userMemberRow) =
toGroupInfo vr userContactId chatTags ((groupId, localDisplayName, displayName, fullName, localAlias, description, image, enableNtfs_, sendRcpts, BI favorite, groupPreferences, memberAdmission) :. (createdAt, updatedAt, chatTs, userMemberProfileSentAt, connReqToConnect) :. businessRow :. (uiThemes, customData, chatItemTTL, membersRequireAttention) :. userMemberRow) =
let membership = (toGroupMember userContactId userMemberRow) {memberChatVRange = vr}
chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts = unBI <$> sendRcpts, favorite}
fullGroupPreferences = mergeGroupPreferences groupPreferences
groupProfile = GroupProfile {displayName, fullName, description, image, groupPreferences, memberAdmission}
businessChat = toBusinessChatInfo businessRow
in GroupInfo {groupId, localDisplayName, groupProfile, localAlias, businessChat, fullGroupPreferences, membership, chatSettings, createdAt, updatedAt, chatTs, userMemberProfileSentAt, chatTags, chatItemTTL, uiThemes, customData, membersRequireAttention}
in GroupInfo {groupId, localDisplayName, groupProfile, localAlias, businessChat, fullGroupPreferences, membership, chatSettings, createdAt, updatedAt, chatTs, userMemberProfileSentAt, connReqToConnect, chatTags, chatItemTTL, uiThemes, customData, membersRequireAttention}
toGroupMember :: Int64 -> GroupMemberRow -> GroupMember
toGroupMember userContactId ((groupMemberId, groupId, memberId, minVer, maxVer, memberRole, memberCategory, memberStatus, BI showMessages, memberRestriction_) :. (invitedById, invitedByGroupMemberId, localDisplayName, memberContactId, memberContactProfileId, profileId, displayName, fullName, image, contactLink, localAlias, preferences) :. (createdAt, updatedAt) :. (supportChatTs_, supportChatUnread, supportChatMemberAttention, supportChatMentions, supportChatLastMsgFromMemberTs)) =
@@ -623,7 +623,8 @@ groupInfoQuery =
-- GroupInfo
g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image,
g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_chat, g.business_member_id, g.customer_member_id,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.conn_req_to_connect,
g.business_chat, g.business_member_id, g.customer_member_id,
g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention,
-- GroupMember - membership
mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category,

View File

@@ -51,7 +51,7 @@ import Simplex.Chat.Types.UITheme
import Simplex.Chat.Types.Util
import Simplex.FileTransfer.Description (FileDigest)
import Simplex.FileTransfer.Types (RcvFileId, SndFileId)
import Simplex.Messaging.Agent.Protocol (ACorrId, AEventTag (..), AEvtTag (..), ConnId, ConnShortLink, ConnectionLink, ConnectionMode (..), ConnectionRequestUri, CreatedConnLink, InvitationId, SAEntity (..), UserId)
import Simplex.Messaging.Agent.Protocol (AConnectionRequestUri, ACorrId, AEventTag (..), AEvtTag (..), ConnId, ConnShortLink, ConnectionLink, ConnectionMode (..), ConnectionRequestUri, CreatedConnLink, InvitationId, SAEntity (..), UserId)
import Simplex.Messaging.Agent.Store.DB (Binary (..), blobFieldDecoder, fromTextField_)
import Simplex.Messaging.Crypto.File (CryptoFileArgs (..))
import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport, pattern PQEncOff)
@@ -188,6 +188,7 @@ data Contact = Contact
createdAt :: UTCTime,
updatedAt :: UTCTime,
chatTs :: Maybe UTCTime,
connReqToConnect :: Maybe AConnectionRequestUri,
contactGroupMemberId :: Maybe GroupMemberId,
contactGrpInvSent :: Bool,
chatTags :: [ChatTagId],
@@ -418,6 +419,7 @@ data GroupInfo = GroupInfo
updatedAt :: UTCTime,
chatTs :: Maybe UTCTime,
userMemberProfileSentAt :: Maybe UTCTime,
connReqToConnect :: Maybe ConnReqContact,
chatTags :: [ChatTagId],
chatItemTTL :: Maybe Int64,
uiThemes :: Maybe UIThemeEntityOverrides,
@@ -649,6 +651,18 @@ instance ToField ImageData where toField (ImageData t) = toField t
deriving newtype instance FromField ImageData
-- TODO [short links] StrEncoding instances?
data ContactShortLinkData = ContactShortLinkData
{ profile :: Profile,
welcomeMessage :: Maybe Text
}
deriving (Show)
data GroupShortLinkData = GroupShortLinkData
{ groupProfile :: GroupProfile
}
deriving (Show)
data CReqClientData = CRDataGroup {groupLinkId :: GroupLinkId}
newtype GroupLinkId = GroupLinkId {unGroupLinkId :: ByteString} -- used to identify invitation via group link
@@ -1916,6 +1930,10 @@ instance FromField MsgFilter where fromField = fromIntField_ msgFilterIntP
instance ToField MsgFilter where toField = toField . msgFilterInt
$(JQ.deriveJSON defaultJSON ''ContactShortLinkData)
$(JQ.deriveJSON defaultJSON ''GroupShortLinkData)
$(JQ.deriveJSON defaultJSON ''CReqClientData)
$(JQ.deriveJSON defaultJSON ''MemberIdRole)

View File

@@ -1818,7 +1818,7 @@ viewConnectionUserChanged User {localDisplayName = n} PendingContactConnection {
viewConnectionPlan :: ChatConfig -> ConnectionPlan -> [StyledString]
viewConnectionPlan ChatConfig {logLevel, testView} = \case
CPInvitationLink ilp -> case ilp of
ILPOk -> [invLink "ok to connect"]
ILPOk _contactSLinkData -> [invLink "ok to connect"]
ILPOwnLink -> [invLink "own link"]
ILPConnecting Nothing -> [invLink "connecting"]
ILPConnecting (Just ct) -> [invLink ("connecting to contact " <> ttyContact' ct)]
@@ -1829,7 +1829,7 @@ viewConnectionPlan ChatConfig {logLevel, testView} = \case
where
invLink = ("invitation link: " <>)
CPContactAddress cap -> case cap of
CAPOk -> [ctAddr "ok to connect"]
CAPOk _contactSLinkData -> [ctAddr "ok to connect"]
CAPOwnLink -> [ctAddr "own address"]
CAPConnectingConfirmReconnect -> [ctAddr "connecting, allowed to reconnect"]
CAPConnectingProhibit ct -> [ctAddr ("connecting to contact " <> ttyContact' ct)]
@@ -1841,7 +1841,7 @@ viewConnectionPlan ChatConfig {logLevel, testView} = \case
where
ctAddr = ("contact address: " <>)
CPGroupLink glp -> case glp of
GLPOk -> [grpLink "ok to connect"]
GLPOk _groupSLinkData -> [grpLink "ok to connect"]
GLPOwnLink g -> [grpLink "own link for group " <> ttyGroup' g]
GLPConnectingConfirmReconnect -> [grpLink "connecting, allowed to reconnect"]
GLPConnectingProhibit Nothing -> [grpLink "connecting"]
@@ -2286,6 +2286,7 @@ viewChatError isCmd logLevel testView = \case
<> (", connection id: " <> show connId)
<> maybe "" (\MsgMetaJSON {rcvId} -> ", agent msg rcv id: " <> show rcvId) msgMeta_
]
CEConnReqMessageProhibited -> ["message is not allowed with this connection link"]
CEContactNotFound cName m_ -> viewContactNotFound cName m_
CEContactNotReady c -> [ttyContact' c <> ": not ready"]
CEContactDisabled ct -> [ttyContact' ct <> ": disabled, to enable: " <> highlight ("/enable " <> viewContactName ct) <> ", to delete: " <> highlight ("/d " <> viewContactName ct)]

View File

@@ -226,16 +226,16 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do
#==# XInfo Profile {displayName = "alice", fullName = "", image = Nothing, contactLink = Nothing, preferences = testChatPreferences}
it "x.contact with xContactId" $
"{\"v\":\"1\",\"event\":\"x.contact\",\"params\":{\"contactReqId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}"
#==# XContact testProfile (Just $ XContactId "\1\2\3\4")
#==# XContact testProfile (Just $ XContactId "\1\2\3\4") Nothing
it "x.contact without XContactId" $
"{\"v\":\"1\",\"event\":\"x.contact\",\"params\":{\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}"
#==# XContact testProfile Nothing
#==# XContact testProfile Nothing Nothing
it "x.contact with content null" $
"{\"v\":\"1\",\"event\":\"x.contact\",\"params\":{\"content\":null,\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}"
==# XContact testProfile Nothing
it "x.contact with content (ignored)" $
==# XContact testProfile Nothing Nothing
it "x.contact with content" $
"{\"v\":\"1\",\"event\":\"x.contact\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}"
==# XContact testProfile Nothing
==# XContact testProfile Nothing (Just MCText {text = "hello"})
it "x.grp.inv" $
"{\"v\":\"1\",\"event\":\"x.grp.inv\",\"params\":{\"groupInvitation\":{\"connRequest\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"invitedMember\":{\"memberRole\":\"member\",\"memberId\":\"BQYHCA==\"},\"groupProfile\":{\"fullName\":\"Team\",\"displayName\":\"team\",\"groupPreferences\":{\"reactions\":{\"enable\":\"on\"},\"voice\":{\"enable\":\"on\"}}},\"fromMember\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\"}}}}"
#==# XGrpInv GroupInvitation {fromMember = MemberIdRole (MemberId "\1\2\3\4") GRAdmin, invitedMember = MemberIdRole (MemberId "\5\6\7\8") GRMember, connRequest = testConnReq, groupProfile = testGroupProfile, business = Nothing, groupLinkId = Nothing, groupSize = Nothing}