From 7a5d4a5a3d0637646b25cc21914582169b3d1f00 Mon Sep 17 00:00:00 2001
From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
Date: Fri, 1 Sep 2023 19:20:07 +0400
Subject: [PATCH 1/6] core: communicate connection chat version range (#2886)
* core: communicate connection chat version range
* encoding
* type
* implementation wip
* contact requests
* tests
* more tests
* refactor
* remove comment
* change encoding
* remove Maybe
---------
Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
---
simplex-chat.cabal | 1 +
src/Simplex/Chat.hs | 220 ++++++++++--------
src/Simplex/Chat/Controller.hs | 4 +-
.../M20230829_connections_chat_vrange.hs | 26 +++
src/Simplex/Chat/Migrations/chat_schema.sql | 4 +
src/Simplex/Chat/Protocol.hs | 40 +++-
src/Simplex/Chat/Store/Connections.hs | 3 +-
src/Simplex/Chat/Store/Direct.hs | 58 +++--
src/Simplex/Chat/Store/Files.hs | 2 +-
src/Simplex/Chat/Store/Groups.hs | 27 ++-
src/Simplex/Chat/Store/Messages.hs | 4 +-
src/Simplex/Chat/Store/Migrations.hs | 4 +-
src/Simplex/Chat/Store/Profiles.hs | 6 +-
src/Simplex/Chat/Store/Shared.hs | 46 ++--
src/Simplex/Chat/Types.hs | 3 +
src/Simplex/Chat/View.hs | 10 +-
tests/ChatTests/Direct.hs | 156 +++++++++++--
tests/ChatTests/Groups.hs | 19 +-
tests/ChatTests/Profiles.hs | 2 +
tests/ChatTests/Utils.hs | 11 +-
tests/ProtocolTests.hs | 121 +++++-----
21 files changed, 524 insertions(+), 243 deletions(-)
create mode 100644 src/Simplex/Chat/Migrations/M20230829_connections_chat_vrange.hs
diff --git a/simplex-chat.cabal b/simplex-chat.cabal
index f26e3432c..c510e7333 100644
--- a/simplex-chat.cabal
+++ b/simplex-chat.cabal
@@ -108,6 +108,7 @@ library
Simplex.Chat.Migrations.M20230705_delivery_receipts
Simplex.Chat.Migrations.M20230721_group_snd_item_statuses
Simplex.Chat.Migrations.M20230814_indexes
+ Simplex.Chat.Migrations.M20230829_connections_chat_vrange
Simplex.Chat.Mobile
Simplex.Chat.Mobile.WebRTC
Simplex.Chat.Options
diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs
index 1fabed45b..1c353f7e8 100644
--- a/src/Simplex/Chat.hs
+++ b/src/Simplex/Chat.hs
@@ -104,6 +104,7 @@ import UnliftIO.Concurrent (forkFinally, forkIO, mkWeakThreadId, threadDelay)
import UnliftIO.Directory
import UnliftIO.IO (hClose, hSeek, hTell, openFile)
import UnliftIO.STM
+import Simplex.Messaging.Version
defaultChatConfig :: ChatConfig
defaultChatConfig =
@@ -113,6 +114,7 @@ defaultChatConfig =
{ tcpPort = undefined, -- agent does not listen to TCP
tbqSize = 1024
},
+ chatVRange = supportedChatVRange,
confirmMigrations = MCConsole,
defaultServers =
DefaultAgentServers
@@ -1290,7 +1292,8 @@ processChatCommand = \case
-- [incognito] generate profile to send
incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing
let profileToSend = userProfileToSend user incognitoProfile Nothing
- connId <- withAgent $ \a -> joinConnection a (aUserId user) True cReq . directMessage $ XInfo profileToSend
+ dm <- directMessage $ XInfo profileToSend
+ connId <- withAgent $ \a -> joinConnection a (aUserId user) True cReq dm
conn <- withStore' $ \db -> createDirectConnection db user connId cReq ConnJoined $ incognitoProfile $> profileToSend
toView $ CRNewContactConnection user conn
pure $ CRSentConfirmation user
@@ -1430,7 +1433,8 @@ processChatCommand = \case
APIJoinGroup groupId -> withUser $ \user@User {userId} -> do
ReceivedGroupInvitation {fromMember, connRequest, groupInfo = g@GroupInfo {membership}} <- withStore $ \db -> getGroupInvitation db user groupId
withChatLock "joinGroup" . procCmd $ do
- agentConnId <- withAgent $ \a -> joinConnection a (aUserId user) True connRequest . directMessage $ XGrpAcpt (memberId (membership :: GroupMember))
+ dm <- directMessage $ XGrpAcpt (memberId (membership :: GroupMember))
+ agentConnId <- withAgent $ \a -> joinConnection a (aUserId user) True connRequest dm
withStore' $ \db -> do
createMemberConnection db userId fromMember agentConnId
updateGroupMemberStatus db userId fromMember GSMemAccepted
@@ -1820,7 +1824,8 @@ processChatCommand = \case
-- [incognito] generate profile to send
incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing
let profileToSend = userProfileToSend user incognitoProfile Nothing
- connId <- withAgent $ \a -> joinConnection a (aUserId user) True cReq $ directMessage (XContact profileToSend $ Just xContactId)
+ dm <- directMessage (XContact profileToSend $ Just xContactId)
+ connId <- withAgent $ \a -> joinConnection a (aUserId user) True cReq dm
let groupLinkId = crClientData >>= decodeJSON >>= \(CRDataGroup gli) -> Just gli
conn <- withStore' $ \db -> createConnReqConnection db userId connId cReqHash xContactId incognitoProfile groupLinkId
toView $ CRNewContactConnection user conn
@@ -2210,7 +2215,7 @@ acceptFileReceive user@User {userId} RcvFileTransfer {fileId, xftpRcvFile, fileI
case (xftpRcvFile, fileConnReq) of
-- direct file protocol
(Nothing, Just connReq) -> do
- connIds <- joinAgentConnectionAsync user True connReq . directMessage $ XFileAcpt fName
+ connIds <- joinAgentConnectionAsync user True connReq =<< directMessage (XFileAcpt fName)
filePath <- getRcvFilePath fileId filePath_ fName True
withStoreCtx (Just "acceptFileReceive, acceptRcvFileTransfer") $ \db -> acceptRcvFileTransfer db user fileId connIds ConnJoined filePath
-- XFTP
@@ -2325,17 +2330,18 @@ getRcvFilePath fileId fPath_ fn keepHandle = case fPath_ of
in ifM (doesFileExist f) (tryCombine $ n + 1) (pure f)
acceptContactRequest :: ChatMonad m => User -> UserContactRequest -> Maybe IncognitoProfile -> m Contact
-acceptContactRequest user UserContactRequest {agentInvitationId = AgentInvId invId, localDisplayName = cName, profileId, profile = cp, userContactLinkId, xContactId} incognitoProfile = do
+acceptContactRequest user UserContactRequest {agentInvitationId = AgentInvId invId, cReqChatVRange, localDisplayName = cName, profileId, profile = cp, userContactLinkId, xContactId} incognitoProfile = do
let profileToSend = profileToSendOnAccept user incognitoProfile
- acId <- withAgent $ \a -> acceptContact a True invId . directMessage $ XInfo profileToSend
- withStore' $ \db -> createAcceptedContact db user acId cName profileId cp userContactLinkId xContactId incognitoProfile
+ dm <- directMessage $ XInfo profileToSend
+ acId <- withAgent $ \a -> acceptContact a True invId dm
+ withStore' $ \db -> createAcceptedContact db user acId cReqChatVRange cName profileId cp userContactLinkId xContactId incognitoProfile
acceptContactRequestAsync :: ChatMonad m => User -> UserContactRequest -> Maybe IncognitoProfile -> m Contact
-acceptContactRequestAsync user UserContactRequest {agentInvitationId = AgentInvId invId, localDisplayName = cName, profileId, profile = p, userContactLinkId, xContactId} incognitoProfile = do
+acceptContactRequestAsync user UserContactRequest {agentInvitationId = AgentInvId invId, cReqChatVRange, localDisplayName = cName, profileId, profile = p, userContactLinkId, xContactId} incognitoProfile = do
let profileToSend = profileToSendOnAccept user incognitoProfile
(cmdId, acId) <- agentAcceptContactAsync user True invId $ XInfo profileToSend
withStore' $ \db -> do
- ct@Contact {activeConn = Connection {connId}} <- createAcceptedContact db user acId cName profileId p userContactLinkId xContactId incognitoProfile
+ ct@Contact {activeConn = Connection {connId}} <- createAcceptedContact db user acId cReqChatVRange cName profileId p userContactLinkId xContactId incognitoProfile
setCommandConnId db user cmdId connId
pure ct
@@ -2825,15 +2831,17 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
-- [incognito] send saved profile
incognitoProfile <- forM customUserProfileId $ \profileId -> withStore (\db -> getProfileById db userId profileId)
let profileToSend = userProfileToSend user (fromLocalProfile <$> incognitoProfile) Nothing
- saveConnInfo conn connInfo
+ conn' <- saveConnInfo conn connInfo
-- [async agent commands] no continuation needed, but command should be asynchronous for stability
- allowAgentConnectionAsync user conn confId $ XInfo profileToSend
- INFO connInfo ->
- saveConnInfo conn connInfo
+ allowAgentConnectionAsync user conn' confId $ XInfo profileToSend
+ INFO connInfo -> do
+ _conn' <- saveConnInfo conn connInfo
+ pure ()
MSG meta _msgFlags msgBody -> do
cmdId <- createAckCmd conn
- withAckMessage agentConnId cmdId meta $
- saveRcvMSG conn (ConnectionId connId) meta msgBody cmdId $> False
+ withAckMessage agentConnId cmdId meta $ do
+ (_conn', _) <- saveRcvMSG conn (ConnectionId connId) meta msgBody cmdId
+ pure False
SENT msgId ->
sentMsgDeliveryEvent conn msgId
OK ->
@@ -2863,49 +2871,52 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
MSG msgMeta _msgFlags msgBody -> do
cmdId <- createAckCmd conn
withAckMessage agentConnId cmdId msgMeta $ do
- msg@RcvMessage {chatMsgEvent = ACME _ event} <- saveRcvMSG conn (ConnectionId connId) msgMeta msgBody cmdId
- assertDirectAllowed user MDRcv ct $ toCMEventTag event
+ (conn', msg@RcvMessage {chatMsgEvent = ACME _ event}) <- saveRcvMSG conn (ConnectionId connId) msgMeta msgBody cmdId
+ let ct' = ct {activeConn = conn'} :: Contact
+ assertDirectAllowed user MDRcv ct' $ toCMEventTag event
updateChatLock "directMessage" event
case event of
- XMsgNew mc -> newContentMessage ct mc msg msgMeta
- XMsgFileDescr sharedMsgId fileDescr -> messageFileDescription ct sharedMsgId fileDescr msgMeta
- XMsgFileCancel sharedMsgId -> cancelMessageFile ct sharedMsgId msgMeta
- XMsgUpdate sharedMsgId mContent ttl live -> messageUpdate ct sharedMsgId mContent msg msgMeta ttl live
- XMsgDel sharedMsgId _ -> messageDelete ct sharedMsgId msg msgMeta
- XMsgReact sharedMsgId _ reaction add -> directMsgReaction ct sharedMsgId reaction add msg msgMeta
+ XMsgNew mc -> newContentMessage ct' mc msg msgMeta
+ XMsgFileDescr sharedMsgId fileDescr -> messageFileDescription ct' sharedMsgId fileDescr msgMeta
+ XMsgFileCancel sharedMsgId -> cancelMessageFile ct' sharedMsgId msgMeta
+ XMsgUpdate sharedMsgId mContent ttl live -> messageUpdate ct' sharedMsgId mContent msg msgMeta ttl live
+ XMsgDel sharedMsgId _ -> messageDelete ct' sharedMsgId msg msgMeta
+ XMsgReact sharedMsgId _ reaction add -> directMsgReaction ct' sharedMsgId reaction add msg msgMeta
-- TODO discontinue XFile
- XFile fInv -> processFileInvitation' ct fInv msg msgMeta
- XFileCancel sharedMsgId -> xFileCancel ct sharedMsgId msgMeta
- XFileAcptInv sharedMsgId fileConnReq_ fName -> xFileAcptInv ct sharedMsgId fileConnReq_ fName msgMeta
- XInfo p -> xInfo ct p
- XGrpInv gInv -> processGroupInvitation ct gInv msg msgMeta
- XInfoProbe probe -> xInfoProbe ct probe
- XInfoProbeCheck probeHash -> xInfoProbeCheck ct probeHash
- XInfoProbeOk probe -> xInfoProbeOk ct probe
- XCallInv callId invitation -> xCallInv ct callId invitation msg msgMeta
- XCallOffer callId offer -> xCallOffer ct callId offer msg msgMeta
- XCallAnswer callId answer -> xCallAnswer ct callId answer msg msgMeta
- XCallExtra callId extraInfo -> xCallExtra ct callId extraInfo msg msgMeta
- XCallEnd callId -> xCallEnd ct callId msg msgMeta
- BFileChunk sharedMsgId chunk -> bFileChunk ct sharedMsgId chunk msgMeta
+ XFile fInv -> processFileInvitation' ct' fInv msg msgMeta
+ XFileCancel sharedMsgId -> xFileCancel ct' sharedMsgId msgMeta
+ XFileAcptInv sharedMsgId fileConnReq_ fName -> xFileAcptInv ct' sharedMsgId fileConnReq_ fName msgMeta
+ XInfo p -> xInfo ct' p
+ XGrpInv gInv -> processGroupInvitation ct' gInv msg msgMeta
+ XInfoProbe probe -> xInfoProbe ct' probe
+ XInfoProbeCheck probeHash -> xInfoProbeCheck ct' probeHash
+ XInfoProbeOk probe -> xInfoProbeOk ct' probe
+ XCallInv callId invitation -> xCallInv ct' callId invitation msg msgMeta
+ XCallOffer callId offer -> xCallOffer ct' callId offer msg msgMeta
+ XCallAnswer callId answer -> xCallAnswer ct' callId answer msg msgMeta
+ XCallExtra callId extraInfo -> xCallExtra ct' callId extraInfo msg msgMeta
+ XCallEnd callId -> xCallEnd ct' callId msg msgMeta
+ BFileChunk sharedMsgId chunk -> bFileChunk ct' sharedMsgId chunk msgMeta
_ -> messageError $ "unsupported message: " <> T.pack (show event)
- let Contact {chatSettings = ChatSettings {sendRcpts}} = ct
+ let Contact {chatSettings = ChatSettings {sendRcpts}} = ct'
pure $ fromMaybe (sendRcptsContacts user) sendRcpts && hasDeliveryReceipt (toCMEventTag event)
RCVD msgMeta msgRcpt ->
withAckMessage' agentConnId conn msgMeta $
directMsgReceived ct conn msgMeta msgRcpt
CONF confId _ connInfo -> do
-- confirming direct connection with a member
- ChatMessage {chatMsgEvent} <- parseChatMessage conn connInfo
+ ChatMessage {chatVRange, chatMsgEvent} <- parseChatMessage conn connInfo
+ conn' <- updateConnChatVRange conn chatVRange
case chatMsgEvent of
XGrpMemInfo _memId _memProfile -> do
-- TODO check member ID
-- TODO update member profile
-- [async agent commands] no continuation needed, but command should be asynchronous for stability
- allowAgentConnectionAsync user conn confId XOk
+ allowAgentConnectionAsync user conn' confId XOk
_ -> messageError "CONF from member must have x.grp.mem.info"
INFO connInfo -> do
- ChatMessage {chatMsgEvent} <- parseChatMessage conn connInfo
+ ChatMessage {chatVRange, chatMsgEvent} <- parseChatMessage conn connInfo
+ _conn' <- updateConnChatVRange conn chatVRange
case chatMsgEvent of
XGrpMemInfo _memId _memProfile -> do
-- TODO check member ID
@@ -3031,7 +3042,8 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
_ -> throwChatError $ CECommandError "unexpected cmdFunction"
CRContactUri _ -> throwChatError $ CECommandError "unexpected ConnectionRequestUri type"
CONF confId _ connInfo -> do
- ChatMessage {chatMsgEvent} <- parseChatMessage conn connInfo
+ ChatMessage {chatVRange, chatMsgEvent} <- parseChatMessage conn connInfo
+ conn' <- updateConnChatVRange conn chatVRange
case memberCategory m of
GCInviteeMember ->
case chatMsgEvent of
@@ -3039,7 +3051,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
| sameMemberId memId m -> do
withStore $ \db -> liftIO $ updateGroupMemberStatus db userId m GSMemAccepted
-- [async agent commands] no continuation needed, but command should be asynchronous for stability
- allowAgentConnectionAsync user conn confId XOk
+ allowAgentConnectionAsync user conn' confId XOk
| otherwise -> messageError "x.grp.acpt: memberId is different from expected"
_ -> messageError "CONF from invited member must have x.grp.acpt"
_ ->
@@ -3048,11 +3060,12 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
| sameMemberId memId m -> do
-- TODO update member profile
-- [async agent commands] no continuation needed, but command should be asynchronous for stability
- allowAgentConnectionAsync user conn confId $ XGrpMemInfo (memberId (membership :: GroupMember)) (fromLocalProfile $ memberProfile membership)
+ allowAgentConnectionAsync user conn' confId $ XGrpMemInfo (memberId (membership :: GroupMember)) (fromLocalProfile $ memberProfile membership)
| otherwise -> messageError "x.grp.mem.info: memberId is different from expected"
_ -> messageError "CONF from member must have x.grp.mem.info"
INFO connInfo -> do
- ChatMessage {chatMsgEvent} <- parseChatMessage conn connInfo
+ ChatMessage {chatVRange, chatMsgEvent} <- parseChatMessage conn connInfo
+ _conn' <- updateConnChatVRange conn chatVRange
case chatMsgEvent of
XGrpMemInfo memId _memProfile
| sameMemberId memId m -> do
@@ -3110,28 +3123,29 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
MSG msgMeta _msgFlags msgBody -> do
cmdId <- createAckCmd conn
withAckMessage agentConnId cmdId msgMeta $ do
- msg@RcvMessage {chatMsgEvent = ACME _ event} <- saveRcvMSG conn (GroupId groupId) msgMeta msgBody cmdId
+ (conn', msg@RcvMessage {chatMsgEvent = ACME _ event}) <- saveRcvMSG conn (GroupId groupId) msgMeta msgBody cmdId
+ let m' = m {activeConn = Just conn'} :: GroupMember
updateChatLock "groupMessage" event
case event of
- XMsgNew mc -> canSend $ newGroupContentMessage gInfo m mc msg msgMeta
- XMsgFileDescr sharedMsgId fileDescr -> canSend $ groupMessageFileDescription gInfo m sharedMsgId fileDescr msgMeta
- XMsgFileCancel sharedMsgId -> cancelGroupMessageFile gInfo m sharedMsgId msgMeta
- XMsgUpdate sharedMsgId mContent ttl live -> canSend $ groupMessageUpdate gInfo m sharedMsgId mContent msg msgMeta ttl live
- XMsgDel sharedMsgId memberId -> groupMessageDelete gInfo m sharedMsgId memberId msg msgMeta
- XMsgReact sharedMsgId (Just memberId) reaction add -> groupMsgReaction gInfo m sharedMsgId memberId reaction add msg msgMeta
+ XMsgNew mc -> canSend m' $ newGroupContentMessage gInfo m' mc msg msgMeta
+ XMsgFileDescr sharedMsgId fileDescr -> canSend m' $ groupMessageFileDescription gInfo m' sharedMsgId fileDescr msgMeta
+ XMsgFileCancel sharedMsgId -> cancelGroupMessageFile gInfo m' sharedMsgId msgMeta
+ XMsgUpdate sharedMsgId mContent ttl live -> canSend m' $ groupMessageUpdate gInfo m' sharedMsgId mContent msg msgMeta ttl live
+ XMsgDel sharedMsgId memberId -> groupMessageDelete gInfo m' sharedMsgId memberId msg msgMeta
+ XMsgReact sharedMsgId (Just memberId) reaction add -> groupMsgReaction gInfo m' sharedMsgId memberId reaction add msg msgMeta
-- TODO discontinue XFile
- XFile fInv -> processGroupFileInvitation' gInfo m fInv msg msgMeta
- XFileCancel sharedMsgId -> xFileCancelGroup gInfo m sharedMsgId msgMeta
- XFileAcptInv sharedMsgId fileConnReq_ fName -> xFileAcptInvGroup gInfo m sharedMsgId fileConnReq_ fName msgMeta
- XGrpMemNew memInfo -> xGrpMemNew gInfo m memInfo msg msgMeta
- XGrpMemIntro memInfo -> xGrpMemIntro gInfo m memInfo
- XGrpMemInv memId introInv -> xGrpMemInv gInfo m memId introInv
- XGrpMemFwd memInfo introInv -> xGrpMemFwd gInfo m memInfo introInv
- XGrpMemRole memId memRole -> xGrpMemRole gInfo m memId memRole msg msgMeta
- XGrpMemDel memId -> xGrpMemDel gInfo m memId msg msgMeta
- XGrpLeave -> xGrpLeave gInfo m msg msgMeta
- XGrpDel -> xGrpDel gInfo m msg msgMeta
- XGrpInfo p' -> xGrpInfo gInfo m p' msg msgMeta
+ XFile fInv -> processGroupFileInvitation' gInfo m' fInv msg msgMeta
+ XFileCancel sharedMsgId -> xFileCancelGroup gInfo m' sharedMsgId msgMeta
+ XFileAcptInv sharedMsgId fileConnReq_ fName -> xFileAcptInvGroup gInfo m' sharedMsgId fileConnReq_ fName msgMeta
+ XGrpMemNew memInfo -> xGrpMemNew gInfo m' memInfo msg msgMeta
+ XGrpMemIntro memInfo -> xGrpMemIntro gInfo m' memInfo
+ XGrpMemInv memId introInv -> xGrpMemInv gInfo m' memId introInv
+ XGrpMemFwd memInfo introInv -> xGrpMemFwd gInfo m' memInfo introInv
+ XGrpMemRole memId memRole -> xGrpMemRole gInfo m' memId memRole msg msgMeta
+ XGrpMemDel memId -> xGrpMemDel gInfo m' memId msg msgMeta
+ XGrpLeave -> xGrpLeave gInfo m' msg msgMeta
+ XGrpDel -> xGrpDel gInfo m' msg msgMeta
+ XGrpInfo p' -> xGrpInfo gInfo m' p' msg msgMeta
BFileChunk sharedMsgId chunk -> bFileChunkGroup gInfo sharedMsgId chunk msgMeta
_ -> messageError $ "unsupported message: " <> T.pack (show event)
currentMemCount <- withStore' $ \db -> getGroupCurrentMembersCount db user gInfo
@@ -3141,8 +3155,8 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
&& hasDeliveryReceipt (toCMEventTag event)
&& currentMemCount <= smallGroupsRcptsMemLimit
where
- canSend a
- | memberRole (m :: GroupMember) <= GRObserver = messageError "member is not allowed to send messages"
+ canSend mem a
+ | memberRole (mem :: GroupMember) <= GRObserver = messageError "member is not allowed to send messages"
| otherwise = a
RCVD msgMeta msgRcpt ->
withAckMessage' agentConnId conn msgMeta $
@@ -3227,14 +3241,15 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
-- SMP CONF for SndFileConnection happens for direct file protocol
-- when recipient of the file "joins" connection created by the sender
CONF confId _ connInfo -> do
- ChatMessage {chatMsgEvent} <- parseChatMessage conn connInfo
+ ChatMessage {chatVRange, chatMsgEvent} <- parseChatMessage conn connInfo
+ conn' <- updateConnChatVRange conn chatVRange
case chatMsgEvent of
-- TODO save XFileAcpt message
XFileAcpt name
| name == fileName -> do
withStore' $ \db -> updateSndFileStatus db ft FSAccepted
-- [async agent commands] no continuation needed, but command should be asynchronous for stability
- allowAgentConnectionAsync user conn confId XOk
+ allowAgentConnectionAsync user conn' confId XOk
| otherwise -> messageError "x.file.acpt: fileName is different from expected"
_ -> messageError "CONF from file connection must have x.file.acpt"
CON -> do
@@ -3295,9 +3310,10 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
-- when sender of the file "joins" connection created by the recipient
-- (sender doesn't create connections for all group members)
CONF confId _ connInfo -> do
- ChatMessage {chatMsgEvent} <- parseChatMessage conn connInfo
+ ChatMessage {chatVRange, chatMsgEvent} <- parseChatMessage conn connInfo
+ conn' <- updateConnChatVRange conn chatVRange
case chatMsgEvent of
- XOk -> allowAgentConnectionAsync user conn confId XOk -- [async agent commands] no continuation needed, but command should be asynchronous for stability
+ XOk -> allowAgentConnectionAsync user conn' confId XOk -- [async agent commands] no continuation needed, but command should be asynchronous for stability
_ -> pure ()
CON -> startReceivingFile user fileId
MSG meta _ msgBody -> do
@@ -3356,10 +3372,10 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
processUserContactRequest :: ACommand 'Agent e -> ConnectionEntity -> Connection -> UserContact -> m ()
processUserContactRequest agentMsg connEntity conn UserContact {userContactLinkId} = case agentMsg of
REQ invId _ connInfo -> do
- ChatMessage {chatMsgEvent} <- parseChatMessage conn connInfo
+ ChatMessage {chatVRange, chatMsgEvent} <- parseChatMessage conn connInfo
case chatMsgEvent of
- XContact p xContactId_ -> profileContactRequest invId p xContactId_
- XInfo p -> profileContactRequest invId p Nothing
+ XContact p xContactId_ -> profileContactRequest invId chatVRange p xContactId_
+ XInfo p -> profileContactRequest invId chatVRange p Nothing
-- TODO show/log error, other events in contact request
_ -> pure ()
MERR _ err -> do
@@ -3371,9 +3387,9 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
-- TODO add debugging output
_ -> pure ()
where
- profileContactRequest :: InvitationId -> Profile -> Maybe XContactId -> m ()
- profileContactRequest invId p xContactId_ = do
- withStore (\db -> createOrUpdateContactRequest db user userContactLinkId invId p xContactId_) >>= \case
+ profileContactRequest :: InvitationId -> VersionRange -> Profile -> Maybe XContactId -> m ()
+ profileContactRequest invId chatVRange p xContactId_ = do
+ withStore (\db -> createOrUpdateContactRequest db user userContactLinkId invId chatVRange p xContactId_) >>= \case
CORContact contact -> toView $ CRContactRequestAlreadyAccepted user contact
CORRequest cReq@UserContactRequest {localDisplayName} -> do
withStore' (\db -> getUserContactLinkById db userId userContactLinkId) >>= \case
@@ -3870,7 +3886,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
then unless cancelled $ case fileConnReq_ of
-- receiving via a separate connection
Just fileConnReq -> do
- connIds <- joinAgentConnectionAsync user True fileConnReq $ directMessage XOk
+ connIds <- joinAgentConnectionAsync user True fileConnReq =<< directMessage XOk
withStore' $ \db -> createSndDirectFTConnection db user fileId connIds
-- receiving inline
_ -> do
@@ -3967,7 +3983,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
(Just fileConnReq, _) -> do
-- receiving via a separate connection
-- [async agent commands] no continuation needed, but command should be asynchronous for stability
- connIds <- joinAgentConnectionAsync user True fileConnReq $ directMessage XOk
+ connIds <- joinAgentConnectionAsync user True fileConnReq =<< directMessage XOk
withStore' $ \db -> createSndGroupFileTransferConnection db user fileId connIds m
(_, Just conn) -> do
-- receiving inline
@@ -3999,7 +4015,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
(gInfo@GroupInfo {groupId, localDisplayName, groupProfile, membership = membership@GroupMember {groupMemberId, memberId}}, hostId) <- withStore $ \db -> createGroupInvitation db user ct inv customUserProfileId
if sameGroupLinkId groupLinkId groupLinkId'
then do
- connIds <- joinAgentConnectionAsync user True connRequest . directMessage $ XGrpAcpt memberId
+ connIds <- joinAgentConnectionAsync user True connRequest =<< directMessage (XGrpAcpt memberId)
withStore' $ \db -> do
createMemberConnectionAsync db user hostId connIds
updateGroupMemberStatusById db userId hostId GSMemAccepted
@@ -4201,15 +4217,17 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
withStore' $ \db -> mergeContactRecords db userId c1 c2
toView $ CRContactsMerged user c1 c2
- saveConnInfo :: Connection -> ConnInfo -> m ()
+ saveConnInfo :: Connection -> ConnInfo -> m Connection
saveConnInfo activeConn connInfo = do
- ChatMessage {chatMsgEvent} <- parseChatMessage activeConn connInfo
+ ChatMessage {chatVRange, chatMsgEvent} <- parseChatMessage activeConn connInfo
+ conn' <- updateConnChatVRange activeConn chatVRange
case chatMsgEvent of
XInfo p -> do
- ct <- withStore $ \db -> createDirectContact db user activeConn p
+ ct <- withStore $ \db -> createDirectContact db user conn' p
toView $ CRContactConnecting user ct
+ pure conn'
-- TODO show/log error, other events in SMP confirmation
- _ -> pure ()
+ _ -> pure conn'
xGrpMemNew :: GroupInfo -> GroupMember -> MemberInfo -> RcvMessage -> MsgMeta -> m ()
xGrpMemNew gInfo m memInfo@(MemberInfo memId memRole memberProfile) msg msgMeta = do
@@ -4274,10 +4292,10 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
Just m' -> pure m'
withStore' $ \db -> saveMemberInvitation db toMember introInv
-- [incognito] send membership incognito profile, create direct connection as incognito
- let msg = XGrpMemInfo (memberId (membership :: GroupMember)) (fromLocalProfile $ memberProfile membership)
+ dm <- directMessage $ XGrpMemInfo (memberId (membership :: GroupMember)) (fromLocalProfile $ memberProfile membership)
-- [async agent commands] no continuation needed, but commands should be asynchronous for stability
- groupConnIds <- joinAgentConnectionAsync user enableNtfs groupConnReq $ directMessage msg
- directConnIds <- joinAgentConnectionAsync user enableNtfs directConnReq $ directMessage msg
+ groupConnIds <- joinAgentConnectionAsync user enableNtfs groupConnReq dm
+ directConnIds <- joinAgentConnectionAsync user enableNtfs directConnReq dm
let customUserProfileId = if memberIncognito membership then Just (localProfileId $ memberProfile membership) else Nothing
withStore' $ \db -> createIntroToMemberContact db user m toMember groupConnIds directConnIds customUserProfileId
@@ -4419,6 +4437,13 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
toView $ CRChatItemStatusUpdated user (AChatItem SCTGroup SMDSnd (GroupChat gInfo) chatItem)
_ -> pure ()
+updateConnChatVRange :: ChatMonad m => Connection -> VersionRange -> m Connection
+updateConnChatVRange conn@Connection {connId, connChatVRange} msgChatVRange
+ | msgChatVRange /= connChatVRange = do
+ withStore' $ \db -> setConnChatVRange db connId msgChatVRange
+ pure conn {connChatVRange = msgChatVRange}
+ | otherwise = pure conn
+
parseFileDescription :: (ChatMonad m, FilePartyI p) => Text -> m (ValidFileDescription p)
parseFileDescription =
liftEither . first (ChatError . CEInvalidFileDescription) . (strDecode . encodeUtf8)
@@ -4617,12 +4642,15 @@ sendDirectMessage conn chatMsgEvent connOrGroupId = do
createSndMessage :: (MsgEncodingI e, ChatMonad m) => ChatMsgEvent e -> ConnOrGroupId -> m SndMessage
createSndMessage chatMsgEvent connOrGroupId = do
gVar <- asks idsDrg
+ ChatConfig {chatVRange} <- asks config
withStore $ \db -> createNewSndMessage db gVar connOrGroupId $ \sharedMsgId ->
- let msgBody = strEncode ChatMessage {msgId = Just sharedMsgId, chatMsgEvent}
+ let msgBody = strEncode ChatMessage {chatVRange, msgId = Just sharedMsgId, chatMsgEvent}
in NewMessage {chatMsgEvent, msgBody}
-directMessage :: MsgEncodingI e => ChatMsgEvent e -> ByteString
-directMessage chatMsgEvent = strEncode ChatMessage {msgId = Nothing, chatMsgEvent}
+directMessage :: (MsgEncodingI e, ChatMonad m) => ChatMsgEvent e -> m ByteString
+directMessage chatMsgEvent = do
+ ChatConfig {chatVRange} <- asks config
+ pure $ strEncode ChatMessage {chatVRange, msgId = Nothing, chatMsgEvent}
deliverMessage :: ChatMonad m => Connection -> CMEventTag e -> MsgBody -> MessageId -> m Int64
deliverMessage conn@Connection {connId} cmEventTag msgBody msgId = do
@@ -4677,15 +4705,17 @@ sendPendingGroupMessages user GroupMember {groupMemberId, localDisplayName} conn
_ -> throwChatError $ CEGroupMemberIntroNotFound localDisplayName
_ -> pure ()
-saveRcvMSG :: ChatMonad m => Connection -> ConnOrGroupId -> MsgMeta -> MsgBody -> CommandId -> m RcvMessage
+saveRcvMSG :: ChatMonad m => Connection -> ConnOrGroupId -> MsgMeta -> MsgBody -> CommandId -> m (Connection, RcvMessage)
saveRcvMSG conn@Connection {connId} connOrGroupId agentMsgMeta msgBody agentAckCmdId = do
- ACMsg _ ChatMessage {msgId = sharedMsgId_, chatMsgEvent} <- parseAChatMessage conn agentMsgMeta msgBody
+ ACMsg _ ChatMessage {chatVRange, msgId = sharedMsgId_, chatMsgEvent} <- parseAChatMessage conn agentMsgMeta msgBody
+ conn' <- updateConnChatVRange conn chatVRange
let agentMsgId = fst $ recipient agentMsgMeta
newMsg = NewMessage {chatMsgEvent, msgBody}
rcvMsgDelivery = RcvMsgDelivery {connId, agentMsgId, agentMsgMeta, agentAckCmdId}
- withStoreCtx'
+ msg <- withStoreCtx'
(Just $ "createNewMessageAndRcvMsgDelivery, rcvMsgDelivery: " <> show rcvMsgDelivery <> ", sharedMsgId_: " <> show sharedMsgId_ <> ", msgDeliveryStatus: MDSRcvAgent")
$ \db -> createNewMessageAndRcvMsgDelivery db connOrGroupId newMsg sharedMsgId_ rcvMsgDelivery
+ pure (conn', msg)
saveSndChatItem :: ChatMonad m => User -> ChatDirection c 'MDSnd -> SndMessage -> CIContent 'MDSnd -> m (ChatItem c 'MDSnd)
saveSndChatItem user cd msg content = saveSndChatItem' user cd msg content Nothing Nothing Nothing False
@@ -4785,13 +4815,15 @@ joinAgentConnectionAsync user enableNtfs cReqUri cInfo = do
allowAgentConnectionAsync :: (MsgEncodingI e, ChatMonad m) => User -> Connection -> ConfirmationId -> ChatMsgEvent e -> m ()
allowAgentConnectionAsync user conn@Connection {connId} confId msg = do
cmdId <- withStore' $ \db -> createCommand db user (Just connId) CFAllowConn
- withAgent $ \a -> allowConnectionAsync a (aCorrId cmdId) (aConnId conn) confId $ directMessage msg
+ dm <- directMessage msg
+ withAgent $ \a -> allowConnectionAsync a (aCorrId cmdId) (aConnId conn) confId dm
withStore' $ \db -> updateConnectionStatus db conn ConnAccepted
agentAcceptContactAsync :: (MsgEncodingI e, ChatMonad m) => User -> Bool -> InvitationId -> ChatMsgEvent e -> m (CommandId, ConnId)
agentAcceptContactAsync user enableNtfs invId msg = do
cmdId <- withStore' $ \db -> createCommand db user Nothing CFAcceptContact
- connId <- withAgent $ \a -> acceptContactAsync a (aCorrId cmdId) enableNtfs invId $ directMessage msg
+ dm <- directMessage msg
+ connId <- withAgent $ \a -> acceptContactAsync a (aCorrId cmdId) enableNtfs invId dm
pure (cmdId, connId)
deleteAgentConnectionAsync :: ChatMonad m => User -> ConnId -> m ()
diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs
index 615e472f2..b942256c1 100644
--- a/src/Simplex/Chat/Controller.hs
+++ b/src/Simplex/Chat/Controller.hs
@@ -64,6 +64,7 @@ import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..))
import Simplex.Messaging.Transport (simplexMQVersion)
import Simplex.Messaging.Transport.Client (TransportHost)
import Simplex.Messaging.Util (allFinally, catchAllErrors, tryAllErrors)
+import Simplex.Messaging.Version
import System.IO (Handle)
import System.Mem.Weak (Weak)
import UnliftIO.STM
@@ -72,7 +73,7 @@ versionNumber :: String
versionNumber = showVersion SC.version
versionString :: String -> String
-versionString version = "SimpleX Chat v" <> version
+versionString ver = "SimpleX Chat v" <> ver
updateStr :: String
updateStr = "To update run: curl -o- https://raw.githubusercontent.com/simplex-chat/simplex-chat/master/install.sh | bash"
@@ -101,6 +102,7 @@ coreVersionInfo simplexmqCommit =
data ChatConfig = ChatConfig
{ agentConfig :: AgentConfig,
+ chatVRange :: VersionRange,
confirmMigrations :: MigrationConfirmation,
defaultServers :: DefaultAgentServers,
tbqSize :: Natural,
diff --git a/src/Simplex/Chat/Migrations/M20230829_connections_chat_vrange.hs b/src/Simplex/Chat/Migrations/M20230829_connections_chat_vrange.hs
new file mode 100644
index 000000000..b657cc648
--- /dev/null
+++ b/src/Simplex/Chat/Migrations/M20230829_connections_chat_vrange.hs
@@ -0,0 +1,26 @@
+{-# LANGUAGE QuasiQuotes #-}
+
+module Simplex.Chat.Migrations.M20230829_connections_chat_vrange where
+
+import Database.SQLite.Simple (Query)
+import Database.SQLite.Simple.QQ (sql)
+
+m20230829_connections_chat_vrange :: Query
+m20230829_connections_chat_vrange =
+ [sql|
+ALTER TABLE connections ADD COLUMN chat_vrange_min_version INTEGER NOT NULL DEFAULT 1;
+ALTER TABLE connections ADD COLUMN chat_vrange_max_version INTEGER NOT NULL DEFAULT 1;
+
+ALTER TABLE contact_requests ADD COLUMN chat_vrange_min_version INTEGER NOT NULL DEFAULT 1;
+ALTER TABLE contact_requests ADD COLUMN chat_vrange_max_version INTEGER NOT NULL DEFAULT 1;
+|]
+
+down_m20230829_connections_chat_vrange :: Query
+down_m20230829_connections_chat_vrange =
+ [sql|
+ALTER TABLE contact_requests DROP COLUMN chat_vrange_max_version;
+ALTER TABLE contact_requests DROP COLUMN chat_vrange_min_version;
+
+ALTER TABLE connections DROP COLUMN chat_vrange_max_version;
+ALTER TABLE connections DROP COLUMN chat_vrange_min_version;
+|]
diff --git a/src/Simplex/Chat/Migrations/chat_schema.sql b/src/Simplex/Chat/Migrations/chat_schema.sql
index 76b7ba4a1..b41d7efe6 100644
--- a/src/Simplex/Chat/Migrations/chat_schema.sql
+++ b/src/Simplex/Chat/Migrations/chat_schema.sql
@@ -283,6 +283,8 @@ CREATE TABLE connections(
security_code TEXT NULL,
security_code_verified_at TEXT NULL,
auth_err_counter INTEGER DEFAULT 0 CHECK(auth_err_counter NOT NULL),
+ chat_vrange_min_version INTEGER NOT NULL DEFAULT 1,
+ chat_vrange_max_version INTEGER NOT NULL DEFAULT 1,
FOREIGN KEY(snd_file_id, connection_id)
REFERENCES snd_files(file_id, connection_id)
ON DELETE CASCADE
@@ -316,6 +318,8 @@ CREATE TABLE contact_requests(
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE,
updated_at TEXT CHECK(updated_at NOT NULL),
xcontact_id BLOB,
+ chat_vrange_min_version INTEGER NOT NULL DEFAULT 1,
+ chat_vrange_max_version INTEGER NOT NULL DEFAULT 1,
FOREIGN KEY(user_id, local_display_name)
REFERENCES display_names(user_id, local_display_name)
ON UPDATE CASCADE
diff --git a/src/Simplex/Chat/Protocol.hs b/src/Simplex/Chat/Protocol.hs
index 31d1eb573..5c33eb06c 100644
--- a/src/Simplex/Chat/Protocol.hs
+++ b/src/Simplex/Chat/Protocol.hs
@@ -46,6 +46,13 @@ import Simplex.Messaging.Encoding
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (dropPrefix, fromTextField_, fstToLower, parseAll, sumTypeJSON, taggedObjectJSON)
import Simplex.Messaging.Util (eitherToMaybe, safeDecodeUtf8, (<$?>))
+import Simplex.Messaging.Version hiding (version)
+
+currentChatVersion :: Version
+currentChatVersion = 2
+
+supportedChatVRange :: VersionRange
+supportedChatVRange = mkVersionRange 1 currentChatVersion
data ConnectionEntity
= RcvDirectMsgConnection {entityConnection :: Connection, contact :: Maybe Contact}
@@ -100,9 +107,22 @@ data AppMessage (e :: MsgEncoding) where
AMJson :: AppMessageJson -> AppMessage 'Json
AMBinary :: AppMessageBinary -> AppMessage 'Binary
+newtype ChatVersionRange = ChatVersionRange {fromChatVRange :: VersionRange} deriving (Eq, Show)
+
+chatInitialVRange :: VersionRange
+chatInitialVRange = versionToRange 1
+
+instance FromJSON ChatVersionRange where
+ parseJSON v = ChatVersionRange <$> strParseJSON "ChatVersionRange" v
+
+instance ToJSON ChatVersionRange where
+ toJSON (ChatVersionRange vr) = strToJSON vr
+ toEncoding (ChatVersionRange vr) = strToJEncoding vr
+
-- chat message is sent as JSON with these properties
data AppMessageJson = AppMessageJson
- { msgId :: Maybe SharedMsgId,
+ { v :: Maybe ChatVersionRange,
+ msgId :: Maybe SharedMsgId,
event :: Text,
params :: J.Object
}
@@ -161,7 +181,11 @@ instance ToJSON MsgRef where
toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True}
toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True}
-data ChatMessage e = ChatMessage {msgId :: Maybe SharedMsgId, chatMsgEvent :: ChatMsgEvent e}
+data ChatMessage e = ChatMessage
+ { chatVRange :: VersionRange,
+ msgId :: Maybe SharedMsgId,
+ chatMsgEvent :: ChatMsgEvent e
+ }
deriving (Eq, Show)
data AChatMessage = forall e. MsgEncodingI e => ACMsg (SMsgEncoding e) (ChatMessage e)
@@ -724,17 +748,17 @@ appBinaryToCM :: AppMessageBinary -> Either String (ChatMessage 'Binary)
appBinaryToCM AppMessageBinary {msgId, tag, body} = do
eventTag <- strDecode $ B.singleton tag
chatMsgEvent <- parseAll (msg eventTag) body
- pure ChatMessage {msgId, chatMsgEvent}
+ pure ChatMessage {chatVRange = chatInitialVRange, msgId, chatMsgEvent}
where
msg :: CMEventTag 'Binary -> A.Parser (ChatMsgEvent 'Binary)
msg = \case
BFileChunk_ -> BFileChunk <$> (SharedMsgId <$> smpP) <*> (unIFC <$> smpP)
appJsonToCM :: AppMessageJson -> Either String (ChatMessage 'Json)
-appJsonToCM AppMessageJson {msgId, event, params} = do
+appJsonToCM AppMessageJson {v, msgId, event, params} = do
eventTag <- strDecode $ encodeUtf8 event
chatMsgEvent <- msg eventTag
- pure ChatMessage {msgId, chatMsgEvent}
+ pure ChatMessage {chatVRange = maybe chatInitialVRange fromChatVRange v, msgId, chatMsgEvent}
where
p :: FromJSON a => J.Key -> Either String a
p key = JT.parseEither (.: key) params
@@ -784,11 +808,11 @@ appJsonToCM AppMessageJson {msgId, event, params} = do
key .=? value = maybe id ((:) . (key .=)) value
chatToAppMessage :: forall e. MsgEncodingI e => ChatMessage e -> AppMessage e
-chatToAppMessage ChatMessage {msgId, chatMsgEvent} = case encoding @e of
+chatToAppMessage ChatMessage {chatVRange, msgId, chatMsgEvent} = case encoding @e of
SBinary ->
let (binaryMsgId, body) = toBody chatMsgEvent
in AMBinary AppMessageBinary {msgId = binaryMsgId, tag = B.head $ strEncode tag, body}
- SJson -> AMJson AppMessageJson {msgId, event = textEncode tag, params = params chatMsgEvent}
+ SJson -> AMJson AppMessageJson {v = Just $ ChatVersionRange chatVRange, msgId, event = textEncode tag, params = params chatMsgEvent}
where
tag = toCMEventTag chatMsgEvent
o :: [(J.Key, J.Value)] -> J.Object
@@ -804,7 +828,7 @@ chatToAppMessage ChatMessage {msgId, chatMsgEvent} = case encoding @e of
XMsgUpdate msgId' content ttl live -> o $ ("ttl" .=? ttl) $ ("live" .=? live) ["msgId" .= msgId', "content" .= content]
XMsgDel msgId' memberId -> o $ ("memberId" .=? memberId) ["msgId" .= msgId']
XMsgDeleted -> JM.empty
- XMsgReact msgId' memberId reaction add -> o $ ("memberId" .=? memberId) ["msgId" .= msgId', "reaction" .= reaction, "add" .= add]
+ XMsgReact msgId' memberId reaction add -> o $ ("memberId" .=? memberId) ["msgId" .= msgId', "reaction" .= reaction, "add" .= add]
XFile fileInv -> o ["file" .= fileInv]
XFileAcpt fileName -> o ["fileName" .= fileName]
XFileAcptInv sharedMsgId fileConnReq fileName -> o $ ("fileConnReq" .=? fileConnReq) ["msgId" .= sharedMsgId, "fileName" .= fileName]
diff --git a/src/Simplex/Chat/Store/Connections.hs b/src/Simplex/Chat/Store/Connections.hs
index e31598812..a7c8fd6c3 100644
--- a/src/Simplex/Chat/Store/Connections.hs
+++ b/src/Simplex/Chat/Store/Connections.hs
@@ -49,7 +49,8 @@ getConnectionEntity db user@User {userId, userContactId} agentConnId = do
db
[sql|
SELECT connection_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, via_group_link, group_link_id, custom_user_profile_id,
- conn_status, conn_type, local_alias, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at, security_code, security_code_verified_at, auth_err_counter
+ conn_status, conn_type, local_alias, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at, security_code, security_code_verified_at, auth_err_counter,
+ chat_vrange_min_version, chat_vrange_max_version
FROM connections
WHERE user_id = ? AND agent_conn_id = ?
|]
diff --git a/src/Simplex/Chat/Store/Direct.hs b/src/Simplex/Chat/Store/Direct.hs
index de1c5014b..00d3d55c9 100644
--- a/src/Simplex/Chat/Store/Direct.hs
+++ b/src/Simplex/Chat/Store/Direct.hs
@@ -75,6 +75,7 @@ import Simplex.Chat.Types.Preferences
import Simplex.Messaging.Agent.Protocol (ConnId, InvitationId, UserId)
import Simplex.Messaging.Agent.Store.SQLite (firstRow, maybeFirstRow)
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
+import Simplex.Messaging.Version
getPendingContactConnection :: DB.Connection -> UserId -> Int64 -> ExceptT StoreError IO PendingContactConnection
getPendingContactConnection db userId connId = do
@@ -143,7 +144,8 @@ getConnReqContactXContactId db user@User {userId} cReqHash = do
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts,
-- 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.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.auth_err_counter
+ 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.auth_err_counter,
+ c.chat_vrange_min_version, c.chat_vrange_max_version
FROM contacts ct
JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id
JOIN connections c ON c.contact_id = ct.contact_id
@@ -411,8 +413,8 @@ getUserContacts db user@User {userId} = do
contactIds <- map fromOnly <$> DB.query db "SELECT contact_id FROM contacts WHERE user_id = ? AND deleted = 0" (Only userId)
rights <$> mapM (runExceptT . getContact db user) contactIds
-createOrUpdateContactRequest :: DB.Connection -> User -> Int64 -> InvitationId -> Profile -> Maybe XContactId -> ExceptT StoreError IO ContactOrRequest
-createOrUpdateContactRequest db user@User {userId} userContactLinkId invId Profile {displayName, fullName, image, contactLink, preferences} xContactId_ =
+createOrUpdateContactRequest :: DB.Connection -> User -> Int64 -> InvitationId -> VersionRange -> Profile -> Maybe XContactId -> ExceptT StoreError IO ContactOrRequest
+createOrUpdateContactRequest db user@User {userId} userContactLinkId invId (VersionRange minV maxV) Profile {displayName, fullName, image, contactLink, preferences} xContactId_ =
liftIO (maybeM getContact' xContactId_) >>= \case
Just contact -> pure $ CORContact contact
Nothing -> CORRequest <$> createOrUpdate_
@@ -441,10 +443,10 @@ createOrUpdateContactRequest db user@User {userId} userContactLinkId invId Profi
db
[sql|
INSERT INTO contact_requests
- (user_contact_link_id, agent_invitation_id, contact_profile_id, local_display_name, user_id, created_at, updated_at, xcontact_id)
- VALUES (?,?,?,?,?,?,?,?)
+ (user_contact_link_id, agent_invitation_id, chat_vrange_min_version, chat_vrange_max_version, contact_profile_id, local_display_name, user_id, created_at, updated_at, xcontact_id)
+ VALUES (?,?,?,?,?,?,?,?,?,?)
|]
- (userContactLinkId, invId, profileId, ldn, userId, currentTs, currentTs, xContactId_)
+ (userContactLinkId, invId, minV, maxV, profileId, ldn, userId, currentTs, currentTs, xContactId_)
insertedRowId db
getContact' :: XContactId -> IO (Maybe Contact)
getContact' xContactId =
@@ -458,7 +460,8 @@ createOrUpdateContactRequest db user@User {userId} userContactLinkId invId Profi
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts,
-- 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.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.auth_err_counter
+ 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.auth_err_counter,
+ c.chat_vrange_min_version, c.chat_vrange_max_version
FROM contacts ct
JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id
LEFT JOIN connections c ON c.contact_id = ct.contact_id
@@ -475,7 +478,8 @@ createOrUpdateContactRequest db user@User {userId} userContactLinkId invId Profi
[sql|
SELECT
cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.user_contact_link_id,
- c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, p.preferences, cr.created_at, cr.updated_at
+ c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, p.preferences, cr.created_at, cr.updated_at,
+ cr.chat_vrange_min_version, cr.chat_vrange_max_version
FROM contact_requests cr
JOIN connections c USING (user_contact_link_id)
JOIN contact_profiles p USING (contact_profile_id)
@@ -489,10 +493,26 @@ createOrUpdateContactRequest db user@User {userId} userContactLinkId invId Profi
currentTs <- liftIO getCurrentTime
updateProfile currentTs
if displayName == oldDisplayName
- then Right <$> DB.execute db "UPDATE contact_requests SET agent_invitation_id = ?, updated_at = ? WHERE user_id = ? AND contact_request_id = ?" (invId, currentTs, userId, cReqId)
+ then
+ Right
+ <$> DB.execute
+ db
+ [sql|
+ UPDATE contact_requests
+ SET agent_invitation_id = ?, chat_vrange_min_version = ?, chat_vrange_max_version = ?, updated_at = ?
+ WHERE user_id = ? AND contact_request_id = ?
+ |]
+ (invId, minV, maxV, currentTs, userId, cReqId)
else withLocalDisplayName db userId displayName $ \ldn ->
Right <$> do
- DB.execute db "UPDATE contact_requests SET agent_invitation_id = ?, local_display_name = ?, updated_at = ? WHERE user_id = ? AND contact_request_id = ?" (invId, ldn, currentTs, userId, cReqId)
+ DB.execute
+ db
+ [sql|
+ UPDATE contact_requests
+ SET agent_invitation_id = ?, chat_vrange_min_version = ?, chat_vrange_max_version = ?, local_display_name = ?, updated_at = ?
+ WHERE user_id = ? AND contact_request_id = ?
+ |]
+ (invId, minV, maxV, ldn, currentTs, userId, cReqId)
DB.execute db "DELETE FROM display_names WHERE local_display_name = ? AND user_id = ?" (oldLdn, userId)
where
updateProfile currentTs =
@@ -527,7 +547,8 @@ getContactRequest db User {userId} contactRequestId =
[sql|
SELECT
cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.user_contact_link_id,
- c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, p.preferences, cr.created_at, cr.updated_at
+ c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, p.preferences, cr.created_at, cr.updated_at,
+ cr.chat_vrange_min_version, cr.chat_vrange_max_version
FROM contact_requests cr
JOIN connections c USING (user_contact_link_id)
JOIN contact_profiles p USING (contact_profile_id)
@@ -566,8 +587,8 @@ deleteContactRequest db User {userId} contactRequestId = do
(userId, userId, contactRequestId)
DB.execute db "DELETE FROM contact_requests WHERE user_id = ? AND contact_request_id = ?" (userId, contactRequestId)
-createAcceptedContact :: DB.Connection -> User -> ConnId -> ContactName -> ProfileId -> Profile -> Int64 -> Maybe XContactId -> Maybe IncognitoProfile -> IO Contact
-createAcceptedContact db user@User {userId, profile = LocalProfile {preferences}} agentConnId localDisplayName profileId profile userContactLinkId xContactId incognitoProfile = do
+createAcceptedContact :: DB.Connection -> User -> ConnId -> VersionRange -> ContactName -> ProfileId -> Profile -> Int64 -> Maybe XContactId -> Maybe IncognitoProfile -> IO Contact
+createAcceptedContact db user@User {userId, profile = LocalProfile {preferences}} agentConnId cReqChatVRange localDisplayName profileId profile userContactLinkId xContactId incognitoProfile = do
DB.execute db "DELETE FROM contact_requests WHERE user_id = ? AND local_display_name = ?" (userId, localDisplayName)
createdAt <- getCurrentTime
customUserProfileId <- forM incognitoProfile $ \case
@@ -579,7 +600,7 @@ createAcceptedContact db user@User {userId, profile = LocalProfile {preferences}
"INSERT INTO contacts (user_id, local_display_name, contact_profile_id, enable_ntfs, user_preferences, created_at, updated_at, chat_ts, xcontact_id) VALUES (?,?,?,?,?,?,?,?,?)"
(userId, localDisplayName, profileId, True, userPreferences, createdAt, createdAt, createdAt, xContactId)
contactId <- insertedRowId db
- activeConn <- createConnection_ db userId ConnContact (Just contactId) agentConnId Nothing (Just userContactLinkId) customUserProfileId 0 createdAt
+ activeConn <- createConnection_ db userId ConnContact (Just contactId) agentConnId cReqChatVRange Nothing (Just userContactLinkId) customUserProfileId 0 createdAt
let mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito activeConn
pure $ Contact {contactId, localDisplayName, profile = toLocalProfile profileId profile "", activeConn, viaGroup = Nothing, contactUsed = False, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = createdAt, updatedAt = createdAt, chatTs = Just createdAt}
@@ -603,7 +624,8 @@ getContact_ db user@User {userId} contactId deleted =
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts,
-- 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.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.auth_err_counter
+ 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.auth_err_counter,
+ c.chat_vrange_min_version, c.chat_vrange_max_version
FROM contacts ct
JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id
LEFT JOIN connections c ON c.contact_id = ct.contact_id
@@ -651,7 +673,8 @@ getContactConnections db userId Contact {contactId} =
db
[sql|
SELECT 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.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.auth_err_counter
+ c.conn_status, c.conn_type, 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.auth_err_counter,
+ c.chat_vrange_min_version, c.chat_vrange_max_version
FROM connections c
JOIN contacts ct ON ct.contact_id = c.contact_id
WHERE c.user_id = ? AND ct.user_id = ? AND ct.contact_id = ?
@@ -667,7 +690,8 @@ getConnectionById db User {userId} connId = ExceptT $ do
db
[sql|
SELECT connection_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, via_group_link, group_link_id, custom_user_profile_id,
- conn_status, conn_type, local_alias, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at, security_code, security_code_verified_at, auth_err_counter
+ conn_status, conn_type, local_alias, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at, security_code, security_code_verified_at, auth_err_counter,
+ chat_vrange_min_version, chat_vrange_max_version
FROM connections
WHERE user_id = ? AND connection_id = ?
|]
diff --git a/src/Simplex/Chat/Store/Files.hs b/src/Simplex/Chat/Store/Files.hs
index 249dfedc3..4c370d15e 100644
--- a/src/Simplex/Chat/Store/Files.hs
+++ b/src/Simplex/Chat/Store/Files.hs
@@ -421,7 +421,7 @@ getChatRefByFileId db User {userId} fileId =
createSndFileConnection_ :: DB.Connection -> UserId -> Int64 -> ConnId -> IO Connection
createSndFileConnection_ db userId fileId agentConnId = do
currentTs <- getCurrentTime
- createConnection_ db userId ConnSndFile (Just fileId) agentConnId Nothing Nothing Nothing 0 currentTs
+ createConnection_ db userId ConnSndFile (Just fileId) agentConnId chatInitialVRange Nothing Nothing Nothing 0 currentTs
updateSndFileStatus :: DB.Connection -> SndFileTransfer -> FileStatus -> IO ()
updateSndFileStatus db SndFileTransfer {fileId, connId} status = do
diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs
index 32a3b9110..6c2f32f76 100644
--- a/src/Simplex/Chat/Store/Groups.hs
+++ b/src/Simplex/Chat/Store/Groups.hs
@@ -98,6 +98,7 @@ import Database.SQLite.Simple.QQ (sql)
import Simplex.Chat.Messages
import Simplex.Chat.Store.Direct
import Simplex.Chat.Store.Shared
+import Simplex.Chat.Protocol (chatInitialVRange)
import Simplex.Chat.Types
import Simplex.Chat.Types.Preferences
import Simplex.Messaging.Agent.Protocol (ConnId, UserId)
@@ -142,7 +143,7 @@ createGroupLink db User {userId} groupInfo@GroupInfo {groupId, localDisplayName}
"INSERT INTO user_contact_links (user_id, group_id, group_link_id, local_display_name, conn_req_contact, group_link_member_role, auto_accept, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)"
(userId, groupId, groupLinkId, "group_link_" <> localDisplayName, cReq, memberRole, True, currentTs, currentTs)
userContactLinkId <- insertedRowId db
- void $ createConnection_ db userId ConnUserContact (Just userContactLinkId) agentConnId Nothing Nothing Nothing 0 currentTs
+ void $ createConnection_ db userId ConnUserContact (Just userContactLinkId) agentConnId chatInitialVRange Nothing Nothing Nothing 0 currentTs
getGroupLinkConnection :: DB.Connection -> User -> GroupInfo -> ExceptT StoreError IO Connection
getGroupLinkConnection db User {userId} groupInfo@GroupInfo {groupId} =
@@ -151,7 +152,8 @@ getGroupLinkConnection db User {userId} groupInfo@GroupInfo {groupId} =
db
[sql|
SELECT 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.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.auth_err_counter
+ c.conn_status, c.conn_type, 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.auth_err_counter,
+ c.chat_vrange_min_version, c.chat_vrange_max_version
FROM connections c
JOIN user_contact_links uc ON c.user_contact_link_id = uc.user_contact_link_id
WHERE c.user_id = ? AND uc.user_id = ? AND uc.group_id = ?
@@ -232,7 +234,8 @@ getGroupAndMember db User {userId, userContactId} groupMemberId =
m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, m.member_status,
m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences,
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.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.auth_err_counter
+ c.conn_status, c.conn_type, 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.auth_err_counter,
+ c.chat_vrange_min_version, c.chat_vrange_max_version
FROM group_members m
JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id)
JOIN groups g ON g.group_id = m.group_id
@@ -524,7 +527,8 @@ groupMemberQuery =
m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, m.member_status,
m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences,
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.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.auth_err_counter
+ c.conn_status, c.conn_type, 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.auth_err_counter,
+ c.chat_vrange_min_version, c.chat_vrange_max_version
FROM group_members m
JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id)
LEFT JOIN connections c ON c.connection_id = (
@@ -682,7 +686,8 @@ getContactViaMember db user@User {userId} GroupMember {groupMemberId} =
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts,
-- 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.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.auth_err_counter
+ 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.auth_err_counter,
+ c.chat_vrange_min_version, c.chat_vrange_max_version
FROM contacts ct
JOIN contact_profiles cp ON cp.contact_profile_id = ct.contact_profile_id
JOIN connections c ON c.connection_id = (
@@ -911,7 +916,7 @@ createIntroReMember :: DB.Connection -> User -> GroupInfo -> GroupMember -> Memb
createIntroReMember db user@User {userId} gInfo@GroupInfo {groupId} _host@GroupMember {memberContactId, activeConn} memInfo@(MemberInfo _ _ memberProfile) (groupCmdId, groupAgentConnId) (directCmdId, directAgentConnId) customUserProfileId = do
let cLevel = 1 + maybe 0 (connLevel :: Connection -> Int) activeConn
currentTs <- liftIO getCurrentTime
- Connection {connId = directConnId} <- liftIO $ createConnection_ db userId ConnContact Nothing directAgentConnId memberContactId Nothing customUserProfileId cLevel currentTs
+ Connection {connId = directConnId} <- liftIO $ createConnection_ db userId ConnContact Nothing directAgentConnId chatInitialVRange memberContactId Nothing customUserProfileId cLevel currentTs
liftIO $ setCommandConnId db user directCmdId directConnId
(localDisplayName, contactId, memProfileId) <- createContact_ db userId directConnId memberProfile "" (Just groupId) currentTs Nothing
liftIO $ do
@@ -936,7 +941,7 @@ createIntroToMemberContact db user@User {userId} GroupMember {memberContactId =
currentTs <- getCurrentTime
Connection {connId = groupConnId} <- createMemberConnection_ db userId groupMemberId groupAgentConnId viaContactId cLevel currentTs
setCommandConnId db user groupCmdId groupConnId
- Connection {connId = directConnId} <- createConnection_ db userId ConnContact Nothing directAgentConnId viaContactId Nothing customUserProfileId cLevel currentTs
+ Connection {connId = directConnId} <- createConnection_ db userId ConnContact Nothing directAgentConnId chatInitialVRange viaContactId Nothing customUserProfileId cLevel currentTs
setCommandConnId db user directCmdId directConnId
contactId <- createMemberContact_ directConnId currentTs
updateMember_ contactId currentTs
@@ -967,7 +972,7 @@ createIntroToMemberContact db user@User {userId} GroupMember {memberContactId =
[":contact_id" := contactId, ":updated_at" := ts, ":group_member_id" := groupMemberId]
createMemberConnection_ :: DB.Connection -> UserId -> Int64 -> ConnId -> Maybe Int64 -> Int -> UTCTime -> IO Connection
-createMemberConnection_ db userId groupMemberId agentConnId viaContact = createConnection_ db userId ConnMember (Just groupMemberId) agentConnId viaContact Nothing Nothing
+createMemberConnection_ db userId groupMemberId agentConnId viaContact = createConnection_ db userId ConnMember (Just groupMemberId) agentConnId chatInitialVRange viaContact Nothing Nothing
getViaGroupMember :: DB.Connection -> User -> Contact -> IO (Maybe (GroupInfo, GroupMember))
getViaGroupMember db User {userId, userContactId} Contact {contactId} =
@@ -987,7 +992,8 @@ getViaGroupMember db User {userId, userContactId} Contact {contactId} =
m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, m.member_status,
m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences,
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.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.auth_err_counter
+ c.conn_status, c.conn_type, 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.auth_err_counter,
+ c.chat_vrange_min_version, c.chat_vrange_max_version
FROM group_members m
JOIN contacts ct ON ct.contact_id = m.contact_id
JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id)
@@ -1020,7 +1026,8 @@ getViaGroupContact db user@User {userId} GroupMember {groupMemberId} =
ct.contact_id, ct.contact_profile_id, ct.local_display_name, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, ct.via_group, ct.contact_used, ct.enable_ntfs, ct.send_rcpts, ct.favorite,
p.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts,
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.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.auth_err_counter
+ c.conn_status, c.conn_type, 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.auth_err_counter,
+ c.chat_vrange_min_version, c.chat_vrange_max_version
FROM contacts ct
JOIN contact_profiles p ON ct.contact_profile_id = p.contact_profile_id
JOIN connections c ON c.connection_id = (
diff --git a/src/Simplex/Chat/Store/Messages.hs b/src/Simplex/Chat/Store/Messages.hs
index 7bc2eaf4d..5f760add1 100644
--- a/src/Simplex/Chat/Store/Messages.hs
+++ b/src/Simplex/Chat/Store/Messages.hs
@@ -478,6 +478,7 @@ getDirectChatPreviews_ db user@User {userId} = do
-- 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.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.auth_err_counter,
+ c.chat_vrange_min_version, c.chat_vrange_max_version,
-- ChatStats
COALESCE(ChatStats.UnreadCount, 0), COALESCE(ChatStats.MinUnread, 0), ct.unread_chat,
-- ChatItem
@@ -608,7 +609,8 @@ getContactRequestChatPreviews_ db User {userId} =
[sql|
SELECT
cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.user_contact_link_id,
- c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, p.preferences, cr.created_at, cr.updated_at
+ c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, p.preferences, cr.created_at, cr.updated_at,
+ cr.chat_vrange_min_version, cr.chat_vrange_max_version
FROM contact_requests cr
JOIN connections c ON c.user_contact_link_id = cr.user_contact_link_id
JOIN contact_profiles p ON p.contact_profile_id = cr.contact_profile_id
diff --git a/src/Simplex/Chat/Store/Migrations.hs b/src/Simplex/Chat/Store/Migrations.hs
index 6da0d1cdc..3bd9bf4f7 100644
--- a/src/Simplex/Chat/Store/Migrations.hs
+++ b/src/Simplex/Chat/Store/Migrations.hs
@@ -76,6 +76,7 @@ import Simplex.Chat.Migrations.M20230621_chat_item_moderations
import Simplex.Chat.Migrations.M20230705_delivery_receipts
import Simplex.Chat.Migrations.M20230721_group_snd_item_statuses
import Simplex.Chat.Migrations.M20230814_indexes
+import Simplex.Chat.Migrations.M20230829_connections_chat_vrange
import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..))
schemaMigrations :: [(String, Query, Maybe Query)]
@@ -151,7 +152,8 @@ schemaMigrations =
("20230621_chat_item_moderations", m20230621_chat_item_moderations, Just down_m20230621_chat_item_moderations),
("20230705_delivery_receipts", m20230705_delivery_receipts, Just down_m20230705_delivery_receipts),
("20230721_group_snd_item_statuses", m20230721_group_snd_item_statuses, Just down_m20230721_group_snd_item_statuses),
- ("20230814_indexes", m20230814_indexes, Just down_m20230814_indexes)
+ ("20230814_indexes", m20230814_indexes, Just down_m20230814_indexes),
+ ("20230829_connections_chat_vrange", m20230829_connections_chat_vrange, Just down_m20230829_connections_chat_vrange)
]
-- | The list of migrations in ascending order by date
diff --git a/src/Simplex/Chat/Store/Profiles.hs b/src/Simplex/Chat/Store/Profiles.hs
index 48f2dd144..1d305ffd6 100644
--- a/src/Simplex/Chat/Store/Profiles.hs
+++ b/src/Simplex/Chat/Store/Profiles.hs
@@ -302,7 +302,7 @@ createUserContactLink db User {userId} agentConnId cReq =
"INSERT INTO user_contact_links (user_id, conn_req_contact, created_at, updated_at) VALUES (?,?,?,?)"
(userId, cReq, currentTs, currentTs)
userContactLinkId <- insertedRowId db
- void $ createConnection_ db userId ConnUserContact (Just userContactLinkId) agentConnId Nothing Nothing Nothing 0 currentTs
+ void $ createConnection_ db userId ConnUserContact (Just userContactLinkId) agentConnId chatInitialVRange Nothing Nothing Nothing 0 currentTs
getUserAddressConnections :: DB.Connection -> User -> ExceptT StoreError IO [Connection]
getUserAddressConnections db User {userId} = do
@@ -316,7 +316,8 @@ getUserAddressConnections db User {userId} = do
db
[sql|
SELECT 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.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.auth_err_counter
+ c.conn_status, c.conn_type, 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.auth_err_counter,
+ c.chat_vrange_min_version, c.chat_vrange_max_version
FROM connections c
JOIN user_contact_links uc ON c.user_contact_link_id = uc.user_contact_link_id
WHERE c.user_id = ? AND uc.user_id = ? AND uc.local_display_name = '' AND uc.group_id IS NULL
@@ -331,6 +332,7 @@ getUserContactLinks db User {userId} =
[sql|
SELECT 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.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.auth_err_counter,
+ c.chat_vrange_min_version, c.chat_vrange_max_version,
uc.user_contact_link_id, uc.conn_req_contact, uc.group_id
FROM connections c
JOIN user_contact_links uc ON c.user_contact_link_id = uc.user_contact_link_id
diff --git a/src/Simplex/Chat/Store/Shared.hs b/src/Simplex/Chat/Store/Shared.hs
index c4e6b8d90..5bae79d80 100644
--- a/src/Simplex/Chat/Store/Shared.hs
+++ b/src/Simplex/Chat/Store/Shared.hs
@@ -17,8 +17,8 @@ import Control.Monad.Except
import Crypto.Random (ChaChaDRG, randomBytesGenerate)
import Data.Aeson (ToJSON)
import qualified Data.Aeson as J
-import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Base64 as B64
+import Data.ByteString.Char8 (ByteString)
import Data.Int (Int64)
import Data.Maybe (fromMaybe, isJust, listToMaybe)
import Data.Text (Text)
@@ -37,6 +37,7 @@ import Simplex.Messaging.Agent.Store.SQLite (firstRow, maybeFirstRow)
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON)
import Simplex.Messaging.Util (allFinally)
+import Simplex.Messaging.Version
import UnliftIO.STM
-- These error type constructors must be added to mobile apps
@@ -132,15 +133,16 @@ toFileInfo (fileId, fileStatus, filePath) = CIFileInfo {fileId, fileStatus, file
type EntityIdsRow = (Maybe Int64, Maybe Int64, Maybe Int64, Maybe Int64, Maybe Int64)
-type ConnectionRow = (Int64, ConnId, Int, Maybe Int64, Maybe Int64, Bool, Maybe GroupLinkId, Maybe Int64, ConnStatus, ConnType, LocalAlias) :. EntityIdsRow :. (UTCTime, Maybe Text, Maybe UTCTime, Int)
+type ConnectionRow = (Int64, ConnId, Int, Maybe Int64, Maybe Int64, Bool, Maybe GroupLinkId, Maybe Int64, ConnStatus, ConnType, LocalAlias) :. EntityIdsRow :. (UTCTime, Maybe Text, Maybe UTCTime, Int, Version, Version)
-type MaybeConnectionRow = (Maybe Int64, Maybe ConnId, Maybe Int, Maybe Int64, Maybe Int64, Maybe Bool, Maybe GroupLinkId, Maybe Int64, Maybe ConnStatus, Maybe ConnType, Maybe LocalAlias) :. EntityIdsRow :. (Maybe UTCTime, Maybe Text, Maybe UTCTime, Maybe Int)
+type MaybeConnectionRow = (Maybe Int64, Maybe ConnId, Maybe Int, Maybe Int64, Maybe Int64, Maybe Bool, Maybe GroupLinkId, Maybe Int64, Maybe ConnStatus, Maybe ConnType, Maybe LocalAlias) :. EntityIdsRow :. (Maybe UTCTime, Maybe Text, Maybe UTCTime, Maybe Int, Maybe Version, Maybe Version)
toConnection :: ConnectionRow -> Connection
-toConnection ((connId, acId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (createdAt, code_, verifiedAt_, authErrCounter)) =
+toConnection ((connId, acId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (createdAt, code_, verifiedAt_, authErrCounter, minVer, maxVer)) =
let entityId = entityId_ connType
connectionCode = SecurityCode <$> code_ <*> verifiedAt_
- in Connection {connId, agentConnId = AgentConnId acId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, localAlias, entityId, connectionCode, authErrCounter, createdAt}
+ connChatVRange = fromMaybe (versionToRange maxVer) $ safeVersionRange minVer maxVer
+ in Connection {connId, agentConnId = AgentConnId acId, connChatVRange, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, localAlias, entityId, connectionCode, authErrCounter, createdAt}
where
entityId_ :: ConnType -> Maybe Int64
entityId_ ConnContact = contactId
@@ -150,12 +152,12 @@ toConnection ((connId, acId, connLevel, viaContact, viaUserContactLink, viaGroup
entityId_ ConnUserContact = userContactLinkId
toMaybeConnection :: MaybeConnectionRow -> Maybe Connection
-toMaybeConnection ((Just connId, Just agentConnId, Just connLevel, viaContact, viaUserContactLink, Just viaGroupLink, groupLinkId, customUserProfileId, Just connStatus, Just connType, Just localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (Just createdAt, code_, verifiedAt_, Just authErrCounter)) =
- Just $ toConnection ((connId, agentConnId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (createdAt, code_, verifiedAt_, authErrCounter))
+toMaybeConnection ((Just connId, Just agentConnId, Just connLevel, viaContact, viaUserContactLink, Just viaGroupLink, groupLinkId, customUserProfileId, Just connStatus, Just connType, Just localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (Just createdAt, code_, verifiedAt_, Just authErrCounter, Just minVer, Just maxVer)) =
+ Just $ toConnection ((connId, agentConnId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (createdAt, code_, verifiedAt_, authErrCounter, minVer, maxVer))
toMaybeConnection _ = Nothing
-createConnection_ :: DB.Connection -> UserId -> ConnType -> Maybe Int64 -> ConnId -> Maybe ContactId -> Maybe Int64 -> Maybe ProfileId -> Int -> UTCTime -> IO Connection
-createConnection_ db userId connType entityId acId viaContact viaUserContactLink customUserProfileId connLevel currentTs = do
+createConnection_ :: DB.Connection -> UserId -> ConnType -> Maybe Int64 -> ConnId -> VersionRange -> Maybe ContactId -> Maybe Int64 -> Maybe ProfileId -> Int -> UTCTime -> IO Connection
+createConnection_ db userId connType entityId acId connChatVRange@(VersionRange minV maxV) viaContact viaUserContactLink customUserProfileId connLevel currentTs = do
viaLinkGroupId :: Maybe Int64 <- fmap join . forM viaUserContactLink $ \ucLinkId ->
maybeFirstRow fromOnly $ DB.query db "SELECT group_id FROM user_contact_links WHERE user_id = ? AND user_contact_link_id = ? AND group_id IS NOT NULL" (userId, ucLinkId)
let viaGroupLink = isJust viaLinkGroupId
@@ -164,17 +166,30 @@ createConnection_ db userId connType entityId acId viaContact viaUserContactLink
[sql|
INSERT INTO connections (
user_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, via_group_link, custom_user_profile_id, conn_status, conn_type,
- contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at, updated_at
- ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
+ contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at, updated_at,
+ chat_vrange_min_version, chat_vrange_max_version
+ ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|]
( (userId, acId, connLevel, viaContact, viaUserContactLink, viaGroupLink, customUserProfileId, ConnNew, connType)
:. (ent ConnContact, ent ConnMember, ent ConnSndFile, ent ConnRcvFile, ent ConnUserContact, currentTs, currentTs)
+ :. (minV, maxV)
)
connId <- insertedRowId db
- pure Connection {connId, agentConnId = AgentConnId acId, connType, entityId, viaContact, viaUserContactLink, viaGroupLink, groupLinkId = Nothing, customUserProfileId, connLevel, connStatus = ConnNew, localAlias = "", createdAt = currentTs, connectionCode = Nothing, authErrCounter = 0}
+ pure Connection {connId, agentConnId = AgentConnId acId, connChatVRange, connType, entityId, viaContact, viaUserContactLink, viaGroupLink, groupLinkId = Nothing, customUserProfileId, connLevel, connStatus = ConnNew, localAlias = "", createdAt = currentTs, connectionCode = Nothing, authErrCounter = 0}
where
ent ct = if connType == ct then entityId else Nothing
+setConnChatVRange :: DB.Connection -> Int64 -> VersionRange -> IO ()
+setConnChatVRange db connId (VersionRange minVer maxVer) =
+ DB.execute
+ db
+ [sql|
+ UPDATE connections
+ SET chat_vrange_min_version = ?, chat_vrange_max_version = ?
+ WHERE connection_id = ?
+ |]
+ (minVer, maxVer, connId)
+
setCommandConnId :: DB.Connection -> User -> CommandId -> Int64 -> IO ()
setCommandConnId db User {userId} cmdId connId = do
updatedAt <- getCurrentTime
@@ -256,12 +271,13 @@ getProfileById db userId profileId =
toProfile :: (ContactName, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Maybe Preferences) -> LocalProfile
toProfile (displayName, fullName, image, contactLink, localAlias, preferences) = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences, localAlias}
-type ContactRequestRow = (Int64, ContactName, AgentInvId, Int64, AgentConnId, Int64, ContactName, Text, Maybe ImageData, Maybe ConnReqContact) :. (Maybe XContactId, Maybe Preferences, UTCTime, UTCTime)
+type ContactRequestRow = (Int64, ContactName, AgentInvId, Int64, AgentConnId, Int64, ContactName, Text, Maybe ImageData, Maybe ConnReqContact) :. (Maybe XContactId, Maybe Preferences, UTCTime, UTCTime, Version, Version)
toContactRequest :: ContactRequestRow -> UserContactRequest
-toContactRequest ((contactRequestId, localDisplayName, agentInvitationId, userContactLinkId, agentContactConnId, profileId, displayName, fullName, image, contactLink) :. (xContactId, preferences, createdAt, updatedAt)) = do
+toContactRequest ((contactRequestId, localDisplayName, agentInvitationId, userContactLinkId, agentContactConnId, profileId, displayName, fullName, image, contactLink) :. (xContactId, preferences, createdAt, updatedAt, minVer, maxVer)) = do
let profile = Profile {displayName, fullName, image, contactLink, preferences}
- in UserContactRequest {contactRequestId, agentInvitationId, userContactLinkId, agentContactConnId, localDisplayName, profileId, profile, xContactId, createdAt, updatedAt}
+ cReqChatVRange = fromMaybe (versionToRange maxVer) $ safeVersionRange minVer maxVer
+ in UserContactRequest {contactRequestId, agentInvitationId, userContactLinkId, agentContactConnId, cReqChatVRange, localDisplayName, profileId, profile, xContactId, createdAt, updatedAt}
userQuery :: Query
userQuery =
diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs
index ac71ce612..ac19cbc36 100644
--- a/src/Simplex/Chat/Types.hs
+++ b/src/Simplex/Chat/Types.hs
@@ -46,6 +46,7 @@ import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (dropPrefix, fromTextField_, sumTypeJSON, taggedObjectJSON)
import Simplex.Messaging.Protocol (ProtoServerWithAuth, ProtocolTypeI)
import Simplex.Messaging.Util ((<$?>))
+import Simplex.Messaging.Version
class IsContact a where
contactId' :: a -> ContactId
@@ -231,6 +232,7 @@ data UserContactRequest = UserContactRequest
agentInvitationId :: AgentInvId,
userContactLinkId :: Int64,
agentContactConnId :: AgentConnId, -- connection id of user contact
+ cReqChatVRange :: VersionRange,
localDisplayName :: ContactName,
profileId :: Int64,
profile :: Profile,
@@ -1154,6 +1156,7 @@ type ConnReqContact = ConnectionRequestUri 'CMContact
data Connection = Connection
{ connId :: Int64,
agentConnId :: AgentConnId,
+ connChatVRange :: VersionRange,
connLevel :: Int,
viaContact :: Maybe Int64, -- group member contact ID, if not direct connection
viaUserContactLink :: Maybe Int64, -- user contact link ID, if connected via "user address"
diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs
index 033b1c9f7..2f512a9b7 100644
--- a/src/Simplex/Chat/View.hs
+++ b/src/Simplex/Chat/View.hs
@@ -57,6 +57,7 @@ import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType, Pro
import qualified Simplex.Messaging.Protocol as SMP
import Simplex.Messaging.Transport.Client (TransportHost (..))
import Simplex.Messaging.Util (bshow, tshow)
+import Simplex.Messaging.Version hiding (version)
import System.Console.ANSI.Types
type CurrentTime = UTCTime
@@ -949,7 +950,7 @@ viewNetworkConfig NetworkConfig {socksProxy, tcpTimeout} =
]
viewContactInfo :: Contact -> ConnectionStats -> Maybe Profile -> [StyledString]
-viewContactInfo ct@Contact {contactId, profile = LocalProfile {localAlias, contactLink}} stats incognitoProfile =
+viewContactInfo ct@Contact {contactId, profile = LocalProfile {localAlias, contactLink}, activeConn} stats incognitoProfile =
["contact ID: " <> sShow contactId] <> viewConnectionStats stats
<> maybe [] (\l -> ["contact address: " <> (plain . strEncode) l]) contactLink
<> maybe
@@ -958,6 +959,7 @@ viewContactInfo ct@Contact {contactId, profile = LocalProfile {localAlias, conta
incognitoProfile
<> ["alias: " <> plain localAlias | localAlias /= ""]
<> [viewConnectionVerified (contactSecurityCode ct)]
+ <> [viewConnChatVRange (connChatVRange activeConn)]
viewGroupInfo :: GroupInfo -> GroupSummary -> [StyledString]
viewGroupInfo GroupInfo {groupId} s =
@@ -966,18 +968,22 @@ viewGroupInfo GroupInfo {groupId} s =
]
viewGroupMemberInfo :: GroupInfo -> GroupMember -> Maybe ConnectionStats -> [StyledString]
-viewGroupMemberInfo GroupInfo {groupId} m@GroupMember {groupMemberId, memberProfile = LocalProfile {localAlias}} stats =
+viewGroupMemberInfo GroupInfo {groupId} m@GroupMember {groupMemberId, memberProfile = LocalProfile {localAlias}, activeConn} stats =
[ "group ID: " <> sShow groupId,
"member ID: " <> sShow groupMemberId
]
<> maybe ["member not connected"] viewConnectionStats stats
<> ["alias: " <> plain localAlias | localAlias /= ""]
<> [viewConnectionVerified (memberSecurityCode m) | isJust stats]
+ <> maybe [] (\ac -> [viewConnChatVRange (connChatVRange ac)]) activeConn
viewConnectionVerified :: Maybe SecurityCode -> StyledString
viewConnectionVerified (Just _) = "connection verified" -- TODO show verification time?
viewConnectionVerified _ = "connection not verified, use " <> highlight' "/code" <> " command to see security code"
+viewConnChatVRange :: VersionRange -> StyledString
+viewConnChatVRange (VersionRange minVer maxVer) = "chat protocol version range: (" <> sShow minVer <> ", " <> sShow maxVer <> ")"
+
viewConnectionStats :: ConnectionStats -> [StyledString]
viewConnectionStats ConnectionStats {rcvQueuesInfo, sndQueuesInfo} =
["receiving messages via: " <> viewRcvQueuesInfo rcvQueuesInfo | not $ null rcvQueuesInfo]
diff --git a/tests/ChatTests/Direct.hs b/tests/ChatTests/Direct.hs
index 2384daac3..e7442d90a 100644
--- a/tests/ChatTests/Direct.hs
+++ b/tests/ChatTests/Direct.hs
@@ -17,9 +17,11 @@ import qualified Data.ByteString.Lazy.Char8 as LB
import Simplex.Chat.Call
import Simplex.Chat.Controller (ChatConfig (..))
import Simplex.Chat.Options (ChatOpts (..))
+import Simplex.Chat.Protocol (supportedChatVRange)
import Simplex.Chat.Store (agentStoreFile, chatStoreFile)
import Simplex.Chat.Types (authErrDisableCount, sameVerificationCode, verificationCode)
import qualified Simplex.Messaging.Crypto as C
+import Simplex.Messaging.Version
import System.Directory (copyFile, doesDirectoryExist, doesFileExist)
import System.FilePath ((>))
import Test.Hspec
@@ -94,6 +96,21 @@ chatDirectTests = do
describe "delivery receipts" $ do
it "should send delivery receipts" testSendDeliveryReceipts
it "should send delivery receipts depending on configuration" testConfigureDeliveryReceipts
+ describe "negotiate connection chat protocol version range" $ do
+ describe "version range correctly set for new connection via invitation" $ do
+ testInvVRange supportedChatVRange supportedChatVRange
+ testInvVRange supportedChatVRange vr11
+ testInvVRange vr11 supportedChatVRange
+ testInvVRange vr11 vr11
+ describe "version range correctly set for new connection via contact request" $ do
+ testReqVRange supportedChatVRange supportedChatVRange
+ testReqVRange supportedChatVRange vr11
+ testReqVRange vr11 supportedChatVRange
+ testReqVRange vr11 vr11
+ it "update connection version range on received messages" testUpdateConnChatVRange
+ where
+ testInvVRange vr1 vr2 = it (vRangeStr vr1 <> " - " <> vRangeStr vr2) $ testConnInvChatVRange vr1 vr2
+ testReqVRange vr1 vr2 = it (vRangeStr vr1 <> " - " <> vRangeStr vr2) $ testConnReqChatVRange vr1 vr2
testAddContact :: HasCallStack => SpecWith FilePath
testAddContact = versionTestMatrix2 runTestAddContact
@@ -1939,8 +1956,7 @@ testMarkContactVerified =
testChat2 aliceProfile bobProfile $ \alice bob -> do
connectUsers alice bob
alice ##> "/i bob"
- bobInfo alice
- alice <## "connection not verified, use /code command to see security code"
+ bobInfo alice False
alice ##> "/code bob"
bCode <- getTermLine alice
bob ##> "/code alice"
@@ -1951,28 +1967,31 @@ testMarkContactVerified =
alice ##> ("/verify bob " <> aCode)
alice <## "connection verified"
alice ##> "/i bob"
- bobInfo alice
- alice <## "connection verified"
+ bobInfo alice True
alice ##> "/verify bob"
alice <##. "connection not verified, current code is "
alice ##> "/i bob"
- bobInfo alice
- alice <## "connection not verified, use /code command to see security code"
+ bobInfo alice False
where
- bobInfo :: HasCallStack => TestCC -> IO ()
- bobInfo alice = do
+ bobInfo :: HasCallStack => TestCC -> Bool -> IO ()
+ bobInfo alice verified = do
alice <## "contact ID: 2"
alice <## "receiving messages via: localhost"
alice <## "sending messages via: localhost"
alice <## "you've shared main profile with this contact"
+ alice <## connVerified
+ alice <## currentChatVRangeInfo
+ where
+ connVerified
+ | verified = "connection verified"
+ | otherwise = "connection not verified, use /code command to see security code"
testMarkGroupMemberVerified :: HasCallStack => FilePath -> IO ()
testMarkGroupMemberVerified =
testChat2 aliceProfile bobProfile $ \alice bob -> do
createGroup2 "team" alice bob
alice ##> "/i #team bob"
- bobInfo alice
- alice <## "connection not verified, use /code command to see security code"
+ bobInfo alice False
alice ##> "/code #team bob"
bCode <- getTermLine alice
bob ##> "/code #team alice"
@@ -1983,20 +2002,24 @@ testMarkGroupMemberVerified =
alice ##> ("/verify #team bob " <> aCode)
alice <## "connection verified"
alice ##> "/i #team bob"
- bobInfo alice
- alice <## "connection verified"
+ bobInfo alice True
alice ##> "/verify #team bob"
alice <##. "connection not verified, current code is "
alice ##> "/i #team bob"
- bobInfo alice
- alice <## "connection not verified, use /code command to see security code"
+ bobInfo alice False
where
- bobInfo :: HasCallStack => TestCC -> IO ()
- bobInfo alice = do
+ bobInfo :: HasCallStack => TestCC -> Bool -> IO ()
+ bobInfo alice verified = do
alice <## "group ID: 1"
alice <## "member ID: 2"
alice <## "receiving messages via: localhost"
alice <## "sending messages via: localhost"
+ alice <## connVerified
+ alice <## currentChatVRangeInfo
+ where
+ connVerified
+ | verified = "connection verified"
+ | otherwise = "connection not verified, use /code command to see security code"
testMsgDecryptError :: HasCallStack => FilePath -> IO ()
testMsgDecryptError tmp =
@@ -2088,8 +2111,7 @@ testSyncRatchetCodeReset tmp =
alice <# "bob> hey"
-- connection not verified
bob ##> "/i alice"
- aliceInfo bob
- bob <## "connection not verified, use /code command to see security code"
+ aliceInfo bob False
-- verify connection
alice ##> "/code bob"
bCode <- getTermLine alice
@@ -2097,8 +2119,7 @@ testSyncRatchetCodeReset tmp =
bob <## "connection verified"
-- connection verified
bob ##> "/i alice"
- aliceInfo bob
- bob <## "connection verified"
+ aliceInfo bob True
setupDesynchronizedRatchet tmp alice
withTestChat tmp "bob_old" $ \bob -> do
bob <## "1 contacts connected (use /cs for the list)"
@@ -2115,20 +2136,25 @@ testSyncRatchetCodeReset tmp =
-- connection not verified
bob ##> "/i alice"
- aliceInfo bob
- bob <## "connection not verified, use /code command to see security code"
+ aliceInfo bob False
alice #> "@bob hello again"
bob <# "alice> hello again"
bob #> "@alice received!"
alice <# "bob> received!"
where
- aliceInfo :: HasCallStack => TestCC -> IO ()
- aliceInfo bob = do
+ aliceInfo :: HasCallStack => TestCC -> Bool -> IO ()
+ aliceInfo bob verified = do
bob <## "contact ID: 2"
bob <## "receiving messages via: localhost"
bob <## "sending messages via: localhost"
bob <## "you've shared main profile with this contact"
+ bob <## connVerified
+ bob <## currentChatVRangeInfo
+ where
+ connVerified
+ | verified = "connection verified"
+ | otherwise = "connection not verified, use /code command to see security code"
testSetMessageReactions :: HasCallStack => FilePath -> IO ()
testSetMessageReactions =
@@ -2271,3 +2297,85 @@ testConfigureDeliveryReceipts tmp =
cc1 #> ("@" <> name2 <> " " <> msg)
cc2 <# (name1 <> "> " <> msg)
cc1 / 50000
+
+testConnInvChatVRange :: HasCallStack => VersionRange -> VersionRange -> FilePath -> IO ()
+testConnInvChatVRange ct1VRange ct2VRange tmp =
+ withNewTestChatCfg tmp testCfg {chatVRange = ct1VRange} "alice" aliceProfile $ \alice -> do
+ withNewTestChatCfg tmp testCfg {chatVRange = ct2VRange} "bob" bobProfile $ \bob -> do
+ connectUsers alice bob
+
+ alice ##> "/i bob"
+ contactInfoChatVRange alice ct2VRange
+
+ bob ##> "/i alice"
+ contactInfoChatVRange bob ct1VRange
+
+testConnReqChatVRange :: HasCallStack => VersionRange -> VersionRange -> FilePath -> IO ()
+testConnReqChatVRange ct1VRange ct2VRange tmp =
+ withNewTestChatCfg tmp testCfg {chatVRange = ct1VRange} "alice" aliceProfile $ \alice -> do
+ withNewTestChatCfg tmp testCfg {chatVRange = ct2VRange} "bob" bobProfile $ \bob -> do
+ alice ##> "/ad"
+ cLink <- getContactLink alice True
+ bob ##> ("/c " <> cLink)
+ alice <#? bob
+ alice ##> "/ac bob"
+ alice <## "bob (Bob): accepting contact request..."
+ concurrently_
+ (bob <## "alice (Alice): contact is connected")
+ (alice <## "bob (Bob): contact is connected")
+
+ alice ##> "/i bob"
+ contactInfoChatVRange alice ct2VRange
+
+ bob ##> "/i alice"
+ contactInfoChatVRange bob ct1VRange
+
+testUpdateConnChatVRange :: HasCallStack => FilePath -> IO ()
+testUpdateConnChatVRange tmp =
+ withNewTestChat tmp "alice" aliceProfile $ \alice -> do
+ withNewTestChatCfg tmp cfg11 "bob" bobProfile $ \bob -> do
+ connectUsers alice bob
+
+ alice ##> "/i bob"
+ contactInfoChatVRange alice vr11
+
+ bob ##> "/i alice"
+ contactInfoChatVRange bob supportedChatVRange
+
+ withTestChat tmp "bob" $ \bob -> do
+ bob <## "1 contacts connected (use /cs for the list)"
+
+ bob #> "@alice hello 1"
+ alice <# "bob> hello 1"
+
+ alice ##> "/i bob"
+ contactInfoChatVRange alice supportedChatVRange
+
+ bob ##> "/i alice"
+ contactInfoChatVRange bob supportedChatVRange
+
+ withTestChatCfg tmp cfg11 "bob" $ \bob -> do
+ bob <## "1 contacts connected (use /cs for the list)"
+
+ bob #> "@alice hello 2"
+ alice <# "bob> hello 2"
+
+ alice ##> "/i bob"
+ contactInfoChatVRange alice vr11
+
+ bob ##> "/i alice"
+ contactInfoChatVRange bob supportedChatVRange
+ where
+ cfg11 = testCfg {chatVRange = vr11} :: ChatConfig
+
+vr11 :: VersionRange
+vr11 = mkVersionRange 1 1
+
+contactInfoChatVRange :: TestCC -> VersionRange -> IO ()
+contactInfoChatVRange cc (VersionRange minVer maxVer) = do
+ cc <## "contact ID: 2"
+ cc <## "receiving messages via: localhost"
+ cc <## "sending messages via: localhost"
+ cc <## "you've shared main profile with this contact"
+ cc <## "connection not verified, use /code command to see security code"
+ cc <## ("chat protocol version range: (" <> show minVer <> ", " <> show maxVer <> ")")
diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs
index 2f5b2689b..7a4bb2061 100644
--- a/tests/ChatTests/Groups.hs
+++ b/tests/ChatTests/Groups.hs
@@ -2289,8 +2289,7 @@ testGroupSyncRatchetCodeReset tmp =
alice <# "#team bob> hey"
-- connection not verified
bob ##> "/i #team alice"
- aliceInfo bob
- bob <## "connection not verified, use /code command to see security code"
+ aliceInfo bob False
-- verify connection
alice ##> "/code #team bob"
bCode <- getTermLine alice
@@ -2298,8 +2297,7 @@ testGroupSyncRatchetCodeReset tmp =
bob <## "connection verified"
-- connection verified
bob ##> "/i #team alice"
- aliceInfo bob
- bob <## "connection verified"
+ aliceInfo bob True
setupDesynchronizedRatchet tmp alice
withTestChat tmp "bob_old" $ \bob -> do
bob <## "1 contacts connected (use /cs for the list)"
@@ -2317,20 +2315,25 @@ testGroupSyncRatchetCodeReset tmp =
-- connection not verified
bob ##> "/i #team alice"
- aliceInfo bob
- bob <## "connection not verified, use /code command to see security code"
+ aliceInfo bob False
alice #> "#team hello again"
bob <# "#team alice> hello again"
bob #> "#team received!"
alice <# "#team bob> received!"
where
- aliceInfo :: HasCallStack => TestCC -> IO ()
- aliceInfo bob = do
+ aliceInfo :: HasCallStack => TestCC -> Bool -> IO ()
+ aliceInfo bob verified = do
bob <## "group ID: 1"
bob <## "member ID: 1"
bob <## "receiving messages via: localhost"
bob <## "sending messages via: localhost"
+ bob <## connVerified
+ bob <## currentChatVRangeInfo
+ where
+ connVerified
+ | verified = "connection verified"
+ | otherwise = "connection not verified, use /code command to see security code"
testSetGroupMessageReactions :: HasCallStack => FilePath -> IO ()
testSetGroupMessageReactions =
diff --git a/tests/ChatTests/Profiles.hs b/tests/ChatTests/Profiles.hs
index 98c840388..c51202340 100644
--- a/tests/ChatTests/Profiles.hs
+++ b/tests/ChatTests/Profiles.hs
@@ -214,6 +214,7 @@ testProfileLink =
cc <## ("contact address: " <> cLink)
cc <## "you've shared main profile with this contact"
cc <## "connection not verified, use /code command to see security code"
+ cc <## currentChatVRangeInfo
checkAliceNoProfileLink cc = do
cc ##> "/info alice"
cc <## "contact ID: 2"
@@ -221,6 +222,7 @@ testProfileLink =
cc <##. "sending messages via"
cc <## "you've shared main profile with this contact"
cc <## "connection not verified, use /code command to see security code"
+ cc <## currentChatVRangeInfo
testUserContactLinkAutoAccept :: HasCallStack => FilePath -> IO ()
testUserContactLinkAutoAccept =
diff --git a/tests/ChatTests/Utils.hs b/tests/ChatTests/Utils.hs
index 4c7ca8d0a..b525bf333 100644
--- a/tests/ChatTests/Utils.hs
+++ b/tests/ChatTests/Utils.hs
@@ -18,11 +18,13 @@ import Data.Maybe (fromMaybe)
import Data.String
import qualified Data.Text as T
import Simplex.Chat.Controller (ChatConfig (..), ChatController (..), InlineFilesConfig (..), defaultInlineFilesConfig)
+import Simplex.Chat.Protocol
import Simplex.Chat.Store.Profiles (getUserContactProfiles)
import Simplex.Chat.Types
import Simplex.Chat.Types.Preferences
import Simplex.Messaging.Agent.Store.SQLite (withTransaction)
import Simplex.Messaging.Encoding.String
+import Simplex.Messaging.Version
import System.Directory (doesFileExist)
import System.Environment (lookupEnv)
import System.FilePath ((>))
@@ -356,7 +358,7 @@ dropTime_ msg = case splitAt 6 msg of
_ -> Nothing
dropStrPrefix :: HasCallStack => String -> String -> String
-dropStrPrefix pfx s =
+dropStrPrefix pfx s =
let (p, rest) = splitAt (length pfx) s
in if p == pfx then rest else error $ "no prefix " <> pfx <> " in string : " <> s
@@ -523,3 +525,10 @@ startFileTransferWithDest' cc1 cc2 fileName fileSize fileDest_ = do
concurrently_
(cc2 <## ("started receiving file 1 (" <> fileName <> ") from " <> name1))
(cc1 <## ("started sending file 1 (" <> fileName <> ") to " <> name2))
+
+currentChatVRangeInfo :: String
+currentChatVRangeInfo =
+ "chat protocol version range: " <> vRangeStr supportedChatVRange
+
+vRangeStr :: VersionRange -> String
+vRangeStr (VersionRange minVer maxVer) = "(" <> show minVer <> ", " <> show maxVer <> ")"
diff --git a/tests/ProtocolTests.hs b/tests/ProtocolTests.hs
index 6f7e0b8cf..98c592fa7 100644
--- a/tests/ProtocolTests.hs
+++ b/tests/ProtocolTests.hs
@@ -76,10 +76,10 @@ s ##==## msg = do
s ==## msg
(==#) :: MsgEncodingI e => ByteString -> ChatMsgEvent e -> Expectation
-s ==# msg = s ==## ChatMessage Nothing msg
+s ==# msg = s ==## ChatMessage chatInitialVRange Nothing msg
(#==) :: MsgEncodingI e => ByteString -> ChatMsgEvent e -> Expectation
-s #== msg = s ##== ChatMessage Nothing msg
+s #== msg = s ##== ChatMessage chatInitialVRange Nothing msg
(#==#) :: MsgEncodingI e => ByteString -> ChatMsgEvent e -> Expectation
s #==# msg = do
@@ -101,59 +101,66 @@ testGroupProfile = GroupProfile {displayName = "team", fullName = "Team", descri
decodeChatMessageTest :: Spec
decodeChatMessageTest = describe "Chat message encoding/decoding" $ do
it "x.msg.new simple text" $
- "{\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}"
+ "{\"v\":\"1\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}"
#==# XMsgNew (MCSimple (extMsgContent (MCText "hello") Nothing))
it "x.msg.new simple text - timed message TTL" $
- "{\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"ttl\":3600}}"
+ "{\"v\":\"1\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"ttl\":3600}}"
#==# XMsgNew (MCSimple (ExtMsgContent (MCText "hello") Nothing (Just 3600) Nothing))
it "x.msg.new simple text - live message" $
- "{\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"live\":true}}"
+ "{\"v\":\"1\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"live\":true}}"
#==# XMsgNew (MCSimple (ExtMsgContent (MCText "hello") Nothing Nothing (Just True)))
it "x.msg.new simple link" $
- "{\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"https://simplex.chat\",\"type\":\"link\",\"preview\":{\"description\":\"SimpleX Chat\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgA\",\"title\":\"SimpleX Chat\",\"uri\":\"https://simplex.chat\"}}}}"
+ "{\"v\":\"1\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"https://simplex.chat\",\"type\":\"link\",\"preview\":{\"description\":\"SimpleX Chat\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgA\",\"title\":\"SimpleX Chat\",\"uri\":\"https://simplex.chat\"}}}}"
#==# XMsgNew (MCSimple (extMsgContent (MCLink "https://simplex.chat" $ LinkPreview {uri = "https://simplex.chat", title = "SimpleX Chat", description = "SimpleX Chat", image = ImageData "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgA", content = Nothing}) Nothing))
it "x.msg.new simple image" $
- "{\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}}"
+ "{\"v\":\"1\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}}"
#==# XMsgNew (MCSimple (extMsgContent (MCImage "" $ ImageData "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=") Nothing))
it "x.msg.new simple image with text" $
- "{\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"here's an image\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}}"
+ "{\"v\":\"1\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"here's an image\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}}"
#==# XMsgNew (MCSimple (extMsgContent (MCImage "here's an image" $ ImageData "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=") Nothing))
- it "x.msg.new chat message " $
- "{\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}"
- ##==## ChatMessage (Just $ SharedMsgId "\1\2\3\4") (XMsgNew (MCSimple (extMsgContent (MCText "hello") Nothing)))
+ it "x.msg.new chat message" $
+ "{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}"
+ ##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew (MCSimple (extMsgContent (MCText "hello") Nothing)))
+ it "x.msg.new chat message with chat version range" $
+ "{\"v\":\"1-2\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}"
+ ##==## ChatMessage supportedChatVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew (MCSimple (extMsgContent (MCText "hello") Nothing)))
it "x.msg.new quote" $
- "{\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello to you too\",\"type\":\"text\"},\"quote\":{\"content\":{\"text\":\"hello there!\",\"type\":\"text\"},\"msgRef\":{\"msgId\":\"BQYHCA==\",\"sent\":true,\"sentAt\":\"1970-01-01T00:00:01.000000001Z\"}}}}"
+ "{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello to you too\",\"type\":\"text\"},\"quote\":{\"content\":{\"text\":\"hello there!\",\"type\":\"text\"},\"msgRef\":{\"msgId\":\"BQYHCA==\",\"sent\":true,\"sentAt\":\"1970-01-01T00:00:01.000000001Z\"}}}}"
##==## ChatMessage
+ chatInitialVRange
(Just $ SharedMsgId "\1\2\3\4")
(XMsgNew (MCQuote quotedMsg (extMsgContent (MCText "hello to you too") Nothing)))
it "x.msg.new quote - timed message TTL" $
- "{\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello to you too\",\"type\":\"text\"},\"quote\":{\"content\":{\"text\":\"hello there!\",\"type\":\"text\"},\"msgRef\":{\"msgId\":\"BQYHCA==\",\"sent\":true,\"sentAt\":\"1970-01-01T00:00:01.000000001Z\"}},\"ttl\":3600}}"
+ "{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello to you too\",\"type\":\"text\"},\"quote\":{\"content\":{\"text\":\"hello there!\",\"type\":\"text\"},\"msgRef\":{\"msgId\":\"BQYHCA==\",\"sent\":true,\"sentAt\":\"1970-01-01T00:00:01.000000001Z\"}},\"ttl\":3600}}"
##==## ChatMessage
+ chatInitialVRange
(Just $ SharedMsgId "\1\2\3\4")
(XMsgNew (MCQuote quotedMsg (ExtMsgContent (MCText "hello to you too") Nothing (Just 3600) Nothing)))
it "x.msg.new quote - live message" $
- "{\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello to you too\",\"type\":\"text\"},\"quote\":{\"content\":{\"text\":\"hello there!\",\"type\":\"text\"},\"msgRef\":{\"msgId\":\"BQYHCA==\",\"sent\":true,\"sentAt\":\"1970-01-01T00:00:01.000000001Z\"}},\"live\":true}}"
+ "{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello to you too\",\"type\":\"text\"},\"quote\":{\"content\":{\"text\":\"hello there!\",\"type\":\"text\"},\"msgRef\":{\"msgId\":\"BQYHCA==\",\"sent\":true,\"sentAt\":\"1970-01-01T00:00:01.000000001Z\"}},\"live\":true}}"
##==## ChatMessage
+ chatInitialVRange
(Just $ SharedMsgId "\1\2\3\4")
(XMsgNew (MCQuote quotedMsg (ExtMsgContent (MCText "hello to you too") Nothing Nothing (Just True))))
it "x.msg.new forward" $
- "{\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"forward\":true}}"
- ##==## ChatMessage (Just $ SharedMsgId "\1\2\3\4") (XMsgNew $ MCForward (extMsgContent (MCText "hello") Nothing))
+ "{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"forward\":true}}"
+ ##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew $ MCForward (extMsgContent (MCText "hello") Nothing))
it "x.msg.new forward - timed message TTL" $
- "{\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"forward\":true,\"ttl\":3600}}"
- ##==## ChatMessage (Just $ SharedMsgId "\1\2\3\4") (XMsgNew $ MCForward (ExtMsgContent (MCText "hello") Nothing (Just 3600) Nothing))
+ "{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"forward\":true,\"ttl\":3600}}"
+ ##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew $ MCForward (ExtMsgContent (MCText "hello") Nothing (Just 3600) Nothing))
it "x.msg.new forward - live message" $
- "{\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"forward\":true,\"live\":true}}"
- ##==## ChatMessage (Just $ SharedMsgId "\1\2\3\4") (XMsgNew $ MCForward (ExtMsgContent (MCText "hello") Nothing Nothing (Just True)))
+ "{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"forward\":true,\"live\":true}}"
+ ##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew $ MCForward (ExtMsgContent (MCText "hello") Nothing Nothing (Just True)))
it "x.msg.new simple text with file" $
- "{\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"file\":{\"fileSize\":12345,\"fileName\":\"photo.jpg\"}}}"
+ "{\"v\":\"1\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"file\":{\"fileSize\":12345,\"fileName\":\"photo.jpg\"}}}"
#==# XMsgNew (MCSimple (extMsgContent (MCText "hello") (Just FileInvitation {fileName = "photo.jpg", fileSize = 12345, fileDigest = Nothing, fileConnReq = Nothing, fileInline = Nothing, fileDescr = Nothing})))
it "x.msg.new simple file with file" $
- "{\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"\",\"type\":\"file\"},\"file\":{\"fileSize\":12345,\"fileName\":\"file.txt\"}}}"
+ "{\"v\":\"1\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"\",\"type\":\"file\"},\"file\":{\"fileSize\":12345,\"fileName\":\"file.txt\"}}}"
#==# XMsgNew (MCSimple (extMsgContent (MCFile "") (Just FileInvitation {fileName = "file.txt", fileSize = 12345, fileDigest = Nothing, fileConnReq = Nothing, fileInline = Nothing, fileDescr = Nothing})))
it "x.msg.new quote with file" $
- "{\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello to you too\",\"type\":\"text\"},\"quote\":{\"content\":{\"text\":\"hello there!\",\"type\":\"text\"},\"msgRef\":{\"msgId\":\"BQYHCA==\",\"sent\":true,\"sentAt\":\"1970-01-01T00:00:01.000000001Z\"}},\"file\":{\"fileSize\":12345,\"fileName\":\"photo.jpg\"}}}"
+ "{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello to you too\",\"type\":\"text\"},\"quote\":{\"content\":{\"text\":\"hello there!\",\"type\":\"text\"},\"msgRef\":{\"msgId\":\"BQYHCA==\",\"sent\":true,\"sentAt\":\"1970-01-01T00:00:01.000000001Z\"}},\"file\":{\"fileSize\":12345,\"fileName\":\"photo.jpg\"}}}"
##==## ChatMessage
+ chatInitialVRange
(Just $ SharedMsgId "\1\2\3\4")
( XMsgNew
( MCQuote
@@ -165,101 +172,101 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do
)
)
it "x.msg.new forward with file" $
- "{\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"forward\":true,\"file\":{\"fileSize\":12345,\"fileName\":\"photo.jpg\"}}}"
- ##==## ChatMessage (Just $ SharedMsgId "\1\2\3\4") (XMsgNew $ MCForward (extMsgContent (MCText "hello") (Just FileInvitation {fileName = "photo.jpg", fileSize = 12345, fileDigest = Nothing, fileConnReq = Nothing, fileInline = Nothing, fileDescr = Nothing})))
+ "{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"forward\":true,\"file\":{\"fileSize\":12345,\"fileName\":\"photo.jpg\"}}}"
+ ##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew $ MCForward (extMsgContent (MCText "hello") (Just FileInvitation {fileName = "photo.jpg", fileSize = 12345, fileDigest = Nothing, fileConnReq = Nothing, fileInline = Nothing, fileDescr = Nothing})))
it "x.msg.update" $
- "{\"event\":\"x.msg.update\",\"params\":{\"msgId\":\"AQIDBA==\", \"content\":{\"text\":\"hello\",\"type\":\"text\"}}}"
+ "{\"v\":\"1\",\"event\":\"x.msg.update\",\"params\":{\"msgId\":\"AQIDBA==\", \"content\":{\"text\":\"hello\",\"type\":\"text\"}}}"
#==# XMsgUpdate (SharedMsgId "\1\2\3\4") (MCText "hello") Nothing Nothing
it "x.msg.del" $
- "{\"event\":\"x.msg.del\",\"params\":{\"msgId\":\"AQIDBA==\"}}"
+ "{\"v\":\"1\",\"event\":\"x.msg.del\",\"params\":{\"msgId\":\"AQIDBA==\"}}"
#==# XMsgDel (SharedMsgId "\1\2\3\4") Nothing
it "x.msg.deleted" $
- "{\"event\":\"x.msg.deleted\",\"params\":{}}"
+ "{\"v\":\"1\",\"event\":\"x.msg.deleted\",\"params\":{}}"
#==# XMsgDeleted
it "x.file" $
- "{\"event\":\"x.file\",\"params\":{\"file\":{\"fileConnReq\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"fileSize\":12345,\"fileName\":\"photo.jpg\"}}}"
+ "{\"v\":\"1\",\"event\":\"x.file\",\"params\":{\"file\":{\"fileConnReq\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"fileSize\":12345,\"fileName\":\"photo.jpg\"}}}"
#==# XFile FileInvitation {fileName = "photo.jpg", fileSize = 12345, fileDigest = Nothing, fileConnReq = Just testConnReq, fileInline = Nothing, fileDescr = Nothing}
it "x.file without file invitation" $
- "{\"event\":\"x.file\",\"params\":{\"file\":{\"fileSize\":12345,\"fileName\":\"photo.jpg\"}}}"
+ "{\"v\":\"1\",\"event\":\"x.file\",\"params\":{\"file\":{\"fileSize\":12345,\"fileName\":\"photo.jpg\"}}}"
#==# XFile FileInvitation {fileName = "photo.jpg", fileSize = 12345, fileDigest = Nothing, fileConnReq = Nothing, fileInline = Nothing, fileDescr = Nothing}
it "x.file.acpt" $
- "{\"event\":\"x.file.acpt\",\"params\":{\"fileName\":\"photo.jpg\"}}"
+ "{\"v\":\"1\",\"event\":\"x.file.acpt\",\"params\":{\"fileName\":\"photo.jpg\"}}"
#==# XFileAcpt "photo.jpg"
it "x.file.acpt.inv" $
- "{\"event\":\"x.file.acpt.inv\",\"params\":{\"msgId\":\"AQIDBA==\",\"fileName\":\"photo.jpg\",\"fileConnReq\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}"
+ "{\"v\":\"1\",\"event\":\"x.file.acpt.inv\",\"params\":{\"msgId\":\"AQIDBA==\",\"fileName\":\"photo.jpg\",\"fileConnReq\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}"
#==# XFileAcptInv (SharedMsgId "\1\2\3\4") (Just testConnReq) "photo.jpg"
it "x.file.acpt.inv" $
- "{\"event\":\"x.file.acpt.inv\",\"params\":{\"msgId\":\"AQIDBA==\",\"fileName\":\"photo.jpg\"}}"
+ "{\"v\":\"1\",\"event\":\"x.file.acpt.inv\",\"params\":{\"msgId\":\"AQIDBA==\",\"fileName\":\"photo.jpg\"}}"
#==# XFileAcptInv (SharedMsgId "\1\2\3\4") Nothing "photo.jpg"
it "x.file.cancel" $
- "{\"event\":\"x.file.cancel\",\"params\":{\"msgId\":\"AQIDBA==\"}}"
+ "{\"v\":\"1\",\"event\":\"x.file.cancel\",\"params\":{\"msgId\":\"AQIDBA==\"}}"
#==# XFileCancel (SharedMsgId "\1\2\3\4")
it "x.info" $
- "{\"event\":\"x.info\",\"params\":{\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}"
+ "{\"v\":\"1\",\"event\":\"x.info\",\"params\":{\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}"
#==# XInfo testProfile
it "x.info with empty full name" $
- "{\"event\":\"x.info\",\"params\":{\"profile\":{\"fullName\":\"\",\"displayName\":\"alice\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}"
+ "{\"v\":\"1\",\"event\":\"x.info\",\"params\":{\"profile\":{\"fullName\":\"\",\"displayName\":\"alice\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}"
#==# XInfo Profile {displayName = "alice", fullName = "", image = Nothing, contactLink = Nothing, preferences = testChatPreferences}
it "x.contact with xContactId" $
- "{\"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\"}}}}}"
+ "{\"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")
it "x.contact without XContactId" $
- "{\"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\"}}}}}"
+ "{\"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
it "x.contact with content null" $
- "{\"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\"}}}}}"
+ "{\"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)" $
- "{\"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\"}}}}}"
+ "{\"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
it "x.grp.inv" $
- "{\"event\":\"x.grp.inv\",\"params\":{\"groupInvitation\":{\"connRequest\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%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==\"}}}}"
+ "{\"v\":\"1\",\"event\":\"x.grp.inv\",\"params\":{\"groupInvitation\":{\"connRequest\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%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, groupLinkId = Nothing}
it "x.grp.inv with group link id" $
- "{\"event\":\"x.grp.inv\",\"params\":{\"groupInvitation\":{\"connRequest\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%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==\"}, \"groupLinkId\":\"AQIDBA==\"}}}"
+ "{\"v\":\"1\",\"event\":\"x.grp.inv\",\"params\":{\"groupInvitation\":{\"connRequest\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%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==\"}, \"groupLinkId\":\"AQIDBA==\"}}}"
#==# XGrpInv GroupInvitation {fromMember = MemberIdRole (MemberId "\1\2\3\4") GRAdmin, invitedMember = MemberIdRole (MemberId "\5\6\7\8") GRMember, connRequest = testConnReq, groupProfile = testGroupProfile, groupLinkId = Just $ GroupLinkId "\1\2\3\4"}
it "x.grp.acpt without incognito profile" $
- "{\"event\":\"x.grp.acpt\",\"params\":{\"memberId\":\"AQIDBA==\"}}"
+ "{\"v\":\"1\",\"event\":\"x.grp.acpt\",\"params\":{\"memberId\":\"AQIDBA==\"}}"
#==# XGrpAcpt (MemberId "\1\2\3\4")
it "x.grp.mem.new" $
- "{\"event\":\"x.grp.mem.new\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}"
+ "{\"v\":\"1\",\"event\":\"x.grp.mem.new\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}"
#==# XGrpMemNew MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, profile = testProfile}
it "x.grp.mem.intro" $
- "{\"event\":\"x.grp.mem.intro\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}"
+ "{\"v\":\"1\",\"event\":\"x.grp.mem.intro\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}"
#==# XGrpMemIntro MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, profile = testProfile}
it "x.grp.mem.inv" $
- "{\"event\":\"x.grp.mem.inv\",\"params\":{\"memberId\":\"AQIDBA==\",\"memberIntro\":{\"directConnReq\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"groupConnReq\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}}"
+ "{\"v\":\"1\",\"event\":\"x.grp.mem.inv\",\"params\":{\"memberId\":\"AQIDBA==\",\"memberIntro\":{\"directConnReq\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"groupConnReq\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}}"
#==# XGrpMemInv (MemberId "\1\2\3\4") IntroInvitation {groupConnReq = testConnReq, directConnReq = testConnReq}
it "x.grp.mem.fwd" $
- "{\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"directConnReq\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"groupConnReq\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}"
+ "{\"v\":\"1\",\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"directConnReq\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"groupConnReq\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}"
#==# XGrpMemFwd MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, profile = testProfile} IntroInvitation {groupConnReq = testConnReq, directConnReq = testConnReq}
it "x.grp.mem.info" $
- "{\"event\":\"x.grp.mem.info\",\"params\":{\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}"
+ "{\"v\":\"1\",\"event\":\"x.grp.mem.info\",\"params\":{\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}"
#==# XGrpMemInfo (MemberId "\1\2\3\4") testProfile
it "x.grp.mem.con" $
- "{\"event\":\"x.grp.mem.con\",\"params\":{\"memberId\":\"AQIDBA==\"}}"
+ "{\"v\":\"1\",\"event\":\"x.grp.mem.con\",\"params\":{\"memberId\":\"AQIDBA==\"}}"
#==# XGrpMemCon (MemberId "\1\2\3\4")
it "x.grp.mem.con.all" $
- "{\"event\":\"x.grp.mem.con.all\",\"params\":{\"memberId\":\"AQIDBA==\"}}"
+ "{\"v\":\"1\",\"event\":\"x.grp.mem.con.all\",\"params\":{\"memberId\":\"AQIDBA==\"}}"
#==# XGrpMemConAll (MemberId "\1\2\3\4")
it "x.grp.mem.del" $
- "{\"event\":\"x.grp.mem.del\",\"params\":{\"memberId\":\"AQIDBA==\"}}"
+ "{\"v\":\"1\",\"event\":\"x.grp.mem.del\",\"params\":{\"memberId\":\"AQIDBA==\"}}"
#==# XGrpMemDel (MemberId "\1\2\3\4")
it "x.grp.leave" $
- "{\"event\":\"x.grp.leave\",\"params\":{}}"
+ "{\"v\":\"1\",\"event\":\"x.grp.leave\",\"params\":{}}"
==# XGrpLeave
it "x.grp.del" $
- "{\"event\":\"x.grp.del\",\"params\":{}}"
+ "{\"v\":\"1\",\"event\":\"x.grp.del\",\"params\":{}}"
==# XGrpDel
it "x.info.probe" $
- "{\"event\":\"x.info.probe\",\"params\":{\"probe\":\"AQIDBA==\"}}"
+ "{\"v\":\"1\",\"event\":\"x.info.probe\",\"params\":{\"probe\":\"AQIDBA==\"}}"
#==# XInfoProbe (Probe "\1\2\3\4")
it "x.info.probe.check" $
- "{\"event\":\"x.info.probe.check\",\"params\":{\"probeHash\":\"AQIDBA==\"}}"
+ "{\"v\":\"1\",\"event\":\"x.info.probe.check\",\"params\":{\"probeHash\":\"AQIDBA==\"}}"
#==# XInfoProbeCheck (ProbeHash "\1\2\3\4")
it "x.info.probe.ok" $
- "{\"event\":\"x.info.probe.ok\",\"params\":{\"probe\":\"AQIDBA==\"}}"
+ "{\"v\":\"1\",\"event\":\"x.info.probe.ok\",\"params\":{\"probe\":\"AQIDBA==\"}}"
#==# XInfoProbeOk (Probe "\1\2\3\4")
it "x.ok" $
- "{\"event\":\"x.ok\",\"params\":{}}"
+ "{\"v\":\"1\",\"event\":\"x.ok\",\"params\":{}}"
==# XOk
From 43e233f0eb9100d9610e05ac50e855c47c8dc86c Mon Sep 17 00:00:00 2001
From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
Date: Tue, 5 Sep 2023 20:15:50 +0400
Subject: [PATCH 2/6] core: don't create direct connections in group (#2996)
---
docs/protocol/diagrams/group.mmd | 31 +-
docs/protocol/diagrams/group.svg | 2 +-
src/Simplex/Chat.hs | 101 +++---
src/Simplex/Chat/Protocol.hs | 16 +-
src/Simplex/Chat/Store/Direct.hs | 8 +-
src/Simplex/Chat/Store/Groups.hs | 200 ++++++------
src/Simplex/Chat/Store/Shared.hs | 2 +
src/Simplex/Chat/Types.hs | 40 ++-
tests/Bots/DirectoryTests.hs | 112 +++----
tests/ChatClient.hs | 18 +-
tests/ChatTests/Files.hs | 11 +-
tests/ChatTests/Groups.hs | 152 +++++++--
tests/ChatTests/Profiles.hs | 528 ++++++++++++++++---------------
tests/ChatTests/Utils.hs | 6 +-
tests/ProtocolTests.hs | 20 +-
15 files changed, 723 insertions(+), 524 deletions(-)
diff --git a/docs/protocol/diagrams/group.mmd b/docs/protocol/diagrams/group.mmd
index c331b4610..18d392caa 100644
--- a/docs/protocol/diagrams/group.mmd
+++ b/docs/protocol/diagrams/group.mmd
@@ -3,24 +3,31 @@ sequenceDiagram
participant A as Alice
participant B as Bob
participant C as Existing
contact
-
+
note over A, B: 1. send and accept group invitation
A ->> B: x.grp.inv
invite Bob to group
(via contact connection)
- B ->> A: x.grp.acpt
accept invitation
(via member connection)
- B ->> A: establish group member connection
+ B ->> A: x.grp.acpt
accept invitation
(via member connection)
establish group member connection
note over M, B: 2. introduce new member Bob to all existing members
A ->> M: x.grp.mem.new
"announce" Bob
to existing members
(via member connections)
- A ->> B: x.grp.mem.intro * N
"introduce" members
(via member connection)
- B ->> A: x.grp.mem.inv * N
"invitations" to connect
for all members
(via member connection)
- A ->> M: x.grp.mem.fwd
forward "invitations"
to all members
(via member connections)
+ loop batched
+ A ->> B: x.grp.mem.intro * N
"introduce" members and
their chat protocol versions
(via member connection)
+ note over B: prepare group member connections
+ opt chat protocol compatible version < 2
+ note over B: prepare direct connections
+ end
+ B ->> A: x.grp.mem.inv * N
"invitations" to connect
for all members
(via member connection)
+ end
+ A ->> M: x.grp.mem.fwd
forward "invitations" and
Bob's chat protocol version
to all members
(via member connections)
note over M, B: 3. establish direct and group member connections
M ->> B: establish group member connection
- M ->> B: establish direct connection
- note over M, C: 4. deduplicate new contact
- B ->> M: x.info.probe
"probe" is sent to all new members
- B ->> C: x.info.probe.check
"probe" hash,
in case contact and
member profiles match
- C ->> B: x.info.probe.ok
original "probe",
in case contact and member
are the same user
- note over B: merge existing and new contacts if received and sent probe hashes match
+ opt chat protocol compatible version < 2
+ M ->> B: establish direct connection
+ note over M, C: 4. deduplicate new contact
+ B ->> M: x.info.probe
"probe" is sent to all new members
+ B ->> C: x.info.probe.check
"probe" hash,
in case contact and
member profiles match
+ C ->> B: x.info.probe.ok
original "probe",
in case contact and member
are the same user
+ note over B: merge existing and new contacts if received and sent probe hashes match
+ end
diff --git a/docs/protocol/diagrams/group.svg b/docs/protocol/diagrams/group.svg
index d66b560b2..8c1b65dee 100644
--- a/docs/protocol/diagrams/group.svg
+++ b/docs/protocol/diagrams/group.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs
index 1c353f7e8..1165a2947 100644
--- a/src/Simplex/Chat.hs
+++ b/src/Simplex/Chat.hs
@@ -94,6 +94,7 @@ import qualified Simplex.Messaging.Protocol as SMP
import qualified Simplex.Messaging.TMap as TM
import Simplex.Messaging.Transport.Client (defaultSocksProxy)
import Simplex.Messaging.Util
+import Simplex.Messaging.Version
import System.Exit (exitFailure, exitSuccess)
import System.FilePath (combine, splitExtensions, takeFileName, (>))
import System.IO (Handle, IOMode (..), SeekMode (..), hFlush, stdout)
@@ -104,7 +105,6 @@ import UnliftIO.Concurrent (forkFinally, forkIO, mkWeakThreadId, threadDelay)
import UnliftIO.Directory
import UnliftIO.IO (hClose, hSeek, hTell, openFile)
import UnliftIO.STM
-import Simplex.Messaging.Version
defaultChatConfig :: ChatConfig
defaultChatConfig =
@@ -1431,12 +1431,16 @@ processChatCommand = \case
Nothing -> throwChatError $ CEGroupCantResendInvitation gInfo cName
| otherwise -> throwChatError $ CEGroupDuplicateMember cName
APIJoinGroup groupId -> withUser $ \user@User {userId} -> do
- ReceivedGroupInvitation {fromMember, connRequest, groupInfo = g@GroupInfo {membership}} <- withStore $ \db -> getGroupInvitation db user groupId
+ (invitation, ct) <- withStore $ \db -> do
+ inv@ReceivedGroupInvitation {fromMember} <- getGroupInvitation db user groupId
+ (inv,) <$> getContactViaMember db user fromMember
+ let ReceivedGroupInvitation {fromMember, connRequest, groupInfo = g@GroupInfo {membership}} = invitation
+ Contact {activeConn = Connection {connChatVRange}} = ct
withChatLock "joinGroup" . procCmd $ do
dm <- directMessage $ XGrpAcpt (memberId (membership :: GroupMember))
agentConnId <- withAgent $ \a -> joinConnection a (aUserId user) True connRequest dm
withStore' $ \db -> do
- createMemberConnection db userId fromMember agentConnId
+ createMemberConnection db userId fromMember agentConnId connChatVRange
updateGroupMemberStatus db userId fromMember GSMemAccepted
updateGroupMemberStatus db userId membership GSMemAccepted
updateCIGroupInvitationStatus user
@@ -1878,11 +1882,11 @@ processChatCommand = \case
mergedProfile' = userProfileToSend user' Nothing $ Just ct'
if mergedProfile' == mergedProfile
then pure s {notChanged = notChanged + 1}
- else
- let cts' = if mergedPreferences ct == mergedPreferences ct' then cts else ct' : cts
+ else
+ let cts' = if mergedPreferences ct == mergedPreferences ct' then cts else ct' : cts
in (notifyContact mergedProfile' ct' $> s {updateSuccesses = updateSuccesses + 1, changedContacts = cts'})
`catchChatError` \e -> when (ll <= CLLInfo) (toView $ CRChatError (Just user) e) $> s {updateFailures = updateFailures + 1, changedContacts = cts'}
- where
+ where
notifyContact mergedProfile' ct' = do
void $ sendDirectContactMessage ct' (XInfo mergedProfile')
when (directOrUsed ct') $ createSndFeatureItems user' ct ct'
@@ -2825,7 +2829,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
_ -> Nothing
processDirectMessage :: ACommand 'Agent e -> ConnectionEntity -> Connection -> Maybe Contact -> m ()
- processDirectMessage agentMsg connEntity conn@Connection {connId, viaUserContactLink, groupLinkId, customUserProfileId, connectionCode} = \case
+ processDirectMessage agentMsg connEntity conn@Connection {connId, connChatVRange, viaUserContactLink, groupLinkId, customUserProfileId, connectionCode} = \case
Nothing -> case agentMsg of
CONF confId _ connInfo -> do
-- [incognito] send saved profile
@@ -2866,7 +2870,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
setConnConnReqInv db user connId cReq
getXGrpMemIntroContDirect db user ct
forM_ contData $ \(hostConnId, xGrpMemIntroCont) ->
- sendXGrpMemInv hostConnId directConnReq xGrpMemIntroCont
+ sendXGrpMemInv hostConnId (Just directConnReq) xGrpMemIntroCont
CRContactUri _ -> throwChatError $ CECommandError "unexpected ConnectionRequestUri type"
MSG msgMeta _msgFlags msgBody -> do
cmdId <- createAckCmd conn
@@ -2948,7 +2952,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
forM_ groupId_ $ \groupId -> do
gVar <- asks idsDrg
groupConnIds <- createAgentConnectionAsync user CFCreateConnGrpInv True SCMInvitation
- withStore $ \db -> createNewContactMemberAsync db gVar user groupId ct gLinkMemRole groupConnIds
+ withStore $ \db -> createNewContactMemberAsync db gVar user groupId ct gLinkMemRole groupConnIds connChatVRange
_ -> pure ()
Just (gInfo@GroupInfo {membership}, m@GroupMember {activeConn}) ->
when (maybe False ((== ConnReady) . connStatus) activeConn) $ do
@@ -3015,22 +3019,32 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
case cReq of
groupConnReq@(CRInvitationUri _ _) -> case cmdFunction of
-- [async agent commands] XGrpMemIntro continuation on receiving INV
- CFCreateConnGrpMemInv -> do
- contData <- withStore' $ \db -> do
- setConnConnReqInv db user connId cReq
- getXGrpMemIntroContGroup db user m
- forM_ contData $ \(hostConnId, directConnReq) -> do
- let GroupMember {groupMemberId, memberId} = m
- sendXGrpMemInv hostConnId directConnReq XGrpMemIntroCont {groupId, groupMemberId, memberId, groupConnReq}
+ CFCreateConnGrpMemInv ->
+ ifM
+ (featureVersionSupported (connChatVRange conn) groupNoDirectVersion)
+ sendWithoutDirectCReq
+ sendWithDirectCReq
+ where
+ sendWithoutDirectCReq = do
+ let GroupMember {groupMemberId, memberId} = m
+ hostConnId <- withStore $ \db -> do
+ liftIO $ setConnConnReqInv db user connId cReq
+ getHostConnId db user groupId
+ sendXGrpMemInv hostConnId Nothing XGrpMemIntroCont {groupId, groupMemberId, memberId, groupConnReq}
+ sendWithDirectCReq = do
+ let GroupMember {groupMemberId, memberId} = m
+ contData <- withStore' $ \db -> do
+ setConnConnReqInv db user connId cReq
+ getXGrpMemIntroContGroup db user m
+ forM_ contData $ \(hostConnId, directConnReq) ->
+ sendXGrpMemInv hostConnId (Just directConnReq) XGrpMemIntroCont {groupId, groupMemberId, memberId, groupConnReq}
-- [async agent commands] group link auto-accept continuation on receiving INV
- CFCreateConnGrpInv ->
- withStore' (\db -> getContactViaMember db user m) >>= \case
- Nothing -> messageError "implementation error: invitee does not have contact"
- Just ct -> do
- withStore' $ \db -> setNewContactMemberConnRequest db user m cReq
- groupLinkId <- withStore' $ \db -> getGroupLinkId db user gInfo
- sendGrpInvitation ct m groupLinkId
- toView $ CRSentGroupInvitation user gInfo ct m
+ CFCreateConnGrpInv -> do
+ ct <- withStore $ \db -> getContactViaMember db user m
+ withStore' $ \db -> setNewContactMemberConnRequest db user m cReq
+ groupLinkId <- withStore' $ \db -> getGroupLinkId db user gInfo
+ sendGrpInvitation ct m groupLinkId
+ toView $ CRSentGroupInvitation user gInfo ct m
where
sendGrpInvitation :: Contact -> GroupMember -> Maybe GroupLinkId -> m ()
sendGrpInvitation ct GroupMember {memberId, memberRole = memRole} groupLinkId = do
@@ -3106,7 +3120,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
processIntro intro `catchChatError` (toView . CRChatError (Just user))
where
processIntro intro@GroupMemberIntro {introId} = do
- void $ sendDirectMessage conn (XGrpMemIntro . memberInfo $ reMember intro) (GroupId groupId)
+ void $ sendDirectMessage conn (XGrpMemIntro $ memberInfo (reMember intro)) (GroupId groupId)
withStore' $ \db -> updateIntroStatus db introId GMIntroSent
_ -> do
-- TODO send probe and decide whether to use existing contact connection or the new contact connection
@@ -4006,7 +4020,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
processGroupInvitation :: Contact -> GroupInvitation -> RcvMessage -> MsgMeta -> m ()
processGroupInvitation ct inv msg msgMeta = do
- let Contact {localDisplayName = c, activeConn = Connection {customUserProfileId, groupLinkId = groupLinkId'}} = ct
+ let Contact {localDisplayName = c, activeConn = Connection {connChatVRange, customUserProfileId, groupLinkId = groupLinkId'}} = ct
GroupInvitation {fromMember = (MemberIdRole fromMemId fromRole), invitedMember = (MemberIdRole memId memRole), connRequest, groupLinkId} = inv
checkIntegrityCreateItem (CDDirectRcv ct) msgMeta
when (fromRole < GRAdmin || fromRole < memRole) $ throwChatError (CEGroupContactRole c)
@@ -4017,7 +4031,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
then do
connIds <- joinAgentConnectionAsync user True connRequest =<< directMessage (XGrpAcpt memberId)
withStore' $ \db -> do
- createMemberConnectionAsync db user hostId connIds
+ createMemberConnectionAsync db user hostId connIds connChatVRange
updateGroupMemberStatusById db userId hostId GSMemAccepted
updateGroupMemberStatus db userId membership GSMemAccepted
toView $ CRUserAcceptedGroupSent user gInfo {membership = membership {memberStatus = GSMemAccepted}} (Just ct)
@@ -4230,7 +4244,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
_ -> pure conn'
xGrpMemNew :: GroupInfo -> GroupMember -> MemberInfo -> RcvMessage -> MsgMeta -> m ()
- xGrpMemNew gInfo m memInfo@(MemberInfo memId memRole memberProfile) msg msgMeta = do
+ xGrpMemNew gInfo m memInfo@(MemberInfo memId memRole _ memberProfile) msg msgMeta = do
checkHostRole m memRole
members <- withStore' $ \db -> getGroupMembers db user gInfo
unless (sameMemberId memId $ membership gInfo) $
@@ -4243,7 +4257,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
toView $ CRJoinedGroupMemberConnecting user gInfo m newMember
xGrpMemIntro :: GroupInfo -> GroupMember -> MemberInfo -> m ()
- xGrpMemIntro gInfo@GroupInfo {membership, chatSettings = ChatSettings {enableNtfs}} m@GroupMember {memberRole, localDisplayName = c} memInfo@(MemberInfo memId _ _) = do
+ xGrpMemIntro gInfo@GroupInfo {membership, chatSettings = ChatSettings {enableNtfs}} m@GroupMember {memberRole, localDisplayName = c} memInfo@(MemberInfo memId _ memberChatVRange _) = do
case memberCategory m of
GCHostMember -> do
members <- withStore' $ \db -> getGroupMembers db user gInfo
@@ -4252,14 +4266,21 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
else do
when (memberRole < GRAdmin) $ throwChatError (CEGroupContactRole c)
-- [async agent commands] commands should be asynchronous, continuation is to send XGrpMemInv - have to remember one has completed and process on second
- groupConnIds <- createAgentConnectionAsync user CFCreateConnGrpMemInv enableNtfs SCMInvitation
- directConnIds <- createAgentConnectionAsync user CFCreateConnGrpMemInv enableNtfs SCMInvitation
- -- [incognito] direct connection with member has to be established using the same incognito profile [that was known to host and used for group membership]
+ groupConnIds <- createConn
+ directConnIds <- case memberChatVRange of
+ Nothing -> Just <$> createConn
+ Just mcvr ->
+ ifM
+ (featureVersionSupported (fromChatVRange mcvr) groupNoDirectVersion)
+ (pure Nothing)
+ (Just <$> createConn)
let customUserProfileId = if memberIncognito membership then Just (localProfileId $ memberProfile membership) else Nothing
void $ withStore $ \db -> createIntroReMember db user gInfo m memInfo groupConnIds directConnIds customUserProfileId
_ -> messageError "x.grp.mem.intro can be only sent by host member"
+ where
+ createConn = createAgentConnectionAsync user CFCreateConnGrpMemInv enableNtfs SCMInvitation
- sendXGrpMemInv :: Int64 -> ConnReqInvitation -> XGrpMemIntroCont -> m ()
+ sendXGrpMemInv :: Int64 -> Maybe ConnReqInvitation -> XGrpMemIntroCont -> m ()
sendXGrpMemInv hostConnId directConnReq XGrpMemIntroCont {groupId, groupMemberId, memberId, groupConnReq} = do
hostConn <- withStore $ \db -> getConnectionById db user hostConnId
let msg = XGrpMemInv memberId IntroInvitation {groupConnReq, directConnReq}
@@ -4280,7 +4301,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
_ -> messageError "x.grp.mem.inv can be only sent by invitee member"
xGrpMemFwd :: GroupInfo -> GroupMember -> MemberInfo -> IntroInvitation -> m ()
- xGrpMemFwd gInfo@GroupInfo {membership, chatSettings = ChatSettings {enableNtfs}} m memInfo@(MemberInfo memId memRole _) introInv@IntroInvitation {groupConnReq, directConnReq} = do
+ xGrpMemFwd gInfo@GroupInfo {membership, chatSettings = ChatSettings {enableNtfs}} m memInfo@(MemberInfo memId memRole memberChatVRange _) introInv@IntroInvitation {groupConnReq, directConnReq} = do
checkHostRole m memRole
members <- withStore' $ \db -> getGroupMembers db user gInfo
toMember <- case find (sameMemberId memId) members of
@@ -4295,9 +4316,10 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
dm <- directMessage $ XGrpMemInfo (memberId (membership :: GroupMember)) (fromLocalProfile $ memberProfile membership)
-- [async agent commands] no continuation needed, but commands should be asynchronous for stability
groupConnIds <- joinAgentConnectionAsync user enableNtfs groupConnReq dm
- directConnIds <- joinAgentConnectionAsync user enableNtfs directConnReq dm
+ directConnIds <- forM directConnReq $ \dcr -> joinAgentConnectionAsync user enableNtfs dcr dm
let customUserProfileId = if memberIncognito membership then Just (localProfileId $ memberProfile membership) else Nothing
- withStore' $ \db -> createIntroToMemberContact db user m toMember groupConnIds directConnIds customUserProfileId
+ mcvr = maybe chatInitialVRange fromChatVRange memberChatVRange
+ withStore' $ \db -> createIntroToMemberContact db user m toMember mcvr groupConnIds directConnIds customUserProfileId
xGrpMemRole :: GroupInfo -> GroupMember -> MemberId -> GroupMemberRole -> RcvMessage -> MsgMeta -> m ()
xGrpMemRole gInfo@GroupInfo {membership} m@GroupMember {memberRole = senderRole} memId memRole msg msgMeta
@@ -4444,6 +4466,13 @@ updateConnChatVRange conn@Connection {connId, connChatVRange} msgChatVRange
pure conn {connChatVRange = msgChatVRange}
| otherwise = pure conn
+featureVersionSupported :: ChatMonad' m => VersionRange -> Version -> m Bool
+featureVersionSupported peerVRange v = do
+ ChatConfig {chatVRange} <- asks config
+ case chatVRange `compatibleVersion` peerVRange of
+ Just (Compatible v') -> pure $ v' >= v
+ Nothing -> pure False
+
parseFileDescription :: (ChatMonad m, FilePartyI p) => Text -> m (ValidFileDescription p)
parseFileDescription =
liftEither . first (ChatError . CEInvalidFileDescription) . (strDecode . encodeUtf8)
diff --git a/src/Simplex/Chat/Protocol.hs b/src/Simplex/Chat/Protocol.hs
index 5c33eb06c..2f93ab13d 100644
--- a/src/Simplex/Chat/Protocol.hs
+++ b/src/Simplex/Chat/Protocol.hs
@@ -54,6 +54,10 @@ currentChatVersion = 2
supportedChatVRange :: VersionRange
supportedChatVRange = mkVersionRange 1 currentChatVersion
+-- version that starts support for skipping establishing direct connections in a group
+groupNoDirectVersion :: Version
+groupNoDirectVersion = 2
+
data ConnectionEntity
= RcvDirectMsgConnection {entityConnection :: Connection, contact :: Maybe Contact}
| RcvGroupMsgConnection {entityConnection :: Connection, groupInfo :: GroupInfo, groupMember :: GroupMember}
@@ -107,18 +111,6 @@ data AppMessage (e :: MsgEncoding) where
AMJson :: AppMessageJson -> AppMessage 'Json
AMBinary :: AppMessageBinary -> AppMessage 'Binary
-newtype ChatVersionRange = ChatVersionRange {fromChatVRange :: VersionRange} deriving (Eq, Show)
-
-chatInitialVRange :: VersionRange
-chatInitialVRange = versionToRange 1
-
-instance FromJSON ChatVersionRange where
- parseJSON v = ChatVersionRange <$> strParseJSON "ChatVersionRange" v
-
-instance ToJSON ChatVersionRange where
- toJSON (ChatVersionRange vr) = strToJSON vr
- toEncoding (ChatVersionRange vr) = strToJEncoding vr
-
-- chat message is sent as JSON with these properties
data AppMessageJson = AppMessageJson
{ v :: Maybe ChatVersionRange,
diff --git a/src/Simplex/Chat/Store/Direct.hs b/src/Simplex/Chat/Store/Direct.hs
index 00d3d55c9..3b56e57b7 100644
--- a/src/Simplex/Chat/Store/Direct.hs
+++ b/src/Simplex/Chat/Store/Direct.hs
@@ -498,10 +498,10 @@ createOrUpdateContactRequest db user@User {userId} userContactLinkId invId (Vers
<$> DB.execute
db
[sql|
- UPDATE contact_requests
- SET agent_invitation_id = ?, chat_vrange_min_version = ?, chat_vrange_max_version = ?, updated_at = ?
- WHERE user_id = ? AND contact_request_id = ?
- |]
+ UPDATE contact_requests
+ SET agent_invitation_id = ?, chat_vrange_min_version = ?, chat_vrange_max_version = ?, updated_at = ?
+ WHERE user_id = ? AND contact_request_id = ?
+ |]
(invId, minV, maxV, currentTs, userId, cReqId)
else withLocalDisplayName db userId displayName $ \ldn ->
Right <$> do
diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs
index 6c2f32f76..40c21d616 100644
--- a/src/Simplex/Chat/Store/Groups.hs
+++ b/src/Simplex/Chat/Store/Groups.hs
@@ -83,6 +83,7 @@ module Simplex.Chat.Store.Groups
updateGroupSettings,
getXGrpMemIntroContDirect,
getXGrpMemIntroContGroup,
+ getHostConnId,
)
where
@@ -98,7 +99,6 @@ import Database.SQLite.Simple.QQ (sql)
import Simplex.Chat.Messages
import Simplex.Chat.Store.Direct
import Simplex.Chat.Store.Shared
-import Simplex.Chat.Protocol (chatInitialVRange)
import Simplex.Chat.Types
import Simplex.Chat.Types.Preferences
import Simplex.Messaging.Agent.Protocol (ConnId, UserId)
@@ -106,6 +106,7 @@ import Simplex.Messaging.Agent.Store.SQLite (firstRow, maybeFirstRow)
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Util (eitherToMaybe)
+import Simplex.Messaging.Version
import UnliftIO.STM
type GroupInfoRow = (Int64, GroupName, GroupName, Text, Maybe Text, Maybe ImageData, Maybe ProfileId, Maybe Bool, Maybe Bool, Bool, Maybe GroupPreferences) :. (UTCTime, UTCTime, Maybe UTCTime) :. GroupMemberRow
@@ -481,20 +482,21 @@ getUserGroupsWithSummary db user _contactId_ search_ =
-- the statuses on non-current members should match memberCurrent' function
getGroupSummary :: DB.Connection -> User -> GroupId -> IO GroupSummary
getGroupSummary db User {userId} groupId = do
- currentMembers_ <- maybeFirstRow fromOnly $
- DB.query
- db
- [sql|
- SELECT count (m.group_member_id)
- FROM groups g
- JOIN group_members m USING (group_id)
- WHERE g.user_id = ?
- AND g.group_id = ?
- AND m.member_status != ?
- AND m.member_status != ?
- AND m.member_status != ?
- |]
- (userId, groupId, GSMemRemoved, GSMemLeft, GSMemInvited)
+ currentMembers_ <-
+ maybeFirstRow fromOnly $
+ DB.query
+ db
+ [sql|
+ SELECT count (m.group_member_id)
+ FROM groups g
+ JOIN group_members m USING (group_id)
+ WHERE g.user_id = ?
+ AND g.group_id = ?
+ AND m.member_status != ?
+ AND m.member_status != ?
+ AND m.member_status != ?
+ |]
+ (userId, groupId, GSMemRemoved, GSMemLeft, GSMemInvited)
pure GroupSummary {currentMembers = fromMaybe 0 currentMembers_}
getContactGroupPreferences :: DB.Connection -> User -> Contact -> IO [FullGroupPreferences]
@@ -613,11 +615,11 @@ getGroupInvitation db user groupId =
DB.query db "SELECT g.inv_queue_info FROM groups g WHERE g.group_id = ? AND g.user_id = ?" (groupId, userId)
createNewContactMember :: DB.Connection -> TVar ChaChaDRG -> User -> GroupId -> Contact -> GroupMemberRole -> ConnId -> ConnReqInvitation -> ExceptT StoreError IO GroupMember
-createNewContactMember db gVar User {userId, userContactId} groupId Contact {contactId, localDisplayName, profile} memberRole agentConnId connRequest =
+createNewContactMember db gVar User {userId, userContactId} groupId Contact {contactId, localDisplayName, profile, activeConn = Connection {connChatVRange}} memberRole agentConnId connRequest =
createWithRandomId gVar $ \memId -> do
createdAt <- liftIO getCurrentTime
member@GroupMember {groupMemberId} <- createMember_ (MemberId memId) createdAt
- void $ createMemberConnection_ db userId groupMemberId agentConnId Nothing 0 createdAt
+ void $ createMemberConnection_ db userId groupMemberId agentConnId connChatVRange Nothing 0 createdAt
pure member
where
createMember_ memberId createdAt = do
@@ -652,13 +654,13 @@ createNewContactMember db gVar User {userId, userContactId} groupId Contact {con
:. (userId, localDisplayName, contactId, localProfileId profile, connRequest, createdAt, createdAt)
)
-createNewContactMemberAsync :: DB.Connection -> TVar ChaChaDRG -> User -> GroupId -> Contact -> GroupMemberRole -> (CommandId, ConnId) -> ExceptT StoreError IO ()
-createNewContactMemberAsync db gVar user@User {userId, userContactId} groupId Contact {contactId, localDisplayName, profile} memberRole (cmdId, agentConnId) =
+createNewContactMemberAsync :: DB.Connection -> TVar ChaChaDRG -> User -> GroupId -> Contact -> GroupMemberRole -> (CommandId, ConnId) -> VersionRange -> ExceptT StoreError IO ()
+createNewContactMemberAsync db gVar user@User {userId, userContactId} groupId Contact {contactId, localDisplayName, profile} memberRole (cmdId, agentConnId) connChatVRange =
createWithRandomId gVar $ \memId -> do
createdAt <- liftIO getCurrentTime
insertMember_ (MemberId memId) createdAt
groupMemberId <- liftIO $ insertedRowId db
- Connection {connId} <- createMemberConnection_ db userId groupMemberId agentConnId Nothing 0 createdAt
+ Connection {connId} <- createMemberConnection_ db userId groupMemberId agentConnId connChatVRange Nothing 0 createdAt
setCommandConnId db user cmdId connId
where
insertMember_ memberId createdAt =
@@ -674,31 +676,32 @@ createNewContactMemberAsync db gVar user@User {userId, userContactId} groupId Co
:. (userId, localDisplayName, contactId, localProfileId profile, createdAt, createdAt)
)
-getContactViaMember :: DB.Connection -> User -> GroupMember -> IO (Maybe Contact)
+getContactViaMember :: DB.Connection -> User -> GroupMember -> ExceptT StoreError IO Contact
getContactViaMember db user@User {userId} GroupMember {groupMemberId} =
- maybeFirstRow (toContact user) $
- DB.query
- db
- [sql|
- 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.enable_ntfs, ct.send_rcpts, ct.favorite,
- cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts,
- -- 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.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.auth_err_counter,
- c.chat_vrange_min_version, c.chat_vrange_max_version
- FROM contacts ct
- JOIN contact_profiles cp ON cp.contact_profile_id = ct.contact_profile_id
- JOIN connections c ON c.connection_id = (
- SELECT max(cc.connection_id)
- FROM connections cc
- where cc.contact_id = ct.contact_id
- )
- JOIN group_members m ON m.contact_id = ct.contact_id
- WHERE ct.user_id = ? AND m.group_member_id = ? AND ct.deleted = 0
- |]
- (userId, groupMemberId)
+ ExceptT $
+ firstRow (toContact user) (SEContactNotFoundByMemberId groupMemberId) $
+ DB.query
+ db
+ [sql|
+ 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.enable_ntfs, ct.send_rcpts, ct.favorite,
+ cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts,
+ -- 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.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.auth_err_counter,
+ c.chat_vrange_min_version, c.chat_vrange_max_version
+ FROM contacts ct
+ JOIN contact_profiles cp ON cp.contact_profile_id = ct.contact_profile_id
+ JOIN connections c ON c.connection_id = (
+ SELECT max(cc.connection_id)
+ FROM connections cc
+ where cc.contact_id = ct.contact_id
+ )
+ JOIN group_members m ON m.contact_id = ct.contact_id
+ WHERE ct.user_id = ? AND m.group_member_id = ? AND ct.deleted = 0
+ |]
+ (userId, groupMemberId)
setNewContactMemberConnRequest :: DB.Connection -> User -> GroupMember -> ConnReqInvitation -> IO ()
setNewContactMemberConnRequest db User {userId} GroupMember {groupMemberId} connRequest = do
@@ -710,15 +713,15 @@ getMemberInvitation db User {userId} groupMemberId =
fmap join . maybeFirstRow fromOnly $
DB.query db "SELECT sent_inv_queue_info FROM group_members WHERE group_member_id = ? AND user_id = ?" (groupMemberId, userId)
-createMemberConnection :: DB.Connection -> UserId -> GroupMember -> ConnId -> IO ()
-createMemberConnection db userId GroupMember {groupMemberId} agentConnId = do
+createMemberConnection :: DB.Connection -> UserId -> GroupMember -> ConnId -> VersionRange -> IO ()
+createMemberConnection db userId GroupMember {groupMemberId} agentConnId connChatVRange = do
currentTs <- getCurrentTime
- void $ createMemberConnection_ db userId groupMemberId agentConnId Nothing 0 currentTs
+ void $ createMemberConnection_ db userId groupMemberId agentConnId connChatVRange Nothing 0 currentTs
-createMemberConnectionAsync :: DB.Connection -> User -> GroupMemberId -> (CommandId, ConnId) -> IO ()
-createMemberConnectionAsync db user@User {userId} groupMemberId (cmdId, agentConnId) = do
+createMemberConnectionAsync :: DB.Connection -> User -> GroupMemberId -> (CommandId, ConnId) -> VersionRange -> IO ()
+createMemberConnectionAsync db user@User {userId} groupMemberId (cmdId, agentConnId) connChatVRange = do
currentTs <- getCurrentTime
- Connection {connId} <- createMemberConnection_ db userId groupMemberId agentConnId Nothing 0 currentTs
+ Connection {connId} <- createMemberConnection_ db userId groupMemberId agentConnId connChatVRange Nothing 0 currentTs
setCommandConnId db user cmdId connId
updateGroupMemberStatus :: DB.Connection -> UserId -> GroupMember -> GroupMemberStatus -> IO ()
@@ -738,25 +741,30 @@ updateGroupMemberStatusById db userId groupMemberId memStatus = do
-- | add new member with profile
createNewGroupMember :: DB.Connection -> User -> GroupInfo -> MemberInfo -> GroupMemberCategory -> GroupMemberStatus -> ExceptT StoreError IO GroupMember
-createNewGroupMember db user@User {userId} gInfo memInfo@(MemberInfo _ _ Profile {displayName, fullName, image, contactLink, preferences}) memCategory memStatus =
- ExceptT . withLocalDisplayName db userId displayName $ \localDisplayName -> do
- currentTs <- getCurrentTime
+createNewGroupMember db user gInfo memInfo memCategory memStatus = do
+ currentTs <- liftIO getCurrentTime
+ (localDisplayName, memProfileId) <- createNewMemberProfile_ db user memInfo currentTs
+ let newMember =
+ NewGroupMember
+ { memInfo,
+ memCategory,
+ memStatus,
+ memInvitedBy = IBUnknown,
+ localDisplayName,
+ memContactId = Nothing,
+ memProfileId
+ }
+ liftIO $ createNewMember_ db user gInfo newMember currentTs
+
+createNewMemberProfile_ :: DB.Connection -> User -> MemberInfo -> UTCTime -> ExceptT StoreError IO (Text, ProfileId)
+createNewMemberProfile_ db User {userId} (MemberInfo _ _ _ Profile {displayName, fullName, image, contactLink, preferences}) createdAt =
+ ExceptT . withLocalDisplayName db userId displayName $ \ldn -> do
DB.execute
db
"INSERT INTO contact_profiles (display_name, full_name, image, contact_link, user_id, preferences, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)"
- (displayName, fullName, image, contactLink, userId, preferences, currentTs, currentTs)
- memProfileId <- insertedRowId db
- let newMember =
- NewGroupMember
- { memInfo,
- memCategory,
- memStatus,
- memInvitedBy = IBUnknown,
- localDisplayName,
- memContactId = Nothing,
- memProfileId
- }
- Right <$> createNewMember_ db user gInfo newMember currentTs
+ (displayName, fullName, image, contactLink, userId, preferences, createdAt, createdAt)
+ profileId <- insertedRowId db
+ pure $ Right (ldn, profileId)
createNewMember_ :: DB.Connection -> User -> GroupInfo -> NewGroupMember -> UTCTime -> IO GroupMember
createNewMember_
@@ -764,7 +772,7 @@ createNewMember_
User {userId, userContactId}
GroupInfo {groupId}
NewGroupMember
- { memInfo = MemberInfo memberId memberRole memberProfile,
+ { memInfo = MemberInfo memberId memberRole _ memberProfile,
memCategory = memberCategory,
memStatus = memberStatus,
memInvitedBy = invitedBy,
@@ -908,43 +916,41 @@ getIntroduction_ db reMember toMember = ExceptT $ do
where
toIntro :: [(Int64, Maybe ConnReqInvitation, Maybe ConnReqInvitation, GroupMemberIntroStatus)] -> Either StoreError GroupMemberIntro
toIntro [(introId, groupConnReq, directConnReq, introStatus)] =
- let introInvitation = IntroInvitation <$> groupConnReq <*> directConnReq
+ let introInvitation = IntroInvitation <$> groupConnReq <*> pure directConnReq
in Right GroupMemberIntro {introId, reMember, toMember, introStatus, introInvitation}
toIntro _ = Left SEIntroNotFound
-createIntroReMember :: DB.Connection -> User -> GroupInfo -> GroupMember -> MemberInfo -> (CommandId, ConnId) -> (CommandId, ConnId) -> Maybe ProfileId -> ExceptT StoreError IO GroupMember
-createIntroReMember db user@User {userId} gInfo@GroupInfo {groupId} _host@GroupMember {memberContactId, activeConn} memInfo@(MemberInfo _ _ memberProfile) (groupCmdId, groupAgentConnId) (directCmdId, directAgentConnId) customUserProfileId = do
- let cLevel = 1 + maybe 0 (connLevel :: Connection -> Int) activeConn
+createIntroReMember :: DB.Connection -> User -> GroupInfo -> GroupMember -> MemberInfo -> (CommandId, ConnId) -> Maybe (CommandId, ConnId) -> Maybe ProfileId -> ExceptT StoreError IO GroupMember
+createIntroReMember db user@User {userId} gInfo@GroupInfo {groupId} _host@GroupMember {memberContactId, activeConn} memInfo@(MemberInfo _ _ memberChatVRange memberProfile) (groupCmdId, groupAgentConnId) directConnIds customUserProfileId = do
+ let mcvr = maybe chatInitialVRange fromChatVRange memberChatVRange
+ cLevel = 1 + maybe 0 (connLevel :: Connection -> Int) activeConn
currentTs <- liftIO getCurrentTime
- Connection {connId = directConnId} <- liftIO $ createConnection_ db userId ConnContact Nothing directAgentConnId chatInitialVRange memberContactId Nothing customUserProfileId cLevel currentTs
- liftIO $ setCommandConnId db user directCmdId directConnId
- (localDisplayName, contactId, memProfileId) <- createContact_ db userId directConnId memberProfile "" (Just groupId) currentTs Nothing
+ newMember <- case directConnIds of
+ Just (directCmdId, directAgentConnId) -> do
+ Connection {connId = directConnId} <- liftIO $ createConnection_ db userId ConnContact Nothing directAgentConnId mcvr memberContactId Nothing customUserProfileId cLevel currentTs
+ liftIO $ setCommandConnId db user directCmdId directConnId
+ (localDisplayName, contactId, memProfileId) <- createContact_ db userId directConnId memberProfile "" (Just groupId) currentTs Nothing
+ pure $ NewGroupMember {memInfo, memCategory = GCPreMember, memStatus = GSMemIntroduced, memInvitedBy = IBUnknown, localDisplayName, memContactId = Just contactId, memProfileId}
+ Nothing -> do
+ (localDisplayName, memProfileId) <- createNewMemberProfile_ db user memInfo currentTs
+ pure $ NewGroupMember {memInfo, memCategory = GCPreMember, memStatus = GSMemIntroduced, memInvitedBy = IBUnknown, localDisplayName, memContactId = Nothing, memProfileId}
liftIO $ do
- let newMember =
- NewGroupMember
- { memInfo,
- memCategory = GCPreMember,
- memStatus = GSMemIntroduced,
- memInvitedBy = IBUnknown,
- localDisplayName,
- memContactId = Just contactId,
- memProfileId
- }
member <- createNewMember_ db user gInfo newMember currentTs
- conn@Connection {connId = groupConnId} <- createMemberConnection_ db userId (groupMemberId' member) groupAgentConnId memberContactId cLevel currentTs
+ conn@Connection {connId = groupConnId} <- createMemberConnection_ db userId (groupMemberId' member) groupAgentConnId mcvr memberContactId cLevel currentTs
liftIO $ setCommandConnId db user groupCmdId groupConnId
pure (member :: GroupMember) {activeConn = Just conn}
-createIntroToMemberContact :: DB.Connection -> User -> GroupMember -> GroupMember -> (CommandId, ConnId) -> (CommandId, ConnId) -> Maybe ProfileId -> IO ()
-createIntroToMemberContact db user@User {userId} GroupMember {memberContactId = viaContactId, activeConn} _to@GroupMember {groupMemberId, localDisplayName} (groupCmdId, groupAgentConnId) (directCmdId, directAgentConnId) customUserProfileId = do
+createIntroToMemberContact :: DB.Connection -> User -> GroupMember -> GroupMember -> VersionRange -> (CommandId, ConnId) -> Maybe (CommandId, ConnId) -> Maybe ProfileId -> IO ()
+createIntroToMemberContact db user@User {userId} GroupMember {memberContactId = viaContactId, activeConn} _to@GroupMember {groupMemberId, localDisplayName} mcvr (groupCmdId, groupAgentConnId) directConnIds customUserProfileId = do
let cLevel = 1 + maybe 0 (connLevel :: Connection -> Int) activeConn
currentTs <- getCurrentTime
- Connection {connId = groupConnId} <- createMemberConnection_ db userId groupMemberId groupAgentConnId viaContactId cLevel currentTs
+ Connection {connId = groupConnId} <- createMemberConnection_ db userId groupMemberId groupAgentConnId mcvr viaContactId cLevel currentTs
setCommandConnId db user groupCmdId groupConnId
- Connection {connId = directConnId} <- createConnection_ db userId ConnContact Nothing directAgentConnId chatInitialVRange viaContactId Nothing customUserProfileId cLevel currentTs
- setCommandConnId db user directCmdId directConnId
- contactId <- createMemberContact_ directConnId currentTs
- updateMember_ contactId currentTs
+ forM_ directConnIds $ \(directCmdId, directAgentConnId) -> do
+ Connection {connId = directConnId} <- createConnection_ db userId ConnContact Nothing directAgentConnId mcvr viaContactId Nothing customUserProfileId cLevel currentTs
+ setCommandConnId db user directCmdId directConnId
+ contactId <- createMemberContact_ directConnId currentTs
+ updateMember_ contactId currentTs
where
createMemberContact_ :: Int64 -> UTCTime -> IO Int64
createMemberContact_ connId ts = do
@@ -971,8 +977,8 @@ createIntroToMemberContact db user@User {userId} GroupMember {memberContactId =
|]
[":contact_id" := contactId, ":updated_at" := ts, ":group_member_id" := groupMemberId]
-createMemberConnection_ :: DB.Connection -> UserId -> Int64 -> ConnId -> Maybe Int64 -> Int -> UTCTime -> IO Connection
-createMemberConnection_ db userId groupMemberId agentConnId viaContact = createConnection_ db userId ConnMember (Just groupMemberId) agentConnId chatInitialVRange viaContact Nothing Nothing
+createMemberConnection_ :: DB.Connection -> UserId -> Int64 -> ConnId -> VersionRange -> Maybe Int64 -> Int -> UTCTime -> IO Connection
+createMemberConnection_ db userId groupMemberId agentConnId connChatVRange viaContact = createConnection_ db userId ConnMember (Just groupMemberId) agentConnId connChatVRange viaContact Nothing Nothing
getViaGroupMember :: DB.Connection -> User -> Contact -> IO (Maybe (GroupInfo, GroupMember))
getViaGroupMember db User {userId, userContactId} Contact {contactId} =
@@ -1343,3 +1349,9 @@ getXGrpMemIntroContGroup db User {userId} GroupMember {groupMemberId} = do
toCont (hostConnId, connReq_) = case connReq_ of
Just connReq -> Just (hostConnId, connReq)
_ -> Nothing
+
+getHostConnId :: DB.Connection -> User -> GroupId -> ExceptT StoreError IO GroupMemberId
+getHostConnId db user@User {userId} groupId = do
+ hostMemberId <- getHostMemberId_ db user groupId
+ ExceptT . firstRow fromOnly (SEConnectionNotFoundByMemberId hostMemberId) $
+ DB.query db "SELECT connection_id FROM connections WHERE user_id = ? AND group_member_id = ?" (userId, hostMemberId)
diff --git a/src/Simplex/Chat/Store/Shared.hs b/src/Simplex/Chat/Store/Shared.hs
index 5bae79d80..48e2e5692 100644
--- a/src/Simplex/Chat/Store/Shared.hs
+++ b/src/Simplex/Chat/Store/Shared.hs
@@ -51,6 +51,7 @@ data StoreError
| SEUserNotFoundByContactRequestId {contactRequestId :: Int64}
| SEContactNotFound {contactId :: ContactId}
| SEContactNotFoundByName {contactName :: ContactName}
+ | SEContactNotFoundByMemberId {groupMemberId :: GroupMemberId}
| SEContactNotReady {contactName :: ContactName}
| SEDuplicateContactLink
| SEUserContactLinkNotFound
@@ -78,6 +79,7 @@ data StoreError
| SERcvFileNotFoundXFTP {agentRcvFileId :: AgentRcvFileId}
| SEConnectionNotFound {agentConnId :: AgentConnId}
| SEConnectionNotFoundById {connId :: Int64}
+ | SEConnectionNotFoundByMemberId {groupMemberId :: GroupMemberId}
| SEPendingConnectionNotFound {connId :: Int64}
| SEIntroNotFound
| SEUniqueID
diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs
index ac19cbc36..4981b225b 100644
--- a/src/Simplex/Chat/Types.hs
+++ b/src/Simplex/Chat/Types.hs
@@ -347,11 +347,12 @@ data ChatSettings = ChatSettings
instance ToJSON ChatSettings where toEncoding = J.genericToEncoding J.defaultOptions
defaultChatSettings :: ChatSettings
-defaultChatSettings = ChatSettings
- { enableNtfs = True,
- sendRcpts = Nothing,
- favorite = False
- }
+defaultChatSettings =
+ ChatSettings
+ { enableNtfs = True,
+ sendRcpts = Nothing,
+ favorite = False
+ }
pattern DisableNtfs :: ChatSettings
pattern DisableNtfs <- ChatSettings {enableNtfs = False}
@@ -538,24 +539,31 @@ instance ToJSON MemberIdRole where toEncoding = J.genericToEncoding J.defaultOpt
data IntroInvitation = IntroInvitation
{ groupConnReq :: ConnReqInvitation,
- directConnReq :: ConnReqInvitation
+ directConnReq :: Maybe ConnReqInvitation
}
deriving (Eq, Show, Generic, FromJSON)
-instance ToJSON IntroInvitation where toEncoding = J.genericToEncoding J.defaultOptions
+instance ToJSON IntroInvitation where
+ toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True}
+ toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True}
data MemberInfo = MemberInfo
{ memberId :: MemberId,
memberRole :: GroupMemberRole,
+ v :: Maybe ChatVersionRange,
profile :: Profile
}
deriving (Eq, Show, Generic, FromJSON)
-instance ToJSON MemberInfo where toEncoding = J.genericToEncoding J.defaultOptions
+instance ToJSON MemberInfo where
+ toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True}
+ toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True}
memberInfo :: GroupMember -> MemberInfo
-memberInfo GroupMember {memberId, memberRole, memberProfile} =
- MemberInfo memberId memberRole (fromLocalProfile memberProfile)
+memberInfo GroupMember {memberId, memberRole, memberProfile, activeConn} =
+ MemberInfo memberId memberRole memberChatVRange (fromLocalProfile memberProfile)
+ where
+ memberChatVRange = ChatVersionRange . connChatVRange <$> activeConn
data ReceivedGroupInvitation = ReceivedGroupInvitation
{ fromMember :: GroupMember,
@@ -1467,3 +1475,15 @@ instance ProtocolTypeI p => ToJSON (ServerCfg p) where
instance ProtocolTypeI p => FromJSON (ServerCfg p) where
parseJSON = J.genericParseJSON J.defaultOptions {J.omitNothingFields = True}
+
+newtype ChatVersionRange = ChatVersionRange {fromChatVRange :: VersionRange} deriving (Eq, Show)
+
+chatInitialVRange :: VersionRange
+chatInitialVRange = versionToRange 1
+
+instance FromJSON ChatVersionRange where
+ parseJSON v = ChatVersionRange <$> strParseJSON "ChatVersionRange" v
+
+instance ToJSON ChatVersionRange where
+ toJSON (ChatVersionRange vr) = strToJSON vr
+ toEncoding (ChatVersionRange vr) = strToJEncoding vr
diff --git a/tests/Bots/DirectoryTests.hs b/tests/Bots/DirectoryTests.hs
index 21bdb6577..a0d69d195 100644
--- a/tests/Bots/DirectoryTests.hs
+++ b/tests/Bots/DirectoryTests.hs
@@ -13,13 +13,14 @@ import Control.Monad (forM_)
import Directory.Options
import Directory.Service
import Directory.Store
+import GHC.IO.Handle (hClose)
import Simplex.Chat.Bot.KnownContacts
+import Simplex.Chat.Controller (ChatConfig (..))
import Simplex.Chat.Core
import Simplex.Chat.Options (ChatOpts (..), CoreChatOpts (..))
import Simplex.Chat.Types (GroupMemberRole (..), Profile (..))
import System.FilePath ((>))
import Test.Hspec
-import GHC.IO.Handle (hClose)
directoryServiceTests :: SpecWith FilePath
directoryServiceTests = do
@@ -232,10 +233,10 @@ testJoinGroup tmp =
dan <## "bob (Bob): contact is connected"
dan <## "#privacy: you joined the group"
dan <# ("#privacy bob> " <> welcomeMsg)
- dan <###
- [ "#privacy: member SimpleX-Directory is connected",
- "#privacy: member cath (Catherine) is connected"
- ],
+ dan
+ <### [ "#privacy: member SimpleX-Directory is connected",
+ "#privacy: member cath (Catherine) is connected"
+ ],
do
cath <## "#privacy: bob added dan (Daniel) to the group (connecting...)"
cath <## "#privacy: new member dan is connected"
@@ -243,9 +244,9 @@ testJoinGroup tmp =
testDelistedOwnerLeaves :: HasCallStack => FilePath -> IO ()
testDelistedOwnerLeaves tmp =
- withDirectoryService tmp $ \superUser dsLink ->
- withNewTestChat tmp "bob" bobProfile $ \bob ->
- withNewTestChat tmp "cath" cathProfile $ \cath -> do
+ withDirectoryServiceCfg tmp testCfgCreateGroupDirect $ \superUser dsLink ->
+ withNewTestChatCfg tmp testCfgCreateGroupDirect "bob" bobProfile $ \bob ->
+ withNewTestChatCfg tmp testCfgCreateGroupDirect "cath" cathProfile $ \cath -> do
bob `connectVia` dsLink
registerGroup superUser bob "privacy" "Privacy"
addCathAsOwner bob cath
@@ -259,9 +260,9 @@ testDelistedOwnerLeaves tmp =
testDelistedOwnerRemoved :: HasCallStack => FilePath -> IO ()
testDelistedOwnerRemoved tmp =
- withDirectoryService tmp $ \superUser dsLink ->
- withNewTestChat tmp "bob" bobProfile $ \bob ->
- withNewTestChat tmp "cath" cathProfile $ \cath -> do
+ withDirectoryServiceCfg tmp testCfgCreateGroupDirect $ \superUser dsLink ->
+ withNewTestChatCfg tmp testCfgCreateGroupDirect "bob" bobProfile $ \bob ->
+ withNewTestChatCfg tmp testCfgCreateGroupDirect "cath" cathProfile $ \cath -> do
bob `connectVia` dsLink
registerGroup superUser bob "privacy" "Privacy"
addCathAsOwner bob cath
@@ -274,9 +275,9 @@ testDelistedOwnerRemoved tmp =
testNotDelistedMemberLeaves :: HasCallStack => FilePath -> IO ()
testNotDelistedMemberLeaves tmp =
- withDirectoryService tmp $ \superUser dsLink ->
- withNewTestChat tmp "bob" bobProfile $ \bob ->
- withNewTestChat tmp "cath" cathProfile $ \cath -> do
+ withDirectoryServiceCfg tmp testCfgCreateGroupDirect $ \superUser dsLink ->
+ withNewTestChatCfg tmp testCfgCreateGroupDirect "bob" bobProfile $ \bob ->
+ withNewTestChatCfg tmp testCfgCreateGroupDirect "cath" cathProfile $ \cath -> do
bob `connectVia` dsLink
registerGroup superUser bob "privacy" "Privacy"
addCathAsOwner bob cath
@@ -286,10 +287,10 @@ testNotDelistedMemberLeaves tmp =
groupFound cath "privacy"
testNotDelistedMemberRemoved :: HasCallStack => FilePath -> IO ()
-testNotDelistedMemberRemoved tmp =
- withDirectoryService tmp $ \superUser dsLink ->
- withNewTestChat tmp "bob" bobProfile $ \bob ->
- withNewTestChat tmp "cath" cathProfile $ \cath -> do
+testNotDelistedMemberRemoved tmp =
+ withDirectoryServiceCfg tmp testCfgCreateGroupDirect $ \superUser dsLink ->
+ withNewTestChatCfg tmp testCfgCreateGroupDirect "bob" bobProfile $ \bob ->
+ withNewTestChatCfg tmp testCfgCreateGroupDirect "cath" cathProfile $ \cath -> do
bob `connectVia` dsLink
registerGroup superUser bob "privacy" "Privacy"
addCathAsOwner bob cath
@@ -299,9 +300,9 @@ testNotDelistedMemberRemoved tmp =
testDelistedServiceRemoved :: HasCallStack => FilePath -> IO ()
testDelistedServiceRemoved tmp =
- withDirectoryService tmp $ \superUser dsLink ->
- withNewTestChat tmp "bob" bobProfile $ \bob ->
- withNewTestChat tmp "cath" cathProfile $ \cath -> do
+ withDirectoryServiceCfg tmp testCfgCreateGroupDirect $ \superUser dsLink ->
+ withNewTestChatCfg tmp testCfgCreateGroupDirect "bob" bobProfile $ \bob ->
+ withNewTestChatCfg tmp testCfgCreateGroupDirect "cath" cathProfile $ \cath -> do
bob `connectVia` dsLink
registerGroup superUser bob "privacy" "Privacy"
addCathAsOwner bob cath
@@ -316,9 +317,9 @@ testDelistedServiceRemoved tmp =
testDelistedRoleChanges :: HasCallStack => FilePath -> IO ()
testDelistedRoleChanges tmp =
- withDirectoryService tmp $ \superUser dsLink ->
- withNewTestChat tmp "bob" bobProfile $ \bob ->
- withNewTestChat tmp "cath" cathProfile $ \cath -> do
+ withDirectoryServiceCfg tmp testCfgCreateGroupDirect $ \superUser dsLink ->
+ withNewTestChatCfg tmp testCfgCreateGroupDirect "bob" bobProfile $ \bob ->
+ withNewTestChatCfg tmp testCfgCreateGroupDirect "cath" cathProfile $ \cath -> do
bob `connectVia` dsLink
registerGroup superUser bob "privacy" "Privacy"
addCathAsOwner bob cath
@@ -362,9 +363,9 @@ testDelistedRoleChanges tmp =
testNotDelistedMemberRoleChanged :: HasCallStack => FilePath -> IO ()
testNotDelistedMemberRoleChanged tmp =
- withDirectoryService tmp $ \superUser dsLink ->
- withNewTestChat tmp "bob" bobProfile $ \bob ->
- withNewTestChat tmp "cath" cathProfile $ \cath -> do
+ withDirectoryServiceCfg tmp testCfgCreateGroupDirect $ \superUser dsLink ->
+ withNewTestChatCfg tmp testCfgCreateGroupDirect "bob" bobProfile $ \bob ->
+ withNewTestChatCfg tmp testCfgCreateGroupDirect "cath" cathProfile $ \cath -> do
bob `connectVia` dsLink
registerGroup superUser bob "privacy" "Privacy"
addCathAsOwner bob cath
@@ -426,9 +427,9 @@ testNotApprovedBadRoles tmp =
testRegOwnerChangedProfile :: HasCallStack => FilePath -> IO ()
testRegOwnerChangedProfile tmp =
- withDirectoryService tmp $ \superUser dsLink ->
- withNewTestChat tmp "bob" bobProfile $ \bob ->
- withNewTestChat tmp "cath" cathProfile $ \cath -> do
+ withDirectoryServiceCfg tmp testCfgCreateGroupDirect $ \superUser dsLink ->
+ withNewTestChatCfg tmp testCfgCreateGroupDirect "bob" bobProfile $ \bob ->
+ withNewTestChatCfg tmp testCfgCreateGroupDirect "cath" cathProfile $ \cath -> do
bob `connectVia` dsLink
registerGroup superUser bob "privacy" "Privacy"
addCathAsOwner bob cath
@@ -445,9 +446,9 @@ testRegOwnerChangedProfile tmp =
testAnotherOwnerChangedProfile :: HasCallStack => FilePath -> IO ()
testAnotherOwnerChangedProfile tmp =
- withDirectoryService tmp $ \superUser dsLink ->
- withNewTestChat tmp "bob" bobProfile $ \bob ->
- withNewTestChat tmp "cath" cathProfile $ \cath -> do
+ withDirectoryServiceCfg tmp testCfgCreateGroupDirect $ \superUser dsLink ->
+ withNewTestChatCfg tmp testCfgCreateGroupDirect "bob" bobProfile $ \bob ->
+ withNewTestChatCfg tmp testCfgCreateGroupDirect "cath" cathProfile $ \cath -> do
bob `connectVia` dsLink
registerGroup superUser bob "privacy" "Privacy"
addCathAsOwner bob cath
@@ -464,9 +465,9 @@ testAnotherOwnerChangedProfile tmp =
testRegOwnerRemovedLink :: HasCallStack => FilePath -> IO ()
testRegOwnerRemovedLink tmp =
- withDirectoryService tmp $ \superUser dsLink ->
- withNewTestChat tmp "bob" bobProfile $ \bob ->
- withNewTestChat tmp "cath" cathProfile $ \cath -> do
+ withDirectoryServiceCfg tmp testCfgCreateGroupDirect $ \superUser dsLink ->
+ withNewTestChatCfg tmp testCfgCreateGroupDirect "bob" bobProfile $ \bob ->
+ withNewTestChatCfg tmp testCfgCreateGroupDirect "cath" cathProfile $ \cath -> do
bob `connectVia` dsLink
registerGroup superUser bob "privacy" "Privacy"
addCathAsOwner bob cath
@@ -497,9 +498,9 @@ testRegOwnerRemovedLink tmp =
testAnotherOwnerRemovedLink :: HasCallStack => FilePath -> IO ()
testAnotherOwnerRemovedLink tmp =
- withDirectoryService tmp $ \superUser dsLink ->
- withNewTestChat tmp "bob" bobProfile $ \bob ->
- withNewTestChat tmp "cath" cathProfile $ \cath -> do
+ withDirectoryServiceCfg tmp testCfgCreateGroupDirect $ \superUser dsLink ->
+ withNewTestChatCfg tmp testCfgCreateGroupDirect "bob" bobProfile $ \bob ->
+ withNewTestChatCfg tmp testCfgCreateGroupDirect "cath" cathProfile $ \cath -> do
bob `connectVia` dsLink
registerGroup superUser bob "privacy" "Privacy"
addCathAsOwner bob cath
@@ -646,9 +647,9 @@ testDuplicateProhibitApproval tmp =
testListUserGroups :: HasCallStack => FilePath -> IO ()
testListUserGroups tmp =
- withDirectoryService tmp $ \superUser dsLink ->
- withNewTestChat tmp "bob" bobProfile $ \bob ->
- withNewTestChat tmp "cath" cathProfile $ \cath -> do
+ withDirectoryServiceCfg tmp testCfgCreateGroupDirect $ \superUser dsLink ->
+ withNewTestChatCfg tmp testCfgCreateGroupDirect "bob" bobProfile $ \bob ->
+ withNewTestChatCfg tmp testCfgCreateGroupDirect "cath" cathProfile $ \cath -> do
bob `connectVia` dsLink
cath `connectVia` dsLink
registerGroup superUser bob "privacy" "Privacy"
@@ -686,15 +687,15 @@ testRestoreDirectory tmp = do
withTestChat tmp "bob" $ \bob ->
withTestChat tmp "cath" $ \cath -> do
bob <## "2 contacts connected (use /cs for the list)"
- bob <###
- [ "#privacy (Privacy): connected to server(s)",
- "#security (Security): connected to server(s)"
- ]
+ bob
+ <### [ "#privacy (Privacy): connected to server(s)",
+ "#security (Security): connected to server(s)"
+ ]
cath <## "2 contacts connected (use /cs for the list)"
- cath <###
- [ "#privacy (Privacy): connected to server(s)",
- "#anonymity (Anonymity): connected to server(s)"
- ]
+ cath
+ <### [ "#privacy (Privacy): connected to server(s)",
+ "#anonymity (Anonymity): connected to server(s)"
+ ]
listGroups superUser bob cath
groupFoundN 3 bob "privacy"
groupFound bob "security"
@@ -784,10 +785,13 @@ addCathAsOwner bob cath = do
cath <## "#privacy: member SimpleX-Directory is connected"
withDirectoryService :: HasCallStack => FilePath -> (TestCC -> String -> IO ()) -> IO ()
-withDirectoryService tmp test = do
+withDirectoryService tmp = withDirectoryServiceCfg tmp testCfg
+
+withDirectoryServiceCfg :: HasCallStack => FilePath -> ChatConfig -> (TestCC -> String -> IO ()) -> IO ()
+withDirectoryServiceCfg tmp cfg test = do
dsLink <-
- withNewTestChat tmp serviceDbPrefix directoryProfile $ \ds ->
- withNewTestChat tmp "super_user" aliceProfile $ \superUser -> do
+ withNewTestChatCfg tmp cfg serviceDbPrefix directoryProfile $ \ds ->
+ withNewTestChatCfg tmp cfg "super_user" aliceProfile $ \superUser -> do
connectUsers ds superUser
ds ##> "/ad"
getContactLink ds True
@@ -800,7 +804,7 @@ restoreDirectoryService tmp ctCount grCount test = do
ds <## (show ctCount <> " contacts connected (use /cs for the list)")
ds <## "Your address is active! To show: /sa"
ds <## (show grCount <> " group links active")
- forM_ [1..grCount] $ \_ -> ds <##. "#"
+ forM_ [1 .. grCount] $ \_ -> ds <##. "#"
ds ##> "/sa"
dsLink <- getContactLink ds False
ds <## "auto_accept on"
diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs
index e612f3d09..43760c99b 100644
--- a/tests/ChatClient.hs
+++ b/tests/ChatClient.hs
@@ -22,6 +22,7 @@ import Simplex.Chat
import Simplex.Chat.Controller (ChatConfig (..), ChatController (..), ChatDatabase (..), ChatLogLevel (..))
import Simplex.Chat.Core
import Simplex.Chat.Options
+import Simplex.Chat.Protocol (groupNoDirectVersion)
import Simplex.Chat.Store
import Simplex.Chat.Store.Profiles
import Simplex.Chat.Terminal
@@ -133,6 +134,16 @@ testAgentCfgV1 =
testCfgV1 :: ChatConfig
testCfgV1 = testCfg {agentConfig = testAgentCfgV1}
+testCfgCreateGroupDirect :: ChatConfig
+testCfgCreateGroupDirect =
+ mkCfgCreateGroupDirect testCfg
+
+mkCfgCreateGroupDirect :: ChatConfig -> ChatConfig
+mkCfgCreateGroupDirect cfg = cfg {chatVRange = groupCreateDirectVRange}
+
+groupCreateDirectVRange :: VersionRange
+groupCreateDirectVRange = mkVersionRange 1 (groupNoDirectVersion - 1)
+
createTestChat :: FilePath -> ChatConfig -> ChatOpts -> String -> Profile -> IO TestCC
createTestChat tmp cfg opts@ChatOpts {coreOptions = CoreChatOpts {dbKey}} dbPrefix profile = do
Right db@ChatDatabase {chatStore} <- createChatDatabase (tmp > dbPrefix) dbKey MCError
@@ -249,7 +260,7 @@ getTermLine cc =
Just s -> do
-- remove condition to always echo virtual terminal
when (printOutput cc) $ do
- -- when True $ do
+ -- when True $ do
name <- userName cc
putStrLn $ name <> ": " <> s
pure s
@@ -288,7 +299,10 @@ testChatCfgOpts3 cfg opts p1 p2 p3 test = testChatN cfg opts [p1, p2, p3] test_
test_ _ = error "expected 3 chat clients"
testChat4 :: HasCallStack => Profile -> Profile -> Profile -> Profile -> (HasCallStack => TestCC -> TestCC -> TestCC -> TestCC -> IO ()) -> FilePath -> IO ()
-testChat4 p1 p2 p3 p4 test = testChatN testCfg testOpts [p1, p2, p3, p4] test_
+testChat4 = testChatCfg4 testCfg
+
+testChatCfg4 :: HasCallStack => ChatConfig -> Profile -> Profile -> Profile -> Profile -> (HasCallStack => TestCC -> TestCC -> TestCC -> TestCC -> IO ()) -> FilePath -> IO ()
+testChatCfg4 cfg p1 p2 p3 p4 test = testChatN cfg testOpts [p1, p2, p3, p4] test_
where
test_ :: HasCallStack => [TestCC] -> IO ()
test_ [tc1, tc2, tc3, tc4] = test tc1 tc2 tc3 tc4
diff --git a/tests/ChatTests/Files.hs b/tests/ChatTests/Files.hs
index 4343b547c..9c277e00e 100644
--- a/tests/ChatTests/Files.hs
+++ b/tests/ChatTests/Files.hs
@@ -46,7 +46,7 @@ chatFileTests = do
it "files folder: sender deleted file during transfer" testFilesFoldersImageSndDelete
it "files folder: recipient deleted file during transfer" testFilesFoldersImageRcvDelete
it "send and receive image with text and quote" testSendImageWithTextAndQuote
- describe "send and receive image to group" testGroupSendImage
+ it "send and receive image to group" testGroupSendImage
it "send and receive image with text and quote to group" testGroupSendImageWithTextAndQuote
describe "async sending and receiving files" $ do
-- fails on CI
@@ -724,11 +724,10 @@ testSendImageWithTextAndQuote =
(alice <## "completed sending file 3 (test.jpg) to bob")
B.readFile "./tests/tmp/test_1.jpg" `shouldReturn` src
-testGroupSendImage :: SpecWith FilePath
-testGroupSendImage = versionTestMatrix3 runTestGroupSendImage
- where
- runTestGroupSendImage :: HasCallStack => TestCC -> TestCC -> TestCC -> IO ()
- runTestGroupSendImage alice bob cath = do
+testGroupSendImage :: HasCallStack => FilePath -> IO ()
+testGroupSendImage =
+ testChat3 aliceProfile bobProfile cathProfile $
+ \alice bob cath -> do
createGroup3 "team" alice bob cath
threadDelay 1000000
alice ##> "/_send #1 json {\"filePath\": \"./tests/fixtures/test.jpg\", \"msgContent\": {\"text\":\"\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}"
diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs
index 7a4bb2061..e533a55da 100644
--- a/tests/ChatTests/Groups.hs
+++ b/tests/ChatTests/Groups.hs
@@ -10,8 +10,10 @@ import Control.Concurrent.Async (concurrently_)
import Control.Monad (when)
import qualified Data.Text as T
import Simplex.Chat.Controller (ChatConfig (..))
+import Simplex.Chat.Protocol (supportedChatVRange)
import Simplex.Chat.Store (agentStoreFile, chatStoreFile)
import Simplex.Chat.Types (GroupMemberRole (..))
+import Simplex.Messaging.Version
import System.Directory (copyFile)
import System.FilePath ((>))
import Test.Hspec
@@ -19,7 +21,7 @@ import Test.Hspec
chatGroupTests :: SpecWith FilePath
chatGroupTests = do
describe "chat groups" $ do
- describe "add contacts, create group and send/receive messages" testGroup
+ it "add contacts, create group and send/receive messages" testGroup
it "add contacts, create group and send/receive messages, check messages" testGroupCheckMessages
it "create and join group with 4 members" testGroup2
it "create and delete group" testGroupDelete
@@ -64,15 +66,54 @@ chatGroupTests = do
describe "group delivery receipts" $ do
it "should send delivery receipts in group" testSendGroupDeliveryReceipts
it "should send delivery receipts in group depending on configuration" testConfigureGroupDeliveryReceipts
-
-testGroup :: HasCallStack => SpecWith FilePath
-testGroup = versionTestMatrix3 runTestGroup
+ describe "direct connections in group are not established based on chat protocol version" $ do
+ describe "3 members group" $ do
+ testNoDirect _0 _0 True
+ testNoDirect _0 _1 False
+ testNoDirect _1 _0 False
+ testNoDirect _1 _1 False
+ describe "4 members group" $ do
+ testNoDirect4 _0 _0 _0 True True True
+ testNoDirect4 _0 _0 _1 True False False
+ testNoDirect4 _0 _1 _0 False True False
+ testNoDirect4 _0 _1 _1 False False False
+ testNoDirect4 _1 _0 _0 False False True
+ testNoDirect4 _1 _0 _1 False False False
+ testNoDirect4 _1 _1 _0 False False False
+ testNoDirect4 _1 _1 _1 False False False
where
- runTestGroup alice bob cath = testGroupShared alice bob cath False
+ _0 = supportedChatVRange -- don't create direct connections
+ _1 = groupCreateDirectVRange
+ -- having host configured with older version doesn't have effect in tests
+ -- because host uses current code and sends version in MemberInfo
+ testNoDirect vrMem2 vrMem3 noConns =
+ it
+ ( "host " <> vRangeStr supportedChatVRange
+ <> (", 2nd mem " <> vRangeStr vrMem2)
+ <> (", 3rd mem " <> vRangeStr vrMem3)
+ <> (if noConns then " : 2 3" else " : 2 <##> 3")
+ )
+ $ testNoGroupDirectConns supportedChatVRange vrMem2 vrMem3 noConns
+ testNoDirect4 vrMem2 vrMem3 vrMem4 noConns23 noConns24 noConns34 =
+ it
+ ( "host " <> vRangeStr supportedChatVRange
+ <> (", 2nd mem " <> vRangeStr vrMem2)
+ <> (", 3rd mem " <> vRangeStr vrMem3)
+ <> (", 4th mem " <> vRangeStr vrMem4)
+ <> (if noConns23 then " : 2 3" else " : 2 <##> 3")
+ <> (if noConns24 then " : 2 4" else " : 2 <##> 4")
+ <> (if noConns34 then " : 3 4" else " : 3 <##> 4")
+ )
+ $ testNoGroupDirectConns4Members supportedChatVRange vrMem2 vrMem3 vrMem4 noConns23 noConns24 noConns34
+
+testGroup :: HasCallStack => FilePath -> IO ()
+testGroup =
+ testChatCfg3 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile $
+ \alice bob cath -> testGroupShared alice bob cath False
testGroupCheckMessages :: HasCallStack => FilePath -> IO ()
testGroupCheckMessages =
- testChat3 aliceProfile bobProfile cathProfile $
+ testChatCfg3 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile $
\alice bob cath -> testGroupShared alice bob cath True
testGroupShared :: HasCallStack => TestCC -> TestCC -> TestCC -> Bool -> IO ()
@@ -233,7 +274,7 @@ testGroupShared alice bob cath checkMessages = do
testGroup2 :: HasCallStack => FilePath -> IO ()
testGroup2 =
- testChat4 aliceProfile bobProfile cathProfile danProfile $
+ testChatCfg4 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile danProfile $
\alice bob cath dan -> do
connectUsers alice bob
connectUsers alice cath
@@ -679,7 +720,7 @@ testDeleteGroupMemberProfileKept =
testGroupRemoveAdd :: HasCallStack => FilePath -> IO ()
testGroupRemoveAdd =
- testChat3 aliceProfile bobProfile cathProfile $
+ testChatCfg3 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile $
\alice bob cath -> do
createGroup3 "team" alice bob cath
-- remove member
@@ -754,7 +795,7 @@ testGroupList =
testGroupMessageQuotedReply :: HasCallStack => FilePath -> IO ()
testGroupMessageQuotedReply =
- testChat3 aliceProfile bobProfile cathProfile $
+ testChatCfg3 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile $
\alice bob cath -> do
createGroup3 "team" alice bob cath
threadDelay 1000000
@@ -1232,7 +1273,7 @@ testGroupDeleteUnusedContacts =
cath <## "alice (Alice)"
cath `hasContactProfiles` ["alice", "cath"]
where
- cfg = testCfg {initialCleanupManagerDelay = 0, cleanupManagerInterval = 1, cleanupManagerStepDelay = 0}
+ cfg = mkCfgCreateGroupDirect $ testCfg {initialCleanupManagerDelay = 0, cleanupManagerInterval = 1, cleanupManagerStepDelay = 0}
deleteGroup :: HasCallStack => TestCC -> TestCC -> TestCC -> String -> IO ()
deleteGroup alice bob cath group = do
alice ##> ("/d #" <> group)
@@ -1321,7 +1362,7 @@ testGroupDescription = testChat4 aliceProfile bobProfile cathProfile danProfile
testGroupModerate :: HasCallStack => FilePath -> IO ()
testGroupModerate =
- testChat3 aliceProfile bobProfile cathProfile $
+ testChatCfg3 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile $
\alice bob cath -> do
createGroup3 "team" alice bob cath
alice ##> "/mr team cath member"
@@ -1352,7 +1393,7 @@ testGroupModerate =
testGroupModerateFullDelete :: HasCallStack => FilePath -> IO ()
testGroupModerateFullDelete =
- testChat3 aliceProfile bobProfile cathProfile $
+ testChatCfg3 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile $
\alice bob cath -> do
createGroup3 "team" alice bob cath
alice ##> "/mr team cath member"
@@ -1390,10 +1431,10 @@ testGroupModerateFullDelete =
testGroupDelayedModeration :: HasCallStack => FilePath -> IO ()
testGroupDelayedModeration tmp = do
- withNewTestChat tmp "alice" aliceProfile $ \alice -> do
- withNewTestChat tmp "bob" bobProfile $ \bob -> do
+ withNewTestChatCfg tmp cfg "alice" aliceProfile $ \alice -> do
+ withNewTestChatCfg tmp cfg "bob" bobProfile $ \bob -> do
createGroup2 "team" alice bob
- withNewTestChat tmp "cath" cathProfile $ \cath -> do
+ withNewTestChatCfg tmp cfg "cath" cathProfile $ \cath -> do
connectUsers alice cath
addMember "team" alice cath GRMember
cath ##> "/j team"
@@ -1407,11 +1448,11 @@ testGroupDelayedModeration tmp = do
alice ##> "\\\\ #team @cath hi"
alice <## "message marked deleted by you"
cath <# "#team cath> [marked deleted by alice] hi"
- withTestChat tmp "bob" $ \bob -> do
+ withTestChatCfg tmp cfg "bob" $ \bob -> do
bob <## "1 contacts connected (use /cs for the list)"
bob <## "#team: connected to server(s)"
bob <## "#team: alice added cath (Catherine) to the group (connecting...)"
- withTestChat tmp "cath" $ \cath -> do
+ withTestChatCfg tmp cfg "cath" $ \cath -> do
cath <## "2 contacts connected (use /cs for the list)"
cath <## "#team: connected to server(s)"
cath <## "#team: member bob (Bob) is connected"
@@ -1424,13 +1465,15 @@ testGroupDelayedModeration tmp = do
bob ##> "/_get chat #1 count=2"
r <- chat <$> getTermLine bob
r `shouldMatchList` [(0, "connected"), (0, "hi [marked deleted by alice]")]
+ where
+ cfg = testCfgCreateGroupDirect
testGroupDelayedModerationFullDelete :: HasCallStack => FilePath -> IO ()
testGroupDelayedModerationFullDelete tmp = do
- withNewTestChat tmp "alice" aliceProfile $ \alice -> do
- withNewTestChat tmp "bob" bobProfile $ \bob -> do
+ withNewTestChatCfg tmp cfg "alice" aliceProfile $ \alice -> do
+ withNewTestChatCfg tmp cfg "bob" bobProfile $ \bob -> do
createGroup2 "team" alice bob
- withNewTestChat tmp "cath" cathProfile $ \cath -> do
+ withNewTestChatCfg tmp cfg "cath" cathProfile $ \cath -> do
connectUsers alice cath
addMember "team" alice cath GRMember
cath ##> "/j team"
@@ -1452,14 +1495,14 @@ testGroupDelayedModerationFullDelete tmp = do
cath <## "alice updated group #team:"
cath <## "updated group preferences:"
cath <## "Full deletion: on"
- withTestChat tmp "bob" $ \bob -> do
+ withTestChatCfg tmp cfg "bob" $ \bob -> do
bob <## "1 contacts connected (use /cs for the list)"
bob <## "#team: connected to server(s)"
bob <## "#team: alice added cath (Catherine) to the group (connecting...)"
bob <## "alice updated group #team:"
bob <## "updated group preferences:"
bob <## "Full deletion: on"
- withTestChat tmp "cath" $ \cath -> do
+ withTestChatCfg tmp cfg "cath" $ \cath -> do
cath <## "2 contacts connected (use /cs for the list)"
cath <## "#team: connected to server(s)"
cath <## "#team: member bob (Bob) is connected"
@@ -1472,6 +1515,8 @@ testGroupDelayedModerationFullDelete tmp = do
bob ##> "/_get chat #1 count=3"
r <- chat <$> getTermLine bob
r `shouldMatchList` [(0, "Full deletion: on"), (0, "connected"), (0, "moderated [deleted by alice]")]
+ where
+ cfg = testCfgCreateGroupDirect
testGroupAsync :: HasCallStack => FilePath -> IO ()
testGroupAsync tmp = do
@@ -2127,7 +2172,7 @@ testGroupLinkMemberRole =
testGroupLinkLeaveDelete :: HasCallStack => FilePath -> IO ()
testGroupLinkLeaveDelete =
- testChat3 aliceProfile bobProfile cathProfile $
+ testChatCfg3 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile $
\alice bob cath -> do
connectUsers alice bob
connectUsers cath bob
@@ -2562,7 +2607,7 @@ testConfigureGroupDeliveryReceipts tmp =
receipt bob alice cath "team" "25"
noReceipt bob alice cath "club" "26"
where
- cfg = testCfg {showReceipts = True}
+ cfg = mkCfgCreateGroupDirect $ testCfg {showReceipts = True}
receipt cc1 cc2 cc3 gName msg = do
name1 <- userName cc1
cc1 #> ("#" <> gName <> " " <> msg)
@@ -2582,3 +2627,62 @@ testConfigureGroupDeliveryReceipts tmp =
cc2 <# ("#" <> gName <> " " <> name1 <> "> " <> msg)
cc3 <# ("#" <> gName <> " " <> name1 <> "> " <> msg)
cc1 / 50000
+
+testNoGroupDirectConns :: HasCallStack => VersionRange -> VersionRange -> VersionRange -> Bool -> FilePath -> IO ()
+testNoGroupDirectConns hostVRange mem2VRange mem3VRange noDirectConns tmp =
+ withNewTestChatCfg tmp testCfg {chatVRange = hostVRange} "alice" aliceProfile $ \alice -> do
+ withNewTestChatCfg tmp testCfg {chatVRange = mem2VRange} "bob" bobProfile $ \bob -> do
+ withNewTestChatCfg tmp testCfg {chatVRange = mem3VRange} "cath" cathProfile $ \cath -> do
+ createGroup3 "team" alice bob cath
+ if noDirectConns
+ then contactsDontExist bob cath
+ else bob <##> cath
+ where
+ contactsDontExist bob cath = do
+ bob ##> "@cath hi"
+ bob <## "no contact cath"
+ cath ##> "@bob hi"
+ cath <## "no contact bob"
+
+testNoGroupDirectConns4Members :: HasCallStack => VersionRange -> VersionRange -> VersionRange -> VersionRange -> Bool -> Bool -> Bool -> FilePath -> IO ()
+testNoGroupDirectConns4Members hostVRange mem2VRange mem3VRange mem4VRange noConns23 noConns24 noConns34 tmp =
+ withNewTestChatCfg tmp testCfg {chatVRange = hostVRange} "alice" aliceProfile $ \alice -> do
+ withNewTestChatCfg tmp testCfg {chatVRange = mem2VRange} "bob" bobProfile $ \bob -> do
+ withNewTestChatCfg tmp testCfg {chatVRange = mem3VRange} "cath" cathProfile $ \cath -> do
+ withNewTestChatCfg tmp testCfg {chatVRange = mem4VRange} "dan" danProfile $ \dan -> do
+ createGroup3 "team" alice bob cath
+ connectUsers alice dan
+ addMember "team" alice dan GRMember
+ dan ##> "/j team"
+ concurrentlyN_
+ [ alice <## "#team: dan joined the group",
+ do
+ dan <## "#team: you joined the group"
+ dan
+ <### [ "#team: member bob (Bob) is connected",
+ "#team: member cath (Catherine) is connected"
+ ],
+ aliceAddedDan bob,
+ aliceAddedDan cath
+ ]
+ if noConns23
+ then contactsDontExist bob cath
+ else bob <##> cath
+ if noConns24
+ then contactsDontExist bob dan
+ else bob <##> dan
+ if noConns34
+ then contactsDontExist cath dan
+ else cath <##> dan
+ where
+ aliceAddedDan :: HasCallStack => TestCC -> IO ()
+ aliceAddedDan cc = do
+ cc <## "#team: alice added dan (Daniel) to the group (connecting...)"
+ cc <## "#team: new member dan is connected"
+ contactsDontExist cc1 cc2 = do
+ name1 <- userName cc1
+ name2 <- userName cc2
+ cc1 ##> ("@" <> name2 <> " hi")
+ cc1 <## ("no contact " <> name2)
+ cc2 ##> ("@" <> name1 <> " hi")
+ cc2 <## ("no contact " <> name1)
diff --git a/tests/ChatTests/Profiles.hs b/tests/ChatTests/Profiles.hs
index c51202340..1a2b74f76 100644
--- a/tests/ChatTests/Profiles.hs
+++ b/tests/ChatTests/Profiles.hs
@@ -18,7 +18,7 @@ chatProfileTests = do
it "update user profile and notify contacts" testUpdateProfile
it "update user profile with image" testUpdateProfileImage
describe "user contact link" $ do
- describe "create and connect via contact link" testUserContactLink
+ it "create and connect via contact link" testUserContactLink
it "add contact link to profile" testProfileLink
it "auto accept contact requests" testUserContactLinkAutoAccept
it "deduplicate contact requests" testDeduplicateContactRequests
@@ -57,7 +57,7 @@ chatProfileTests = do
testUpdateProfile :: HasCallStack => FilePath -> IO ()
testUpdateProfile =
- testChat3 aliceProfile bobProfile cathProfile $
+ testChatCfg3 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile $
\alice bob cath -> do
createGroup3 "team" alice bob cath
alice ##> "/p"
@@ -117,33 +117,35 @@ testUpdateProfileImage =
bob <## "use @alice2 to send messages"
(bob )
-testUserContactLink :: SpecWith FilePath
-testUserContactLink = versionTestMatrix3 $ \alice bob cath -> do
- alice ##> "/ad"
- cLink <- getContactLink alice True
- bob ##> ("/c " <> cLink)
- alice <#? bob
- alice @@@ [("<@bob", "")]
- alice ##> "/ac bob"
- alice <## "bob (Bob): accepting contact request..."
- concurrently_
- (bob <## "alice (Alice): contact is connected")
- (alice <## "bob (Bob): contact is connected")
- threadDelay 100000
- alice @@@ [("@bob", lastChatFeature)]
- alice <##> bob
+testUserContactLink :: HasCallStack => FilePath -> IO ()
+testUserContactLink =
+ testChat3 aliceProfile bobProfile cathProfile $
+ \alice bob cath -> do
+ alice ##> "/ad"
+ cLink <- getContactLink alice True
+ bob ##> ("/c " <> cLink)
+ alice <#? bob
+ alice @@@ [("<@bob", "")]
+ alice ##> "/ac bob"
+ alice <## "bob (Bob): accepting contact request..."
+ concurrently_
+ (bob <## "alice (Alice): contact is connected")
+ (alice <## "bob (Bob): contact is connected")
+ threadDelay 100000
+ alice @@@ [("@bob", lastChatFeature)]
+ alice <##> bob
- cath ##> ("/c " <> cLink)
- alice <#? cath
- alice @@@ [("<@cath", ""), ("@bob", "hey")]
- alice ##> "/ac cath"
- alice <## "cath (Catherine): accepting contact request..."
- concurrently_
- (cath <## "alice (Alice): contact is connected")
- (alice <## "cath (Catherine): contact is connected")
- threadDelay 100000
- alice @@@ [("@cath", lastChatFeature), ("@bob", "hey")]
- alice <##> cath
+ cath ##> ("/c " <> cLink)
+ alice <#? cath
+ alice @@@ [("<@cath", ""), ("@bob", "hey")]
+ alice ##> "/ac cath"
+ alice <## "cath (Catherine): accepting contact request..."
+ concurrently_
+ (cath <## "alice (Alice): contact is connected")
+ (alice <## "cath (Catherine): contact is connected")
+ threadDelay 100000
+ alice @@@ [("@cath", lastChatFeature), ("@bob", "hey")]
+ alice <##> cath
testProfileLink :: HasCallStack => FilePath -> IO ()
testProfileLink =
@@ -762,192 +764,193 @@ testSetResetSetConnectionIncognito = testChat2 aliceProfile bobProfile $
bob `hasContactProfiles` ["bob", T.pack aliceIncognito]
testJoinGroupIncognito :: HasCallStack => FilePath -> IO ()
-testJoinGroupIncognito = testChat4 aliceProfile bobProfile cathProfile danProfile $
- \alice bob cath dan -> do
- -- non incognito connections
- connectUsers alice bob
- connectUsers alice dan
- connectUsers bob cath
- connectUsers bob dan
- connectUsers cath dan
- -- cath connected incognito to alice
- alice ##> "/c"
- inv <- getInvitation alice
- cath ##> ("/c i " <> inv)
- cath <## "confirmation sent!"
- cathIncognito <- getTermLine cath
- concurrentlyN_
- [ do
- cath <## ("alice (Alice): contact is connected, your incognito profile for this contact is " <> cathIncognito)
- cath <## "use /i alice to print out this incognito profile again",
- alice <## (cathIncognito <> ": contact is connected")
- ]
- -- alice creates group
- alice ##> "/g secret_club"
- alice <## "group #secret_club is created"
- alice <## "to add members use /a secret_club or /create link #secret_club"
- -- alice invites bob
- alice ##> "/a secret_club bob admin"
- concurrentlyN_
- [ alice <## "invitation to join the group #secret_club sent to bob",
- do
- bob <## "#secret_club: alice invites you to join the group as admin"
- bob <## "use /j secret_club to accept"
- ]
- bob ##> "/j secret_club"
- concurrently_
- (alice <## "#secret_club: bob joined the group")
- (bob <## "#secret_club: you joined the group")
- -- alice invites cath
- alice ##> ("/a secret_club " <> cathIncognito <> " admin")
- concurrentlyN_
- [ alice <## ("invitation to join the group #secret_club sent to " <> cathIncognito),
- do
- cath <## "#secret_club: alice invites you to join the group as admin"
- cath <## ("use /j secret_club to join incognito as " <> cathIncognito)
- ]
- -- cath uses the same incognito profile when joining group, cath and bob don't merge contacts
- cath ##> "/j secret_club"
- concurrentlyN_
- [ alice <## ("#secret_club: " <> cathIncognito <> " joined the group"),
- do
- cath <## ("#secret_club: you joined the group incognito as " <> cathIncognito)
- cath <## "#secret_club: member bob_1 (Bob) is connected",
- do
- bob <## ("#secret_club: alice added " <> cathIncognito <> " to the group (connecting...)")
- bob <## ("#secret_club: new member " <> cathIncognito <> " is connected")
- ]
- -- cath cannot invite to the group because her membership is incognito
- cath ##> "/a secret_club dan"
- cath <## "you've connected to this group using an incognito profile - prohibited to invite contacts"
- -- alice invites dan
- alice ##> "/a secret_club dan admin"
- concurrentlyN_
- [ alice <## "invitation to join the group #secret_club sent to dan",
- do
- dan <## "#secret_club: alice invites you to join the group as admin"
- dan <## "use /j secret_club to accept"
- ]
- dan ##> "/j secret_club"
- -- cath and dan don't merge contacts
- concurrentlyN_
- [ alice <## "#secret_club: dan joined the group",
- do
- dan <## "#secret_club: you joined the group"
- dan
- <### [ ConsoleString $ "#secret_club: member " <> cathIncognito <> " is connected",
- "#secret_club: member bob_1 (Bob) is connected",
- "contact bob_1 is merged into bob",
- "use @bob to send messages"
- ],
- do
- bob <## "#secret_club: alice added dan_1 (Daniel) to the group (connecting...)"
- bob <## "#secret_club: new member dan_1 is connected"
- bob <## "contact dan_1 is merged into dan"
- bob <## "use @dan to send messages",
- do
- cath <## "#secret_club: alice added dan_1 (Daniel) to the group (connecting...)"
- cath <## "#secret_club: new member dan_1 is connected"
- ]
- -- send messages - group is incognito for cath
- alice #> "#secret_club hello"
- concurrentlyN_
- [ bob <# "#secret_club alice> hello",
- cath ?<# "#secret_club alice> hello",
- dan <# "#secret_club alice> hello"
- ]
- bob #> "#secret_club hi there"
- concurrentlyN_
- [ alice <# "#secret_club bob> hi there",
- cath ?<# "#secret_club bob_1> hi there",
- dan <# "#secret_club bob> hi there"
- ]
- cath ?#> "#secret_club hey"
- concurrentlyN_
- [ alice <# ("#secret_club " <> cathIncognito <> "> hey"),
- bob <# ("#secret_club " <> cathIncognito <> "> hey"),
- dan <# ("#secret_club " <> cathIncognito <> "> hey")
- ]
- dan #> "#secret_club how is it going?"
- concurrentlyN_
- [ alice <# "#secret_club dan> how is it going?",
- bob <# "#secret_club dan> how is it going?",
- cath ?<# "#secret_club dan_1> how is it going?"
- ]
- -- cath and bob can send messages via new direct connection, cath is incognito
- bob #> ("@" <> cathIncognito <> " hi, I'm bob")
- cath ?<# "bob_1> hi, I'm bob"
- cath ?#> "@bob_1 hey, I'm incognito"
- bob <# (cathIncognito <> "> hey, I'm incognito")
- -- cath and dan can send messages via new direct connection, cath is incognito
- dan #> ("@" <> cathIncognito <> " hi, I'm dan")
- cath ?<# "dan_1> hi, I'm dan"
- cath ?#> "@dan_1 hey, I'm incognito"
- dan <# (cathIncognito <> "> hey, I'm incognito")
- -- non incognito connections are separate
- bob <##> cath
- dan <##> cath
- -- list groups
- cath ##> "/gs"
- cath <## "i #secret_club (4 members)"
- -- list group members
- alice ##> "/ms secret_club"
- alice
- <### [ "alice (Alice): owner, you, created group",
- "bob (Bob): admin, invited, connected",
- ConsoleString $ cathIncognito <> ": admin, invited, connected",
- "dan (Daniel): admin, invited, connected"
- ]
- bob ##> "/ms secret_club"
- bob
- <### [ "alice (Alice): owner, host, connected",
- "bob (Bob): admin, you, connected",
- ConsoleString $ cathIncognito <> ": admin, connected",
- "dan (Daniel): admin, connected"
- ]
- cath ##> "/ms secret_club"
- cath
- <### [ "alice (Alice): owner, host, connected",
- "bob_1 (Bob): admin, connected",
- ConsoleString $ "i " <> cathIncognito <> ": admin, you, connected",
- "dan_1 (Daniel): admin, connected"
- ]
- dan ##> "/ms secret_club"
- dan
- <### [ "alice (Alice): owner, host, connected",
- "bob (Bob): admin, connected",
- ConsoleString $ cathIncognito <> ": admin, connected",
- "dan (Daniel): admin, you, connected"
- ]
- -- remove member
- bob ##> ("/rm secret_club " <> cathIncognito)
- concurrentlyN_
- [ bob <## ("#secret_club: you removed " <> cathIncognito <> " from the group"),
- alice <## ("#secret_club: bob removed " <> cathIncognito <> " from the group"),
- dan <## ("#secret_club: bob removed " <> cathIncognito <> " from the group"),
- do
- cath <## "#secret_club: bob_1 removed you from the group"
- cath <## "use /d #secret_club to delete the group"
- ]
- bob #> "#secret_club hi"
- concurrentlyN_
- [ alice <# "#secret_club bob> hi",
- dan <# "#secret_club bob> hi",
- (cath )
- ]
- alice #> "#secret_club hello"
- concurrentlyN_
- [ bob <# "#secret_club alice> hello",
- dan <# "#secret_club alice> hello",
- (cath )
- ]
- cath ##> "#secret_club hello"
- cath <## "you are no longer a member of the group"
- -- cath can still message members directly
- bob #> ("@" <> cathIncognito <> " I removed you from group")
- cath ?<# "bob_1> I removed you from group"
- cath ?#> "@bob_1 ok"
- bob <# (cathIncognito <> "> ok")
+testJoinGroupIncognito =
+ testChatCfg4 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile danProfile $
+ \alice bob cath dan -> do
+ -- non incognito connections
+ connectUsers alice bob
+ connectUsers alice dan
+ connectUsers bob cath
+ connectUsers bob dan
+ connectUsers cath dan
+ -- cath connected incognito to alice
+ alice ##> "/c"
+ inv <- getInvitation alice
+ cath ##> ("/c i " <> inv)
+ cath <## "confirmation sent!"
+ cathIncognito <- getTermLine cath
+ concurrentlyN_
+ [ do
+ cath <## ("alice (Alice): contact is connected, your incognito profile for this contact is " <> cathIncognito)
+ cath <## "use /i alice to print out this incognito profile again",
+ alice <## (cathIncognito <> ": contact is connected")
+ ]
+ -- alice creates group
+ alice ##> "/g secret_club"
+ alice <## "group #secret_club is created"
+ alice <## "to add members use /a secret_club or /create link #secret_club"
+ -- alice invites bob
+ alice ##> "/a secret_club bob admin"
+ concurrentlyN_
+ [ alice <## "invitation to join the group #secret_club sent to bob",
+ do
+ bob <## "#secret_club: alice invites you to join the group as admin"
+ bob <## "use /j secret_club to accept"
+ ]
+ bob ##> "/j secret_club"
+ concurrently_
+ (alice <## "#secret_club: bob joined the group")
+ (bob <## "#secret_club: you joined the group")
+ -- alice invites cath
+ alice ##> ("/a secret_club " <> cathIncognito <> " admin")
+ concurrentlyN_
+ [ alice <## ("invitation to join the group #secret_club sent to " <> cathIncognito),
+ do
+ cath <## "#secret_club: alice invites you to join the group as admin"
+ cath <## ("use /j secret_club to join incognito as " <> cathIncognito)
+ ]
+ -- cath uses the same incognito profile when joining group, cath and bob don't merge contacts
+ cath ##> "/j secret_club"
+ concurrentlyN_
+ [ alice <## ("#secret_club: " <> cathIncognito <> " joined the group"),
+ do
+ cath <## ("#secret_club: you joined the group incognito as " <> cathIncognito)
+ cath <## "#secret_club: member bob_1 (Bob) is connected",
+ do
+ bob <## ("#secret_club: alice added " <> cathIncognito <> " to the group (connecting...)")
+ bob <## ("#secret_club: new member " <> cathIncognito <> " is connected")
+ ]
+ -- cath cannot invite to the group because her membership is incognito
+ cath ##> "/a secret_club dan"
+ cath <## "you've connected to this group using an incognito profile - prohibited to invite contacts"
+ -- alice invites dan
+ alice ##> "/a secret_club dan admin"
+ concurrentlyN_
+ [ alice <## "invitation to join the group #secret_club sent to dan",
+ do
+ dan <## "#secret_club: alice invites you to join the group as admin"
+ dan <## "use /j secret_club to accept"
+ ]
+ dan ##> "/j secret_club"
+ -- cath and dan don't merge contacts
+ concurrentlyN_
+ [ alice <## "#secret_club: dan joined the group",
+ do
+ dan <## "#secret_club: you joined the group"
+ dan
+ <### [ ConsoleString $ "#secret_club: member " <> cathIncognito <> " is connected",
+ "#secret_club: member bob_1 (Bob) is connected",
+ "contact bob_1 is merged into bob",
+ "use @bob to send messages"
+ ],
+ do
+ bob <## "#secret_club: alice added dan_1 (Daniel) to the group (connecting...)"
+ bob <## "#secret_club: new member dan_1 is connected"
+ bob <## "contact dan_1 is merged into dan"
+ bob <## "use @dan to send messages",
+ do
+ cath <## "#secret_club: alice added dan_1 (Daniel) to the group (connecting...)"
+ cath <## "#secret_club: new member dan_1 is connected"
+ ]
+ -- send messages - group is incognito for cath
+ alice #> "#secret_club hello"
+ concurrentlyN_
+ [ bob <# "#secret_club alice> hello",
+ cath ?<# "#secret_club alice> hello",
+ dan <# "#secret_club alice> hello"
+ ]
+ bob #> "#secret_club hi there"
+ concurrentlyN_
+ [ alice <# "#secret_club bob> hi there",
+ cath ?<# "#secret_club bob_1> hi there",
+ dan <# "#secret_club bob> hi there"
+ ]
+ cath ?#> "#secret_club hey"
+ concurrentlyN_
+ [ alice <# ("#secret_club " <> cathIncognito <> "> hey"),
+ bob <# ("#secret_club " <> cathIncognito <> "> hey"),
+ dan <# ("#secret_club " <> cathIncognito <> "> hey")
+ ]
+ dan #> "#secret_club how is it going?"
+ concurrentlyN_
+ [ alice <# "#secret_club dan> how is it going?",
+ bob <# "#secret_club dan> how is it going?",
+ cath ?<# "#secret_club dan_1> how is it going?"
+ ]
+ -- cath and bob can send messages via new direct connection, cath is incognito
+ bob #> ("@" <> cathIncognito <> " hi, I'm bob")
+ cath ?<# "bob_1> hi, I'm bob"
+ cath ?#> "@bob_1 hey, I'm incognito"
+ bob <# (cathIncognito <> "> hey, I'm incognito")
+ -- cath and dan can send messages via new direct connection, cath is incognito
+ dan #> ("@" <> cathIncognito <> " hi, I'm dan")
+ cath ?<# "dan_1> hi, I'm dan"
+ cath ?#> "@dan_1 hey, I'm incognito"
+ dan <# (cathIncognito <> "> hey, I'm incognito")
+ -- non incognito connections are separate
+ bob <##> cath
+ dan <##> cath
+ -- list groups
+ cath ##> "/gs"
+ cath <## "i #secret_club (4 members)"
+ -- list group members
+ alice ##> "/ms secret_club"
+ alice
+ <### [ "alice (Alice): owner, you, created group",
+ "bob (Bob): admin, invited, connected",
+ ConsoleString $ cathIncognito <> ": admin, invited, connected",
+ "dan (Daniel): admin, invited, connected"
+ ]
+ bob ##> "/ms secret_club"
+ bob
+ <### [ "alice (Alice): owner, host, connected",
+ "bob (Bob): admin, you, connected",
+ ConsoleString $ cathIncognito <> ": admin, connected",
+ "dan (Daniel): admin, connected"
+ ]
+ cath ##> "/ms secret_club"
+ cath
+ <### [ "alice (Alice): owner, host, connected",
+ "bob_1 (Bob): admin, connected",
+ ConsoleString $ "i " <> cathIncognito <> ": admin, you, connected",
+ "dan_1 (Daniel): admin, connected"
+ ]
+ dan ##> "/ms secret_club"
+ dan
+ <### [ "alice (Alice): owner, host, connected",
+ "bob (Bob): admin, connected",
+ ConsoleString $ cathIncognito <> ": admin, connected",
+ "dan (Daniel): admin, you, connected"
+ ]
+ -- remove member
+ bob ##> ("/rm secret_club " <> cathIncognito)
+ concurrentlyN_
+ [ bob <## ("#secret_club: you removed " <> cathIncognito <> " from the group"),
+ alice <## ("#secret_club: bob removed " <> cathIncognito <> " from the group"),
+ dan <## ("#secret_club: bob removed " <> cathIncognito <> " from the group"),
+ do
+ cath <## "#secret_club: bob_1 removed you from the group"
+ cath <## "use /d #secret_club to delete the group"
+ ]
+ bob #> "#secret_club hi"
+ concurrentlyN_
+ [ alice <# "#secret_club bob> hi",
+ dan <# "#secret_club bob> hi",
+ (cath )
+ ]
+ alice #> "#secret_club hello"
+ concurrentlyN_
+ [ bob <# "#secret_club alice> hello",
+ dan <# "#secret_club alice> hello",
+ (cath )
+ ]
+ cath ##> "#secret_club hello"
+ cath <## "you are no longer a member of the group"
+ -- cath can still message members directly
+ bob #> ("@" <> cathIncognito <> " I removed you from group")
+ cath ?<# "bob_1> I removed you from group"
+ cath ?#> "@bob_1 ok"
+ bob <# (cathIncognito <> "> ok")
testCantInviteContactIncognito :: HasCallStack => FilePath -> IO ()
testCantInviteContactIncognito = testChat2 aliceProfile bobProfile $
@@ -1356,54 +1359,55 @@ testAllowFullDeletionGroup =
testProhibitDirectMessages :: HasCallStack => FilePath -> IO ()
testProhibitDirectMessages =
- testChat4 aliceProfile bobProfile cathProfile danProfile $ \alice bob cath dan -> do
- createGroup3 "team" alice bob cath
- threadDelay 1000000
- alice ##> "/set direct #team off"
- alice <## "updated group preferences:"
- alice <## "Direct messages: off"
- directProhibited bob
- directProhibited cath
- threadDelay 1000000
- -- still can send direct messages to direct contacts
- alice #> "@bob hello again"
- bob <# "alice> hello again"
- alice #> "@cath hello again"
- cath <# "alice> hello again"
- bob ##> "@cath hello again"
- bob <## "direct messages to indirect contact cath are prohibited"
- (cath )
- connectUsers cath dan
- addMember "team" cath dan GRMember
- dan ##> "/j #team"
- concurrentlyN_
- [ cath <## "#team: dan joined the group",
- do
- dan <## "#team: you joined the group"
- dan
- <### [ "#team: member alice (Alice) is connected",
- "#team: member bob (Bob) is connected"
- ],
- do
- alice <## "#team: cath added dan (Daniel) to the group (connecting...)"
- alice <## "#team: new member dan is connected",
- do
- bob <## "#team: cath added dan (Daniel) to the group (connecting...)"
- bob <## "#team: new member dan is connected"
- ]
- alice ##> "@dan hi"
- alice <## "direct messages to indirect contact dan are prohibited"
- bob ##> "@dan hi"
- bob <## "direct messages to indirect contact dan are prohibited"
- (dan )
- dan ##> "@alice hi"
- dan <## "direct messages to indirect contact alice are prohibited"
- dan ##> "@bob hi"
- dan <## "direct messages to indirect contact bob are prohibited"
- dan #> "@cath hi"
- cath <# "dan> hi"
- cath #> "@dan hi"
- dan <# "cath> hi"
+ testChatCfg4 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile danProfile $
+ \alice bob cath dan -> do
+ createGroup3 "team" alice bob cath
+ threadDelay 1000000
+ alice ##> "/set direct #team off"
+ alice <## "updated group preferences:"
+ alice <## "Direct messages: off"
+ directProhibited bob
+ directProhibited cath
+ threadDelay 1000000
+ -- still can send direct messages to direct contacts
+ alice #> "@bob hello again"
+ bob <# "alice> hello again"
+ alice #> "@cath hello again"
+ cath <# "alice> hello again"
+ bob ##> "@cath hello again"
+ bob <## "direct messages to indirect contact cath are prohibited"
+ (cath )
+ connectUsers cath dan
+ addMember "team" cath dan GRMember
+ dan ##> "/j #team"
+ concurrentlyN_
+ [ cath <## "#team: dan joined the group",
+ do
+ dan <## "#team: you joined the group"
+ dan
+ <### [ "#team: member alice (Alice) is connected",
+ "#team: member bob (Bob) is connected"
+ ],
+ do
+ alice <## "#team: cath added dan (Daniel) to the group (connecting...)"
+ alice <## "#team: new member dan is connected",
+ do
+ bob <## "#team: cath added dan (Daniel) to the group (connecting...)"
+ bob <## "#team: new member dan is connected"
+ ]
+ alice ##> "@dan hi"
+ alice <## "direct messages to indirect contact dan are prohibited"
+ bob ##> "@dan hi"
+ bob <## "direct messages to indirect contact dan are prohibited"
+ (dan )
+ dan ##> "@alice hi"
+ dan <## "direct messages to indirect contact alice are prohibited"
+ dan ##> "@bob hi"
+ dan <## "direct messages to indirect contact bob are prohibited"
+ dan #> "@cath hi"
+ cath <# "dan> hi"
+ cath #> "@dan hi"
+ dan <# "cath> hi"
where
directProhibited :: HasCallStack => TestCC -> IO ()
directProhibited cc = do
diff --git a/tests/ChatTests/Utils.hs b/tests/ChatTests/Utils.hs
index b525bf333..72cb99c97 100644
--- a/tests/ChatTests/Utils.hs
+++ b/tests/ChatTests/Utils.hs
@@ -67,9 +67,9 @@ versionTestMatrix2 runTest = do
it "v1 to v2" $ runTestCfg2 testCfg testCfgV1 runTest
it "v2 to v1" $ runTestCfg2 testCfgV1 testCfg runTest
-versionTestMatrix3 :: (HasCallStack => TestCC -> TestCC -> TestCC -> IO ()) -> SpecWith FilePath
-versionTestMatrix3 runTest = do
- it "v2" $ testChat3 aliceProfile bobProfile cathProfile runTest
+-- versionTestMatrix3 :: (HasCallStack => TestCC -> TestCC -> TestCC -> IO ()) -> SpecWith FilePath
+-- versionTestMatrix3 runTest = do
+-- it "v2" $ testChat3 aliceProfile bobProfile cathProfile runTest
-- it "v1" $ testChatCfg3 testCfgV1 aliceProfile bobProfile cathProfile runTest
-- it "v1 to v2" $ runTestCfg3 testCfg testCfgV1 testCfgV1 runTest
diff --git a/tests/ProtocolTests.hs b/tests/ProtocolTests.hs
index 98c592fa7..3acc78e7d 100644
--- a/tests/ProtocolTests.hs
+++ b/tests/ProtocolTests.hs
@@ -230,16 +230,28 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do
#==# XGrpAcpt (MemberId "\1\2\3\4")
it "x.grp.mem.new" $
"{\"v\":\"1\",\"event\":\"x.grp.mem.new\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}"
- #==# XGrpMemNew MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, profile = testProfile}
+ #==# XGrpMemNew MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Nothing, profile = testProfile}
+ it "x.grp.mem.new with member chat version range" $
+ "{\"v\":\"1\",\"event\":\"x.grp.mem.new\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-2\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}"
+ #==# XGrpMemNew MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Just $ ChatVersionRange supportedChatVRange, profile = testProfile}
it "x.grp.mem.intro" $
"{\"v\":\"1\",\"event\":\"x.grp.mem.intro\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}"
- #==# XGrpMemIntro MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, profile = testProfile}
+ #==# XGrpMemIntro MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Nothing, profile = testProfile}
+ it "x.grp.mem.intro with member chat version range" $
+ "{\"v\":\"1\",\"event\":\"x.grp.mem.intro\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-2\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}"
+ #==# XGrpMemIntro MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Just $ ChatVersionRange supportedChatVRange, profile = testProfile}
it "x.grp.mem.inv" $
"{\"v\":\"1\",\"event\":\"x.grp.mem.inv\",\"params\":{\"memberId\":\"AQIDBA==\",\"memberIntro\":{\"directConnReq\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"groupConnReq\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}}"
- #==# XGrpMemInv (MemberId "\1\2\3\4") IntroInvitation {groupConnReq = testConnReq, directConnReq = testConnReq}
+ #==# XGrpMemInv (MemberId "\1\2\3\4") IntroInvitation {groupConnReq = testConnReq, directConnReq = Just testConnReq}
+ it "x.grp.mem.inv w/t directConnReq" $
+ "{\"v\":\"1\",\"event\":\"x.grp.mem.inv\",\"params\":{\"memberId\":\"AQIDBA==\",\"memberIntro\":{\"groupConnReq\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}}"
+ #==# XGrpMemInv (MemberId "\1\2\3\4") IntroInvitation {groupConnReq = testConnReq, directConnReq = Nothing}
it "x.grp.mem.fwd" $
"{\"v\":\"1\",\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"directConnReq\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"groupConnReq\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}"
- #==# XGrpMemFwd MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, profile = testProfile} IntroInvitation {groupConnReq = testConnReq, directConnReq = testConnReq}
+ #==# XGrpMemFwd MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Nothing, profile = testProfile} IntroInvitation {groupConnReq = testConnReq, directConnReq = Just testConnReq}
+ it "x.grp.mem.fwd with member chat version range and w/t directConnReq" $
+ "{\"v\":\"1\",\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"groupConnReq\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-2\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}"
+ #==# XGrpMemFwd MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Just $ ChatVersionRange supportedChatVRange, profile = testProfile} IntroInvitation {groupConnReq = testConnReq, directConnReq = Nothing}
it "x.grp.mem.info" $
"{\"v\":\"1\",\"event\":\"x.grp.mem.info\",\"params\":{\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}"
#==# XGrpMemInfo (MemberId "\1\2\3\4") testProfile
From 68f359c90475205ffeb506532a0cb3ecf564ace8 Mon Sep 17 00:00:00 2001
From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
Date: Wed, 6 Sep 2023 10:15:32 +0400
Subject: [PATCH 3/6] core: enable creation of direct connections in group for
chat protocol v2 (for beta; to be reverted for full release) (#3022)
---
src/Simplex/Chat/Protocol.hs | 2 +-
tests/ChatTests/Groups.hs | 10 +++++-----
2 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/src/Simplex/Chat/Protocol.hs b/src/Simplex/Chat/Protocol.hs
index 2f93ab13d..d104e002f 100644
--- a/src/Simplex/Chat/Protocol.hs
+++ b/src/Simplex/Chat/Protocol.hs
@@ -56,7 +56,7 @@ supportedChatVRange = mkVersionRange 1 currentChatVersion
-- version that starts support for skipping establishing direct connections in a group
groupNoDirectVersion :: Version
-groupNoDirectVersion = 2
+groupNoDirectVersion = 3 -- 2
data ConnectionEntity
= RcvDirectMsgConnection {entityConnection :: Connection, contact :: Maybe Contact}
diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs
index e533a55da..82481e9df 100644
--- a/tests/ChatTests/Groups.hs
+++ b/tests/ChatTests/Groups.hs
@@ -68,16 +68,16 @@ chatGroupTests = do
it "should send delivery receipts in group depending on configuration" testConfigureGroupDeliveryReceipts
describe "direct connections in group are not established based on chat protocol version" $ do
describe "3 members group" $ do
- testNoDirect _0 _0 True
+ testNoDirect _0 _0 False -- True
testNoDirect _0 _1 False
testNoDirect _1 _0 False
testNoDirect _1 _1 False
describe "4 members group" $ do
- testNoDirect4 _0 _0 _0 True True True
- testNoDirect4 _0 _0 _1 True False False
- testNoDirect4 _0 _1 _0 False True False
+ testNoDirect4 _0 _0 _0 False False False -- True True True
+ testNoDirect4 _0 _0 _1 False False False -- True False False
+ testNoDirect4 _0 _1 _0 False False False -- False True False
testNoDirect4 _0 _1 _1 False False False
- testNoDirect4 _1 _0 _0 False False True
+ testNoDirect4 _1 _0 _0 False False False -- False False True
testNoDirect4 _1 _0 _1 False False False
testNoDirect4 _1 _1 _0 False False False
testNoDirect4 _1 _1 _1 False False False
From e6baca561080270b92d697939e8c40acd17b869d Mon Sep 17 00:00:00 2001
From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
Date: Wed, 6 Sep 2023 11:41:23 +0400
Subject: [PATCH 4/6] core: rename conn vrange into peer vrange (#3023)
---
src/Simplex/Chat.hs | 40 +++++++++----------
.../M20230829_connections_chat_vrange.hs | 16 ++++----
src/Simplex/Chat/Migrations/chat_schema.sql | 8 ++--
src/Simplex/Chat/Store/Connections.hs | 2 +-
src/Simplex/Chat/Store/Direct.hs | 20 +++++-----
src/Simplex/Chat/Store/Groups.hs | 30 +++++++-------
src/Simplex/Chat/Store/Messages.hs | 4 +-
src/Simplex/Chat/Store/Profiles.hs | 4 +-
src/Simplex/Chat/Store/Shared.hs | 16 ++++----
src/Simplex/Chat/Types.hs | 4 +-
src/Simplex/Chat/View.hs | 8 ++--
tests/ChatTests/Direct.hs | 14 +++----
tests/ChatTests/Utils.hs | 2 +-
13 files changed, 84 insertions(+), 84 deletions(-)
diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs
index 6fc2f855e..b3ba94c8e 100644
--- a/src/Simplex/Chat.hs
+++ b/src/Simplex/Chat.hs
@@ -1441,12 +1441,12 @@ processChatCommand = \case
inv@ReceivedGroupInvitation {fromMember} <- getGroupInvitation db user groupId
(inv,) <$> getContactViaMember db user fromMember
let ReceivedGroupInvitation {fromMember, connRequest, groupInfo = g@GroupInfo {membership}} = invitation
- Contact {activeConn = Connection {connChatVRange}} = ct
+ Contact {activeConn = Connection {peerChatVRange}} = ct
withChatLock "joinGroup" . procCmd $ do
dm <- directMessage $ XGrpAcpt (memberId (membership :: GroupMember))
agentConnId <- withAgent $ \a -> joinConnection a (aUserId user) True connRequest dm
withStore' $ \db -> do
- createMemberConnection db userId fromMember agentConnId connChatVRange
+ createMemberConnection db userId fromMember agentConnId peerChatVRange
updateGroupMemberStatus db userId fromMember GSMemAccepted
updateGroupMemberStatus db userId membership GSMemAccepted
updateCIGroupInvitationStatus user
@@ -2850,7 +2850,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
_ -> Nothing
processDirectMessage :: ACommand 'Agent e -> ConnectionEntity -> Connection -> Maybe Contact -> m ()
- processDirectMessage agentMsg connEntity conn@Connection {connId, connChatVRange, viaUserContactLink, groupLinkId, customUserProfileId, connectionCode} = \case
+ processDirectMessage agentMsg connEntity conn@Connection {connId, peerChatVRange, viaUserContactLink, groupLinkId, customUserProfileId, connectionCode} = \case
Nothing -> case agentMsg of
CONF confId _ connInfo -> do
-- [incognito] send saved profile
@@ -2931,7 +2931,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
CONF confId _ connInfo -> do
-- confirming direct connection with a member
ChatMessage {chatVRange, chatMsgEvent} <- parseChatMessage conn connInfo
- conn' <- updateConnChatVRange conn chatVRange
+ conn' <- updatePeerChatVRange conn chatVRange
case chatMsgEvent of
XGrpMemInfo _memId _memProfile -> do
-- TODO check member ID
@@ -2941,7 +2941,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
_ -> messageError "CONF from member must have x.grp.mem.info"
INFO connInfo -> do
ChatMessage {chatVRange, chatMsgEvent} <- parseChatMessage conn connInfo
- _conn' <- updateConnChatVRange conn chatVRange
+ _conn' <- updatePeerChatVRange conn chatVRange
case chatMsgEvent of
XGrpMemInfo _memId _memProfile -> do
-- TODO check member ID
@@ -2973,7 +2973,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
forM_ groupId_ $ \groupId -> do
gVar <- asks idsDrg
groupConnIds <- createAgentConnectionAsync user CFCreateConnGrpInv True SCMInvitation
- withStore $ \db -> createNewContactMemberAsync db gVar user groupId ct gLinkMemRole groupConnIds connChatVRange
+ withStore $ \db -> createNewContactMemberAsync db gVar user groupId ct gLinkMemRole groupConnIds peerChatVRange
_ -> pure ()
Just (gInfo@GroupInfo {membership}, m@GroupMember {activeConn}) ->
when (maybe False ((== ConnReady) . connStatus) activeConn) $ do
@@ -3042,7 +3042,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
-- [async agent commands] XGrpMemIntro continuation on receiving INV
CFCreateConnGrpMemInv ->
ifM
- (featureVersionSupported (connChatVRange conn) groupNoDirectVersion)
+ (featureVersionSupported (peerChatVRange conn) groupNoDirectVersion)
sendWithoutDirectCReq
sendWithDirectCReq
where
@@ -3078,7 +3078,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
CRContactUri _ -> throwChatError $ CECommandError "unexpected ConnectionRequestUri type"
CONF confId _ connInfo -> do
ChatMessage {chatVRange, chatMsgEvent} <- parseChatMessage conn connInfo
- conn' <- updateConnChatVRange conn chatVRange
+ conn' <- updatePeerChatVRange conn chatVRange
case memberCategory m of
GCInviteeMember ->
case chatMsgEvent of
@@ -3100,7 +3100,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
_ -> messageError "CONF from member must have x.grp.mem.info"
INFO connInfo -> do
ChatMessage {chatVRange, chatMsgEvent} <- parseChatMessage conn connInfo
- _conn' <- updateConnChatVRange conn chatVRange
+ _conn' <- updatePeerChatVRange conn chatVRange
case chatMsgEvent of
XGrpMemInfo memId _memProfile
| sameMemberId memId m -> do
@@ -3277,7 +3277,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
-- when recipient of the file "joins" connection created by the sender
CONF confId _ connInfo -> do
ChatMessage {chatVRange, chatMsgEvent} <- parseChatMessage conn connInfo
- conn' <- updateConnChatVRange conn chatVRange
+ conn' <- updatePeerChatVRange conn chatVRange
case chatMsgEvent of
-- TODO save XFileAcpt message
XFileAcpt name
@@ -3346,7 +3346,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
-- (sender doesn't create connections for all group members)
CONF confId _ connInfo -> do
ChatMessage {chatVRange, chatMsgEvent} <- parseChatMessage conn connInfo
- conn' <- updateConnChatVRange conn chatVRange
+ conn' <- updatePeerChatVRange conn chatVRange
case chatMsgEvent of
XOk -> allowAgentConnectionAsync user conn' confId XOk -- [async agent commands] no continuation needed, but command should be asynchronous for stability
_ -> pure ()
@@ -4042,7 +4042,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
processGroupInvitation :: Contact -> GroupInvitation -> RcvMessage -> MsgMeta -> m ()
processGroupInvitation ct inv msg msgMeta = do
- let Contact {localDisplayName = c, activeConn = Connection {connChatVRange, customUserProfileId, groupLinkId = groupLinkId'}} = ct
+ let Contact {localDisplayName = c, activeConn = Connection {peerChatVRange, customUserProfileId, groupLinkId = groupLinkId'}} = ct
GroupInvitation {fromMember = (MemberIdRole fromMemId fromRole), invitedMember = (MemberIdRole memId memRole), connRequest, groupLinkId} = inv
checkIntegrityCreateItem (CDDirectRcv ct) msgMeta
when (fromRole < GRAdmin || fromRole < memRole) $ throwChatError (CEGroupContactRole c)
@@ -4053,7 +4053,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
then do
connIds <- joinAgentConnectionAsync user True connRequest =<< directMessage (XGrpAcpt memberId)
withStore' $ \db -> do
- createMemberConnectionAsync db user hostId connIds connChatVRange
+ createMemberConnectionAsync db user hostId connIds peerChatVRange
updateGroupMemberStatusById db userId hostId GSMemAccepted
updateGroupMemberStatus db userId membership GSMemAccepted
toView $ CRUserAcceptedGroupSent user gInfo {membership = membership {memberStatus = GSMemAccepted}} (Just ct)
@@ -4256,7 +4256,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
saveConnInfo :: Connection -> ConnInfo -> m Connection
saveConnInfo activeConn connInfo = do
ChatMessage {chatVRange, chatMsgEvent} <- parseChatMessage activeConn connInfo
- conn' <- updateConnChatVRange activeConn chatVRange
+ conn' <- updatePeerChatVRange activeConn chatVRange
case chatMsgEvent of
XInfo p -> do
ct <- withStore $ \db -> createDirectContact db user conn' p
@@ -4481,11 +4481,11 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
toView $ CRChatItemStatusUpdated user (AChatItem SCTGroup SMDSnd (GroupChat gInfo) chatItem)
_ -> pure ()
-updateConnChatVRange :: ChatMonad m => Connection -> VersionRange -> m Connection
-updateConnChatVRange conn@Connection {connId, connChatVRange} msgChatVRange
- | msgChatVRange /= connChatVRange = do
- withStore' $ \db -> setConnChatVRange db connId msgChatVRange
- pure conn {connChatVRange = msgChatVRange}
+updatePeerChatVRange :: ChatMonad m => Connection -> VersionRange -> m Connection
+updatePeerChatVRange conn@Connection {connId, peerChatVRange} msgChatVRange
+ | msgChatVRange /= peerChatVRange = do
+ withStore' $ \db -> setPeerChatVRange db connId msgChatVRange
+ pure conn {peerChatVRange = msgChatVRange}
| otherwise = pure conn
featureVersionSupported :: ChatMonad' m => VersionRange -> Version -> m Bool
@@ -4759,7 +4759,7 @@ sendPendingGroupMessages user GroupMember {groupMemberId, localDisplayName} conn
saveRcvMSG :: ChatMonad m => Connection -> ConnOrGroupId -> MsgMeta -> MsgBody -> CommandId -> m (Connection, RcvMessage)
saveRcvMSG conn@Connection {connId} connOrGroupId agentMsgMeta msgBody agentAckCmdId = do
ACMsg _ ChatMessage {chatVRange, msgId = sharedMsgId_, chatMsgEvent} <- parseAChatMessage conn agentMsgMeta msgBody
- conn' <- updateConnChatVRange conn chatVRange
+ conn' <- updatePeerChatVRange conn chatVRange
let agentMsgId = fst $ recipient agentMsgMeta
newMsg = NewMessage {chatMsgEvent, msgBody}
rcvMsgDelivery = RcvMsgDelivery {connId, agentMsgId, agentMsgMeta, agentAckCmdId}
diff --git a/src/Simplex/Chat/Migrations/M20230829_connections_chat_vrange.hs b/src/Simplex/Chat/Migrations/M20230829_connections_chat_vrange.hs
index b657cc648..2588553a9 100644
--- a/src/Simplex/Chat/Migrations/M20230829_connections_chat_vrange.hs
+++ b/src/Simplex/Chat/Migrations/M20230829_connections_chat_vrange.hs
@@ -8,19 +8,19 @@ import Database.SQLite.Simple.QQ (sql)
m20230829_connections_chat_vrange :: Query
m20230829_connections_chat_vrange =
[sql|
-ALTER TABLE connections ADD COLUMN chat_vrange_min_version INTEGER NOT NULL DEFAULT 1;
-ALTER TABLE connections ADD COLUMN chat_vrange_max_version INTEGER NOT NULL DEFAULT 1;
+ALTER TABLE connections ADD COLUMN peer_chat_min_version INTEGER NOT NULL DEFAULT 1;
+ALTER TABLE connections ADD COLUMN peer_chat_max_version INTEGER NOT NULL DEFAULT 1;
-ALTER TABLE contact_requests ADD COLUMN chat_vrange_min_version INTEGER NOT NULL DEFAULT 1;
-ALTER TABLE contact_requests ADD COLUMN chat_vrange_max_version INTEGER NOT NULL DEFAULT 1;
+ALTER TABLE contact_requests ADD COLUMN peer_chat_min_version INTEGER NOT NULL DEFAULT 1;
+ALTER TABLE contact_requests ADD COLUMN peer_chat_max_version INTEGER NOT NULL DEFAULT 1;
|]
down_m20230829_connections_chat_vrange :: Query
down_m20230829_connections_chat_vrange =
[sql|
-ALTER TABLE contact_requests DROP COLUMN chat_vrange_max_version;
-ALTER TABLE contact_requests DROP COLUMN chat_vrange_min_version;
+ALTER TABLE contact_requests DROP COLUMN peer_chat_max_version;
+ALTER TABLE contact_requests DROP COLUMN peer_chat_min_version;
-ALTER TABLE connections DROP COLUMN chat_vrange_max_version;
-ALTER TABLE connections DROP COLUMN chat_vrange_min_version;
+ALTER TABLE connections DROP COLUMN peer_chat_max_version;
+ALTER TABLE connections DROP COLUMN peer_chat_min_version;
|]
diff --git a/src/Simplex/Chat/Migrations/chat_schema.sql b/src/Simplex/Chat/Migrations/chat_schema.sql
index eafdc85d1..f0731b6ef 100644
--- a/src/Simplex/Chat/Migrations/chat_schema.sql
+++ b/src/Simplex/Chat/Migrations/chat_schema.sql
@@ -285,8 +285,8 @@ CREATE TABLE connections(
security_code TEXT NULL,
security_code_verified_at TEXT NULL,
auth_err_counter INTEGER DEFAULT 0 CHECK(auth_err_counter NOT NULL),
- chat_vrange_min_version INTEGER NOT NULL DEFAULT 1,
- chat_vrange_max_version INTEGER NOT NULL DEFAULT 1,
+ peer_chat_min_version INTEGER NOT NULL DEFAULT 1,
+ peer_chat_max_version INTEGER NOT NULL DEFAULT 1,
FOREIGN KEY(snd_file_id, connection_id)
REFERENCES snd_files(file_id, connection_id)
ON DELETE CASCADE
@@ -320,8 +320,8 @@ CREATE TABLE contact_requests(
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE,
updated_at TEXT CHECK(updated_at NOT NULL),
xcontact_id BLOB,
- chat_vrange_min_version INTEGER NOT NULL DEFAULT 1,
- chat_vrange_max_version INTEGER NOT NULL DEFAULT 1,
+ peer_chat_min_version INTEGER NOT NULL DEFAULT 1,
+ peer_chat_max_version INTEGER NOT NULL DEFAULT 1,
FOREIGN KEY(user_id, local_display_name)
REFERENCES display_names(user_id, local_display_name)
ON UPDATE CASCADE
diff --git a/src/Simplex/Chat/Store/Connections.hs b/src/Simplex/Chat/Store/Connections.hs
index a7c8fd6c3..4bd092b7b 100644
--- a/src/Simplex/Chat/Store/Connections.hs
+++ b/src/Simplex/Chat/Store/Connections.hs
@@ -50,7 +50,7 @@ getConnectionEntity db user@User {userId, userContactId} agentConnId = do
[sql|
SELECT connection_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, via_group_link, group_link_id, custom_user_profile_id,
conn_status, conn_type, local_alias, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at, security_code, security_code_verified_at, auth_err_counter,
- chat_vrange_min_version, chat_vrange_max_version
+ peer_chat_min_version, peer_chat_max_version
FROM connections
WHERE user_id = ? AND agent_conn_id = ?
|]
diff --git a/src/Simplex/Chat/Store/Direct.hs b/src/Simplex/Chat/Store/Direct.hs
index 3b56e57b7..7df2858e9 100644
--- a/src/Simplex/Chat/Store/Direct.hs
+++ b/src/Simplex/Chat/Store/Direct.hs
@@ -145,7 +145,7 @@ getConnReqContactXContactId db user@User {userId} cReqHash = do
-- 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.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.auth_err_counter,
- c.chat_vrange_min_version, c.chat_vrange_max_version
+ c.peer_chat_min_version, c.peer_chat_max_version
FROM contacts ct
JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id
JOIN connections c ON c.contact_id = ct.contact_id
@@ -443,7 +443,7 @@ createOrUpdateContactRequest db user@User {userId} userContactLinkId invId (Vers
db
[sql|
INSERT INTO contact_requests
- (user_contact_link_id, agent_invitation_id, chat_vrange_min_version, chat_vrange_max_version, contact_profile_id, local_display_name, user_id, created_at, updated_at, xcontact_id)
+ (user_contact_link_id, agent_invitation_id, peer_chat_min_version, peer_chat_max_version, contact_profile_id, local_display_name, user_id, created_at, updated_at, xcontact_id)
VALUES (?,?,?,?,?,?,?,?,?,?)
|]
(userContactLinkId, invId, minV, maxV, profileId, ldn, userId, currentTs, currentTs, xContactId_)
@@ -461,7 +461,7 @@ createOrUpdateContactRequest db user@User {userId} userContactLinkId invId (Vers
-- 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.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.auth_err_counter,
- c.chat_vrange_min_version, c.chat_vrange_max_version
+ c.peer_chat_min_version, c.peer_chat_max_version
FROM contacts ct
JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id
LEFT JOIN connections c ON c.contact_id = ct.contact_id
@@ -479,7 +479,7 @@ createOrUpdateContactRequest db user@User {userId} userContactLinkId invId (Vers
SELECT
cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.user_contact_link_id,
c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, p.preferences, cr.created_at, cr.updated_at,
- cr.chat_vrange_min_version, cr.chat_vrange_max_version
+ cr.peer_chat_min_version, cr.peer_chat_max_version
FROM contact_requests cr
JOIN connections c USING (user_contact_link_id)
JOIN contact_profiles p USING (contact_profile_id)
@@ -499,7 +499,7 @@ createOrUpdateContactRequest db user@User {userId} userContactLinkId invId (Vers
db
[sql|
UPDATE contact_requests
- SET agent_invitation_id = ?, chat_vrange_min_version = ?, chat_vrange_max_version = ?, updated_at = ?
+ SET agent_invitation_id = ?, peer_chat_min_version = ?, peer_chat_max_version = ?, updated_at = ?
WHERE user_id = ? AND contact_request_id = ?
|]
(invId, minV, maxV, currentTs, userId, cReqId)
@@ -509,7 +509,7 @@ createOrUpdateContactRequest db user@User {userId} userContactLinkId invId (Vers
db
[sql|
UPDATE contact_requests
- SET agent_invitation_id = ?, chat_vrange_min_version = ?, chat_vrange_max_version = ?, local_display_name = ?, updated_at = ?
+ SET agent_invitation_id = ?, peer_chat_min_version = ?, peer_chat_max_version = ?, local_display_name = ?, updated_at = ?
WHERE user_id = ? AND contact_request_id = ?
|]
(invId, minV, maxV, ldn, currentTs, userId, cReqId)
@@ -548,7 +548,7 @@ getContactRequest db User {userId} contactRequestId =
SELECT
cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.user_contact_link_id,
c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, p.preferences, cr.created_at, cr.updated_at,
- cr.chat_vrange_min_version, cr.chat_vrange_max_version
+ cr.peer_chat_min_version, cr.peer_chat_max_version
FROM contact_requests cr
JOIN connections c USING (user_contact_link_id)
JOIN contact_profiles p USING (contact_profile_id)
@@ -625,7 +625,7 @@ getContact_ db user@User {userId} contactId deleted =
-- 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.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.auth_err_counter,
- c.chat_vrange_min_version, c.chat_vrange_max_version
+ c.peer_chat_min_version, c.peer_chat_max_version
FROM contacts ct
JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id
LEFT JOIN connections c ON c.contact_id = ct.contact_id
@@ -674,7 +674,7 @@ getContactConnections db userId Contact {contactId} =
[sql|
SELECT 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.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.auth_err_counter,
- c.chat_vrange_min_version, c.chat_vrange_max_version
+ c.peer_chat_min_version, c.peer_chat_max_version
FROM connections c
JOIN contacts ct ON ct.contact_id = c.contact_id
WHERE c.user_id = ? AND ct.user_id = ? AND ct.contact_id = ?
@@ -691,7 +691,7 @@ getConnectionById db User {userId} connId = ExceptT $ do
[sql|
SELECT connection_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, via_group_link, group_link_id, custom_user_profile_id,
conn_status, conn_type, local_alias, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at, security_code, security_code_verified_at, auth_err_counter,
- chat_vrange_min_version, chat_vrange_max_version
+ peer_chat_min_version, peer_chat_max_version
FROM connections
WHERE user_id = ? AND connection_id = ?
|]
diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs
index 40c21d616..59f1b6090 100644
--- a/src/Simplex/Chat/Store/Groups.hs
+++ b/src/Simplex/Chat/Store/Groups.hs
@@ -154,7 +154,7 @@ getGroupLinkConnection db User {userId} groupInfo@GroupInfo {groupId} =
[sql|
SELECT 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.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.auth_err_counter,
- c.chat_vrange_min_version, c.chat_vrange_max_version
+ c.peer_chat_min_version, c.peer_chat_max_version
FROM connections c
JOIN user_contact_links uc ON c.user_contact_link_id = uc.user_contact_link_id
WHERE c.user_id = ? AND uc.user_id = ? AND uc.group_id = ?
@@ -236,7 +236,7 @@ getGroupAndMember db User {userId, userContactId} groupMemberId =
m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences,
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.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.auth_err_counter,
- c.chat_vrange_min_version, c.chat_vrange_max_version
+ c.peer_chat_min_version, c.peer_chat_max_version
FROM group_members m
JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id)
JOIN groups g ON g.group_id = m.group_id
@@ -530,7 +530,7 @@ groupMemberQuery =
m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences,
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.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.auth_err_counter,
- c.chat_vrange_min_version, c.chat_vrange_max_version
+ c.peer_chat_min_version, c.peer_chat_max_version
FROM group_members m
JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id)
LEFT JOIN connections c ON c.connection_id = (
@@ -615,11 +615,11 @@ getGroupInvitation db user groupId =
DB.query db "SELECT g.inv_queue_info FROM groups g WHERE g.group_id = ? AND g.user_id = ?" (groupId, userId)
createNewContactMember :: DB.Connection -> TVar ChaChaDRG -> User -> GroupId -> Contact -> GroupMemberRole -> ConnId -> ConnReqInvitation -> ExceptT StoreError IO GroupMember
-createNewContactMember db gVar User {userId, userContactId} groupId Contact {contactId, localDisplayName, profile, activeConn = Connection {connChatVRange}} memberRole agentConnId connRequest =
+createNewContactMember db gVar User {userId, userContactId} groupId Contact {contactId, localDisplayName, profile, activeConn = Connection {peerChatVRange}} memberRole agentConnId connRequest =
createWithRandomId gVar $ \memId -> do
createdAt <- liftIO getCurrentTime
member@GroupMember {groupMemberId} <- createMember_ (MemberId memId) createdAt
- void $ createMemberConnection_ db userId groupMemberId agentConnId connChatVRange Nothing 0 createdAt
+ void $ createMemberConnection_ db userId groupMemberId agentConnId peerChatVRange Nothing 0 createdAt
pure member
where
createMember_ memberId createdAt = do
@@ -655,12 +655,12 @@ createNewContactMember db gVar User {userId, userContactId} groupId Contact {con
)
createNewContactMemberAsync :: DB.Connection -> TVar ChaChaDRG -> User -> GroupId -> Contact -> GroupMemberRole -> (CommandId, ConnId) -> VersionRange -> ExceptT StoreError IO ()
-createNewContactMemberAsync db gVar user@User {userId, userContactId} groupId Contact {contactId, localDisplayName, profile} memberRole (cmdId, agentConnId) connChatVRange =
+createNewContactMemberAsync db gVar user@User {userId, userContactId} groupId Contact {contactId, localDisplayName, profile} memberRole (cmdId, agentConnId) peerChatVRange =
createWithRandomId gVar $ \memId -> do
createdAt <- liftIO getCurrentTime
insertMember_ (MemberId memId) createdAt
groupMemberId <- liftIO $ insertedRowId db
- Connection {connId} <- createMemberConnection_ db userId groupMemberId agentConnId connChatVRange Nothing 0 createdAt
+ Connection {connId} <- createMemberConnection_ db userId groupMemberId agentConnId peerChatVRange Nothing 0 createdAt
setCommandConnId db user cmdId connId
where
insertMember_ memberId createdAt =
@@ -690,7 +690,7 @@ getContactViaMember db user@User {userId} GroupMember {groupMemberId} =
-- 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.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.auth_err_counter,
- c.chat_vrange_min_version, c.chat_vrange_max_version
+ c.peer_chat_min_version, c.peer_chat_max_version
FROM contacts ct
JOIN contact_profiles cp ON cp.contact_profile_id = ct.contact_profile_id
JOIN connections c ON c.connection_id = (
@@ -714,14 +714,14 @@ getMemberInvitation db User {userId} groupMemberId =
DB.query db "SELECT sent_inv_queue_info FROM group_members WHERE group_member_id = ? AND user_id = ?" (groupMemberId, userId)
createMemberConnection :: DB.Connection -> UserId -> GroupMember -> ConnId -> VersionRange -> IO ()
-createMemberConnection db userId GroupMember {groupMemberId} agentConnId connChatVRange = do
+createMemberConnection db userId GroupMember {groupMemberId} agentConnId peerChatVRange = do
currentTs <- getCurrentTime
- void $ createMemberConnection_ db userId groupMemberId agentConnId connChatVRange Nothing 0 currentTs
+ void $ createMemberConnection_ db userId groupMemberId agentConnId peerChatVRange Nothing 0 currentTs
createMemberConnectionAsync :: DB.Connection -> User -> GroupMemberId -> (CommandId, ConnId) -> VersionRange -> IO ()
-createMemberConnectionAsync db user@User {userId} groupMemberId (cmdId, agentConnId) connChatVRange = do
+createMemberConnectionAsync db user@User {userId} groupMemberId (cmdId, agentConnId) peerChatVRange = do
currentTs <- getCurrentTime
- Connection {connId} <- createMemberConnection_ db userId groupMemberId agentConnId connChatVRange Nothing 0 currentTs
+ Connection {connId} <- createMemberConnection_ db userId groupMemberId agentConnId peerChatVRange Nothing 0 currentTs
setCommandConnId db user cmdId connId
updateGroupMemberStatus :: DB.Connection -> UserId -> GroupMember -> GroupMemberStatus -> IO ()
@@ -978,7 +978,7 @@ createIntroToMemberContact db user@User {userId} GroupMember {memberContactId =
[":contact_id" := contactId, ":updated_at" := ts, ":group_member_id" := groupMemberId]
createMemberConnection_ :: DB.Connection -> UserId -> Int64 -> ConnId -> VersionRange -> Maybe Int64 -> Int -> UTCTime -> IO Connection
-createMemberConnection_ db userId groupMemberId agentConnId connChatVRange viaContact = createConnection_ db userId ConnMember (Just groupMemberId) agentConnId connChatVRange viaContact Nothing Nothing
+createMemberConnection_ db userId groupMemberId agentConnId peerChatVRange viaContact = createConnection_ db userId ConnMember (Just groupMemberId) agentConnId peerChatVRange viaContact Nothing Nothing
getViaGroupMember :: DB.Connection -> User -> Contact -> IO (Maybe (GroupInfo, GroupMember))
getViaGroupMember db User {userId, userContactId} Contact {contactId} =
@@ -999,7 +999,7 @@ getViaGroupMember db User {userId, userContactId} Contact {contactId} =
m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences,
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.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.auth_err_counter,
- c.chat_vrange_min_version, c.chat_vrange_max_version
+ c.peer_chat_min_version, c.peer_chat_max_version
FROM group_members m
JOIN contacts ct ON ct.contact_id = m.contact_id
JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id)
@@ -1033,7 +1033,7 @@ getViaGroupContact db user@User {userId} GroupMember {groupMemberId} =
p.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts,
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.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.auth_err_counter,
- c.chat_vrange_min_version, c.chat_vrange_max_version
+ c.peer_chat_min_version, c.peer_chat_max_version
FROM contacts ct
JOIN contact_profiles p ON ct.contact_profile_id = p.contact_profile_id
JOIN connections c ON c.connection_id = (
diff --git a/src/Simplex/Chat/Store/Messages.hs b/src/Simplex/Chat/Store/Messages.hs
index 12d1b6525..ddd59319d 100644
--- a/src/Simplex/Chat/Store/Messages.hs
+++ b/src/Simplex/Chat/Store/Messages.hs
@@ -479,7 +479,7 @@ getDirectChatPreviews_ db user@User {userId} = do
-- 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.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.auth_err_counter,
- c.chat_vrange_min_version, c.chat_vrange_max_version,
+ c.peer_chat_min_version, c.peer_chat_max_version,
-- ChatStats
COALESCE(ChatStats.UnreadCount, 0), COALESCE(ChatStats.MinUnread, 0), ct.unread_chat,
-- ChatItem
@@ -611,7 +611,7 @@ getContactRequestChatPreviews_ db User {userId} =
SELECT
cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.user_contact_link_id,
c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, p.preferences, cr.created_at, cr.updated_at,
- cr.chat_vrange_min_version, cr.chat_vrange_max_version
+ cr.peer_chat_min_version, cr.peer_chat_max_version
FROM contact_requests cr
JOIN connections c ON c.user_contact_link_id = cr.user_contact_link_id
JOIN contact_profiles p ON p.contact_profile_id = cr.contact_profile_id
diff --git a/src/Simplex/Chat/Store/Profiles.hs b/src/Simplex/Chat/Store/Profiles.hs
index 1d305ffd6..0c2f1f636 100644
--- a/src/Simplex/Chat/Store/Profiles.hs
+++ b/src/Simplex/Chat/Store/Profiles.hs
@@ -317,7 +317,7 @@ getUserAddressConnections db User {userId} = do
[sql|
SELECT 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.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.auth_err_counter,
- c.chat_vrange_min_version, c.chat_vrange_max_version
+ c.peer_chat_min_version, c.peer_chat_max_version
FROM connections c
JOIN user_contact_links uc ON c.user_contact_link_id = uc.user_contact_link_id
WHERE c.user_id = ? AND uc.user_id = ? AND uc.local_display_name = '' AND uc.group_id IS NULL
@@ -332,7 +332,7 @@ getUserContactLinks db User {userId} =
[sql|
SELECT 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.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.auth_err_counter,
- c.chat_vrange_min_version, c.chat_vrange_max_version,
+ c.peer_chat_min_version, c.peer_chat_max_version,
uc.user_contact_link_id, uc.conn_req_contact, uc.group_id
FROM connections c
JOIN user_contact_links uc ON c.user_contact_link_id = uc.user_contact_link_id
diff --git a/src/Simplex/Chat/Store/Shared.hs b/src/Simplex/Chat/Store/Shared.hs
index 48e2e5692..7ec307ab4 100644
--- a/src/Simplex/Chat/Store/Shared.hs
+++ b/src/Simplex/Chat/Store/Shared.hs
@@ -143,8 +143,8 @@ toConnection :: ConnectionRow -> Connection
toConnection ((connId, acId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (createdAt, code_, verifiedAt_, authErrCounter, minVer, maxVer)) =
let entityId = entityId_ connType
connectionCode = SecurityCode <$> code_ <*> verifiedAt_
- connChatVRange = fromMaybe (versionToRange maxVer) $ safeVersionRange minVer maxVer
- in Connection {connId, agentConnId = AgentConnId acId, connChatVRange, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, localAlias, entityId, connectionCode, authErrCounter, createdAt}
+ peerChatVRange = fromMaybe (versionToRange maxVer) $ safeVersionRange minVer maxVer
+ in Connection {connId, agentConnId = AgentConnId acId, peerChatVRange, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, localAlias, entityId, connectionCode, authErrCounter, createdAt}
where
entityId_ :: ConnType -> Maybe Int64
entityId_ ConnContact = contactId
@@ -159,7 +159,7 @@ toMaybeConnection ((Just connId, Just agentConnId, Just connLevel, viaContact, v
toMaybeConnection _ = Nothing
createConnection_ :: DB.Connection -> UserId -> ConnType -> Maybe Int64 -> ConnId -> VersionRange -> Maybe ContactId -> Maybe Int64 -> Maybe ProfileId -> Int -> UTCTime -> IO Connection
-createConnection_ db userId connType entityId acId connChatVRange@(VersionRange minV maxV) viaContact viaUserContactLink customUserProfileId connLevel currentTs = do
+createConnection_ db userId connType entityId acId peerChatVRange@(VersionRange minV maxV) viaContact viaUserContactLink customUserProfileId connLevel currentTs = do
viaLinkGroupId :: Maybe Int64 <- fmap join . forM viaUserContactLink $ \ucLinkId ->
maybeFirstRow fromOnly $ DB.query db "SELECT group_id FROM user_contact_links WHERE user_id = ? AND user_contact_link_id = ? AND group_id IS NOT NULL" (userId, ucLinkId)
let viaGroupLink = isJust viaLinkGroupId
@@ -169,7 +169,7 @@ createConnection_ db userId connType entityId acId connChatVRange@(VersionRange
INSERT INTO connections (
user_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, via_group_link, custom_user_profile_id, conn_status, conn_type,
contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at, updated_at,
- chat_vrange_min_version, chat_vrange_max_version
+ peer_chat_min_version, peer_chat_max_version
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|]
( (userId, acId, connLevel, viaContact, viaUserContactLink, viaGroupLink, customUserProfileId, ConnNew, connType)
@@ -177,17 +177,17 @@ createConnection_ db userId connType entityId acId connChatVRange@(VersionRange
:. (minV, maxV)
)
connId <- insertedRowId db
- pure Connection {connId, agentConnId = AgentConnId acId, connChatVRange, connType, entityId, viaContact, viaUserContactLink, viaGroupLink, groupLinkId = Nothing, customUserProfileId, connLevel, connStatus = ConnNew, localAlias = "", createdAt = currentTs, connectionCode = Nothing, authErrCounter = 0}
+ pure Connection {connId, agentConnId = AgentConnId acId, peerChatVRange, connType, entityId, viaContact, viaUserContactLink, viaGroupLink, groupLinkId = Nothing, customUserProfileId, connLevel, connStatus = ConnNew, localAlias = "", createdAt = currentTs, connectionCode = Nothing, authErrCounter = 0}
where
ent ct = if connType == ct then entityId else Nothing
-setConnChatVRange :: DB.Connection -> Int64 -> VersionRange -> IO ()
-setConnChatVRange db connId (VersionRange minVer maxVer) =
+setPeerChatVRange :: DB.Connection -> Int64 -> VersionRange -> IO ()
+setPeerChatVRange db connId (VersionRange minVer maxVer) =
DB.execute
db
[sql|
UPDATE connections
- SET chat_vrange_min_version = ?, chat_vrange_max_version = ?
+ SET peer_chat_min_version = ?, peer_chat_max_version = ?
WHERE connection_id = ?
|]
(minVer, maxVer, connId)
diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs
index ee5899db2..2d77cbe77 100644
--- a/src/Simplex/Chat/Types.hs
+++ b/src/Simplex/Chat/Types.hs
@@ -564,7 +564,7 @@ memberInfo :: GroupMember -> MemberInfo
memberInfo GroupMember {memberId, memberRole, memberProfile, activeConn} =
MemberInfo memberId memberRole memberChatVRange (fromLocalProfile memberProfile)
where
- memberChatVRange = ChatVersionRange . connChatVRange <$> activeConn
+ memberChatVRange = ChatVersionRange . peerChatVRange <$> activeConn
data ReceivedGroupInvitation = ReceivedGroupInvitation
{ fromMember :: GroupMember,
@@ -1167,7 +1167,7 @@ type ConnReqContact = ConnectionRequestUri 'CMContact
data Connection = Connection
{ connId :: Int64,
agentConnId :: AgentConnId,
- connChatVRange :: VersionRange,
+ peerChatVRange :: VersionRange,
connLevel :: Int,
viaContact :: Maybe Int64, -- group member contact ID, if not direct connection
viaUserContactLink :: Maybe Int64, -- user contact link ID, if connected via "user address"
diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs
index 57f508c71..1a740bef5 100644
--- a/src/Simplex/Chat/View.hs
+++ b/src/Simplex/Chat/View.hs
@@ -963,7 +963,7 @@ viewContactInfo ct@Contact {contactId, profile = LocalProfile {localAlias, conta
incognitoProfile
<> ["alias: " <> plain localAlias | localAlias /= ""]
<> [viewConnectionVerified (contactSecurityCode ct)]
- <> [viewConnChatVRange (connChatVRange activeConn)]
+ <> [viewPeerChatVRange (peerChatVRange activeConn)]
viewGroupInfo :: GroupInfo -> GroupSummary -> [StyledString]
viewGroupInfo GroupInfo {groupId} s =
@@ -979,14 +979,14 @@ viewGroupMemberInfo GroupInfo {groupId} m@GroupMember {groupMemberId, memberProf
<> maybe ["member not connected"] viewConnectionStats stats
<> ["alias: " <> plain localAlias | localAlias /= ""]
<> [viewConnectionVerified (memberSecurityCode m) | isJust stats]
- <> maybe [] (\ac -> [viewConnChatVRange (connChatVRange ac)]) activeConn
+ <> maybe [] (\ac -> [viewPeerChatVRange (peerChatVRange ac)]) activeConn
viewConnectionVerified :: Maybe SecurityCode -> StyledString
viewConnectionVerified (Just _) = "connection verified" -- TODO show verification time?
viewConnectionVerified _ = "connection not verified, use " <> highlight' "/code" <> " command to see security code"
-viewConnChatVRange :: VersionRange -> StyledString
-viewConnChatVRange (VersionRange minVer maxVer) = "chat protocol version range: (" <> sShow minVer <> ", " <> sShow maxVer <> ")"
+viewPeerChatVRange :: VersionRange -> StyledString
+viewPeerChatVRange (VersionRange minVer maxVer) = "peer chat protocol version range: (" <> sShow minVer <> ", " <> sShow maxVer <> ")"
viewConnectionStats :: ConnectionStats -> [StyledString]
viewConnectionStats ConnectionStats {rcvQueuesInfo, sndQueuesInfo} =
diff --git a/tests/ChatTests/Direct.hs b/tests/ChatTests/Direct.hs
index e7442d90a..4bb87b1e9 100644
--- a/tests/ChatTests/Direct.hs
+++ b/tests/ChatTests/Direct.hs
@@ -96,18 +96,18 @@ chatDirectTests = do
describe "delivery receipts" $ do
it "should send delivery receipts" testSendDeliveryReceipts
it "should send delivery receipts depending on configuration" testConfigureDeliveryReceipts
- describe "negotiate connection chat protocol version range" $ do
- describe "version range correctly set for new connection via invitation" $ do
+ describe "negotiate connection peer chat protocol version range" $ do
+ describe "peer version range correctly set for new connection via invitation" $ do
testInvVRange supportedChatVRange supportedChatVRange
testInvVRange supportedChatVRange vr11
testInvVRange vr11 supportedChatVRange
testInvVRange vr11 vr11
- describe "version range correctly set for new connection via contact request" $ do
+ describe "peer version range correctly set for new connection via contact request" $ do
testReqVRange supportedChatVRange supportedChatVRange
testReqVRange supportedChatVRange vr11
testReqVRange vr11 supportedChatVRange
testReqVRange vr11 vr11
- it "update connection version range on received messages" testUpdateConnChatVRange
+ it "update peer version range on received messages" testUpdatePeerChatVRange
where
testInvVRange vr1 vr2 = it (vRangeStr vr1 <> " - " <> vRangeStr vr2) $ testConnInvChatVRange vr1 vr2
testReqVRange vr1 vr2 = it (vRangeStr vr1 <> " - " <> vRangeStr vr2) $ testConnReqChatVRange vr1 vr2
@@ -2330,8 +2330,8 @@ testConnReqChatVRange ct1VRange ct2VRange tmp =
bob ##> "/i alice"
contactInfoChatVRange bob ct1VRange
-testUpdateConnChatVRange :: HasCallStack => FilePath -> IO ()
-testUpdateConnChatVRange tmp =
+testUpdatePeerChatVRange :: HasCallStack => FilePath -> IO ()
+testUpdatePeerChatVRange tmp =
withNewTestChat tmp "alice" aliceProfile $ \alice -> do
withNewTestChatCfg tmp cfg11 "bob" bobProfile $ \bob -> do
connectUsers alice bob
@@ -2378,4 +2378,4 @@ contactInfoChatVRange cc (VersionRange minVer maxVer) = do
cc <## "sending messages via: localhost"
cc <## "you've shared main profile with this contact"
cc <## "connection not verified, use /code command to see security code"
- cc <## ("chat protocol version range: (" <> show minVer <> ", " <> show maxVer <> ")")
+ cc <## ("peer chat protocol version range: (" <> show minVer <> ", " <> show maxVer <> ")")
diff --git a/tests/ChatTests/Utils.hs b/tests/ChatTests/Utils.hs
index 72cb99c97..c120d661f 100644
--- a/tests/ChatTests/Utils.hs
+++ b/tests/ChatTests/Utils.hs
@@ -528,7 +528,7 @@ startFileTransferWithDest' cc1 cc2 fileName fileSize fileDest_ = do
currentChatVRangeInfo :: String
currentChatVRangeInfo =
- "chat protocol version range: " <> vRangeStr supportedChatVRange
+ "peer chat protocol version range: " <> vRangeStr supportedChatVRange
vRangeStr :: VersionRange -> String
vRangeStr (VersionRange minVer maxVer) = "(" <> show minVer <> ", " <> show maxVer <> ")"
From b6c23b59caedb3e0bf160d891045a4c71dbb9fdc Mon Sep 17 00:00:00 2001
From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
Date: Wed, 6 Sep 2023 17:48:37 +0400
Subject: [PATCH 5/6] core: change logic of checking if peer supports feature
(check if peer version is compatible) (#3024)
---
cabal.project | 2 +-
scripts/nix/sha256map.nix | 2 +-
src/Simplex/Chat.hs | 27 ++++++++-------------------
src/Simplex/Chat/Protocol.hs | 6 +++---
stack.yaml | 2 +-
tests/Bots/DirectoryTests.hs | 18 +++++++++---------
tests/ChatClient.hs | 3 +--
tests/ChatTests/Groups.hs | 16 ++++++++--------
8 files changed, 32 insertions(+), 44 deletions(-)
diff --git a/cabal.project b/cabal.project
index b40b009b3..ec72b72fc 100644
--- a/cabal.project
+++ b/cabal.project
@@ -9,7 +9,7 @@ constraints: zip +disable-bzip2 +disable-zstd
source-repository-package
type: git
location: https://github.com/simplex-chat/simplexmq.git
- tag: 980e5c4d1ec15f44290542fd2a5d1c08456f00d1
+ tag: 351f42650c57f310fc1ea858ff9b7178823f1fd4
source-repository-package
type: git
diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix
index 09ac41e49..dbbc7475c 100644
--- a/scripts/nix/sha256map.nix
+++ b/scripts/nix/sha256map.nix
@@ -1,5 +1,5 @@
{
- "https://github.com/simplex-chat/simplexmq.git"."980e5c4d1ec15f44290542fd2a5d1c08456f00d1" = "1lqciyy215dvmbhykyp80bwipqmxybv39p6jff6vjgd5r34958nh";
+ "https://github.com/simplex-chat/simplexmq.git"."351f42650c57f310fc1ea858ff9b7178823f1fd4" = "12r13yc0qk9dkii58808862wraqrk66rzmkrgyp6lg1xrazrd0d2";
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
"https://github.com/kazu-yamamoto/http2.git"."b5a1b7200cf5bc7044af34ba325284271f6dff25" = "0dqb50j57an64nf4qcf5vcz4xkd1vzvghvf8bk529c1k30r9nfzb";
"https://github.com/simplex-chat/direct-sqlcipher.git"."34309410eb2069b029b8fc1872deb1e0db123294" = "0kwkmhyfsn2lixdlgl15smgr1h5gjk7fky6abzh8rng2h5ymnffd";
diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs
index b3ba94c8e..84f6645f9 100644
--- a/src/Simplex/Chat.hs
+++ b/src/Simplex/Chat.hs
@@ -1866,7 +1866,7 @@ processChatCommand = \case
xftpCfg <- readTVarIO =<< asks userXFTPFileConfig
fileSize <- liftIO $ CF.getFileContentsSize $ CryptoFile fsFilePath cfArgs
when (fromInteger fileSize > maxFileSize) $ throwChatError $ CEFileSize f
- let chunks = -((-fileSize) `div` fileChunkSize)
+ let chunks = - ((- fileSize) `div` fileChunkSize)
fileInline = inlineFileMode mc inlineFiles chunks n
fileMode = case xftpCfg of
Just cfg
@@ -2566,7 +2566,7 @@ cleanupManager = do
`catchChatError` (toView . CRChatError (Just user))
cleanupMessages = do
ts <- liftIO getCurrentTime
- let cutoffTs = addUTCTime (-(30 * nominalDay)) ts
+ let cutoffTs = addUTCTime (- (30 * nominalDay)) ts
withStoreCtx' (Just "cleanupManager, deleteOldMessages") (`deleteOldMessages` cutoffTs)
startProximateTimedItemThread :: ChatMonad m => User -> (ChatRef, ChatItemId) -> UTCTime -> m ()
@@ -3040,11 +3040,9 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
case cReq of
groupConnReq@(CRInvitationUri _ _) -> case cmdFunction of
-- [async agent commands] XGrpMemIntro continuation on receiving INV
- CFCreateConnGrpMemInv ->
- ifM
- (featureVersionSupported (peerChatVRange conn) groupNoDirectVersion)
- sendWithoutDirectCReq
- sendWithDirectCReq
+ CFCreateConnGrpMemInv
+ | isCompatibleRange (peerChatVRange conn) groupNoDirectVRange -> sendWithoutDirectCReq
+ | otherwise -> sendWithDirectCReq
where
sendWithoutDirectCReq = do
let GroupMember {groupMemberId, memberId} = m
@@ -4291,11 +4289,9 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
groupConnIds <- createConn
directConnIds <- case memberChatVRange of
Nothing -> Just <$> createConn
- Just mcvr ->
- ifM
- (featureVersionSupported (fromChatVRange mcvr) groupNoDirectVersion)
- (pure Nothing)
- (Just <$> createConn)
+ Just mcvr
+ | isCompatibleRange (fromChatVRange mcvr) groupNoDirectVRange -> pure Nothing
+ | otherwise -> Just <$> createConn
let customUserProfileId = if memberIncognito membership then Just (localProfileId $ memberProfile membership) else Nothing
void $ withStore $ \db -> createIntroReMember db user gInfo m memInfo groupConnIds directConnIds customUserProfileId
_ -> messageError "x.grp.mem.intro can be only sent by host member"
@@ -4488,13 +4484,6 @@ updatePeerChatVRange conn@Connection {connId, peerChatVRange} msgChatVRange
pure conn {peerChatVRange = msgChatVRange}
| otherwise = pure conn
-featureVersionSupported :: ChatMonad' m => VersionRange -> Version -> m Bool
-featureVersionSupported peerVRange v = do
- ChatConfig {chatVRange} <- asks config
- case chatVRange `compatibleVersion` peerVRange of
- Just (Compatible v') -> pure $ v' >= v
- Nothing -> pure False
-
parseFileDescription :: (ChatMonad m, FilePartyI p) => Text -> m (ValidFileDescription p)
parseFileDescription =
liftEither . first (ChatError . CEInvalidFileDescription) . (strDecode . encodeUtf8)
diff --git a/src/Simplex/Chat/Protocol.hs b/src/Simplex/Chat/Protocol.hs
index d104e002f..13692b57c 100644
--- a/src/Simplex/Chat/Protocol.hs
+++ b/src/Simplex/Chat/Protocol.hs
@@ -54,9 +54,9 @@ currentChatVersion = 2
supportedChatVRange :: VersionRange
supportedChatVRange = mkVersionRange 1 currentChatVersion
--- version that starts support for skipping establishing direct connections in a group
-groupNoDirectVersion :: Version
-groupNoDirectVersion = 3 -- 2
+-- version range that supports skipping establishing direct connections in a group
+groupNoDirectVRange :: VersionRange
+groupNoDirectVRange = mkVersionRange 2 currentChatVersion
data ConnectionEntity
= RcvDirectMsgConnection {entityConnection :: Connection, contact :: Maybe Contact}
diff --git a/stack.yaml b/stack.yaml
index c949cbb16..9c6b35432 100644
--- a/stack.yaml
+++ b/stack.yaml
@@ -49,7 +49,7 @@ extra-deps:
# - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561
# - ../simplexmq
- github: simplex-chat/simplexmq
- commit: 980e5c4d1ec15f44290542fd2a5d1c08456f00d1
+ commit: 351f42650c57f310fc1ea858ff9b7178823f1fd4
- github: kazu-yamamoto/http2
commit: b5a1b7200cf5bc7044af34ba325284271f6dff25
# - ../direct-sqlcipher
diff --git a/tests/Bots/DirectoryTests.hs b/tests/Bots/DirectoryTests.hs
index a0d69d195..f34ab042e 100644
--- a/tests/Bots/DirectoryTests.hs
+++ b/tests/Bots/DirectoryTests.hs
@@ -795,7 +795,7 @@ withDirectoryServiceCfg tmp cfg test = do
connectUsers ds superUser
ds ##> "/ad"
getContactLink ds True
- withDirectory tmp dsLink test
+ withDirectory tmp cfg dsLink test
restoreDirectoryService :: HasCallStack => FilePath -> Int -> Int -> (TestCC -> String -> IO ()) -> IO ()
restoreDirectoryService tmp ctCount grCount test = do
@@ -809,24 +809,24 @@ restoreDirectoryService tmp ctCount grCount test = do
dsLink <- getContactLink ds False
ds <## "auto_accept on"
pure dsLink
- withDirectory tmp dsLink test
+ withDirectory tmp testCfg dsLink test
-withDirectory :: HasCallStack => FilePath -> String -> (TestCC -> String -> IO ()) -> IO ()
-withDirectory tmp dsLink test = do
+withDirectory :: HasCallStack => FilePath -> ChatConfig -> String -> (TestCC -> String -> IO ()) -> IO ()
+withDirectory tmp cfg dsLink test = do
let opts = mkDirectoryOpts tmp [KnownContact 2 "alice"]
- runDirectory opts $
- withTestChat tmp "super_user" $ \superUser -> do
+ runDirectory cfg opts $
+ withTestChatCfg tmp cfg "super_user" $ \superUser -> do
superUser <## "1 contacts connected (use /cs for the list)"
test superUser dsLink
-runDirectory :: DirectoryOpts -> IO () -> IO ()
-runDirectory opts@DirectoryOpts {directoryLog} action = do
+runDirectory :: ChatConfig -> DirectoryOpts -> IO () -> IO ()
+runDirectory cfg opts@DirectoryOpts {directoryLog} action = do
st <- restoreDirectoryStore directoryLog
t <- forkIO $ bot st
threadDelay 500000
action `finally` (mapM_ hClose (directoryLogFile st) >> killThread t)
where
- bot st = simplexChatCore testCfg (mkChatOpts opts) Nothing $ directoryService st opts
+ bot st = simplexChatCore cfg (mkChatOpts opts) Nothing $ directoryService st opts
registerGroup :: TestCC -> TestCC -> String -> String -> IO ()
registerGroup su u n fn = registerGroupId su u n fn 1 1
diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs
index 43760c99b..9e5d4fe1c 100644
--- a/tests/ChatClient.hs
+++ b/tests/ChatClient.hs
@@ -22,7 +22,6 @@ import Simplex.Chat
import Simplex.Chat.Controller (ChatConfig (..), ChatController (..), ChatDatabase (..), ChatLogLevel (..))
import Simplex.Chat.Core
import Simplex.Chat.Options
-import Simplex.Chat.Protocol (groupNoDirectVersion)
import Simplex.Chat.Store
import Simplex.Chat.Store.Profiles
import Simplex.Chat.Terminal
@@ -142,7 +141,7 @@ mkCfgCreateGroupDirect :: ChatConfig -> ChatConfig
mkCfgCreateGroupDirect cfg = cfg {chatVRange = groupCreateDirectVRange}
groupCreateDirectVRange :: VersionRange
-groupCreateDirectVRange = mkVersionRange 1 (groupNoDirectVersion - 1)
+groupCreateDirectVRange = mkVersionRange 1 1
createTestChat :: FilePath -> ChatConfig -> ChatOpts -> String -> Profile -> IO TestCC
createTestChat tmp cfg opts@ChatOpts {coreOptions = CoreChatOpts {dbKey}} dbPrefix profile = do
diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs
index 82481e9df..23df9f460 100644
--- a/tests/ChatTests/Groups.hs
+++ b/tests/ChatTests/Groups.hs
@@ -68,17 +68,17 @@ chatGroupTests = do
it "should send delivery receipts in group depending on configuration" testConfigureGroupDeliveryReceipts
describe "direct connections in group are not established based on chat protocol version" $ do
describe "3 members group" $ do
- testNoDirect _0 _0 False -- True
- testNoDirect _0 _1 False
+ testNoDirect _0 _0 True
+ testNoDirect _0 _1 True
testNoDirect _1 _0 False
testNoDirect _1 _1 False
describe "4 members group" $ do
- testNoDirect4 _0 _0 _0 False False False -- True True True
- testNoDirect4 _0 _0 _1 False False False -- True False False
- testNoDirect4 _0 _1 _0 False False False -- False True False
- testNoDirect4 _0 _1 _1 False False False
- testNoDirect4 _1 _0 _0 False False False -- False False True
- testNoDirect4 _1 _0 _1 False False False
+ testNoDirect4 _0 _0 _0 True True True
+ testNoDirect4 _0 _0 _1 True True True
+ testNoDirect4 _0 _1 _0 True True False
+ testNoDirect4 _0 _1 _1 True True False
+ testNoDirect4 _1 _0 _0 False False True
+ testNoDirect4 _1 _0 _1 False False True
testNoDirect4 _1 _1 _0 False False False
testNoDirect4 _1 _1 _1 False False False
where
From 37eef3c6c99d69644650840c8a5b943eec839f79 Mon Sep 17 00:00:00 2001
From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
Date: Wed, 6 Sep 2023 18:05:42 +0400
Subject: [PATCH 6/6] core: enable creation of direct connections in group
(revert this instead of #3022) (#3025)
---
src/Simplex/Chat.hs | 4 ++--
tests/ChatTests/Groups.hs | 16 ++++++++--------
2 files changed, 10 insertions(+), 10 deletions(-)
diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs
index 84f6645f9..359e7f5b5 100644
--- a/src/Simplex/Chat.hs
+++ b/src/Simplex/Chat.hs
@@ -3041,7 +3041,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
groupConnReq@(CRInvitationUri _ _) -> case cmdFunction of
-- [async agent commands] XGrpMemIntro continuation on receiving INV
CFCreateConnGrpMemInv
- | isCompatibleRange (peerChatVRange conn) groupNoDirectVRange -> sendWithoutDirectCReq
+ | isCompatibleRange (peerChatVRange conn) groupNoDirectVRange -> sendWithDirectCReq -- sendWithoutDirectCReq
| otherwise -> sendWithDirectCReq
where
sendWithoutDirectCReq = do
@@ -4290,7 +4290,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
directConnIds <- case memberChatVRange of
Nothing -> Just <$> createConn
Just mcvr
- | isCompatibleRange (fromChatVRange mcvr) groupNoDirectVRange -> pure Nothing
+ | isCompatibleRange (fromChatVRange mcvr) groupNoDirectVRange -> Just <$> createConn -- pure Nothing
| otherwise -> Just <$> createConn
let customUserProfileId = if memberIncognito membership then Just (localProfileId $ memberProfile membership) else Nothing
void $ withStore $ \db -> createIntroReMember db user gInfo m memInfo groupConnIds directConnIds customUserProfileId
diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs
index 23df9f460..d476285fc 100644
--- a/tests/ChatTests/Groups.hs
+++ b/tests/ChatTests/Groups.hs
@@ -68,17 +68,17 @@ chatGroupTests = do
it "should send delivery receipts in group depending on configuration" testConfigureGroupDeliveryReceipts
describe "direct connections in group are not established based on chat protocol version" $ do
describe "3 members group" $ do
- testNoDirect _0 _0 True
- testNoDirect _0 _1 True
+ testNoDirect _0 _0 False -- True
+ testNoDirect _0 _1 False -- True
testNoDirect _1 _0 False
testNoDirect _1 _1 False
describe "4 members group" $ do
- testNoDirect4 _0 _0 _0 True True True
- testNoDirect4 _0 _0 _1 True True True
- testNoDirect4 _0 _1 _0 True True False
- testNoDirect4 _0 _1 _1 True True False
- testNoDirect4 _1 _0 _0 False False True
- testNoDirect4 _1 _0 _1 False False True
+ testNoDirect4 _0 _0 _0 False False False -- True True True
+ testNoDirect4 _0 _0 _1 False False False -- True True True
+ testNoDirect4 _0 _1 _0 False False False -- True True False
+ testNoDirect4 _0 _1 _1 False False False -- True True False
+ testNoDirect4 _1 _0 _0 False False False -- False False True
+ testNoDirect4 _1 _0 _1 False False False -- False False True
testNoDirect4 _1 _1 _0 False False False
testNoDirect4 _1 _1 _1 False False False
where