Merge pull request #2995 from simplex-chat/chat-version-negotiation

core: communicate connection chat version range; don't create direct connections in group (disabled)
This commit is contained in:
spaced4ndy 2023-09-06 18:06:57 +04:00 committed by GitHub
commit 5e8e4c295c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 1215 additions and 748 deletions

View File

@ -9,7 +9,7 @@ constraints: zip +disable-bzip2 +disable-zstd
source-repository-package source-repository-package
type: git type: git
location: https://github.com/simplex-chat/simplexmq.git location: https://github.com/simplex-chat/simplexmq.git
tag: 980e5c4d1ec15f44290542fd2a5d1c08456f00d1 tag: 351f42650c57f310fc1ea858ff9b7178823f1fd4
source-repository-package source-repository-package
type: git type: git

View File

@ -3,24 +3,31 @@ sequenceDiagram
participant A as Alice participant A as Alice
participant B as Bob participant B as Bob
participant C as Existing<br>contact participant C as Existing<br>contact
note over A, B: 1. send and accept group invitation note over A, B: 1. send and accept group invitation
A ->> B: x.grp.inv<br>invite Bob to group<br>(via contact connection) A ->> B: x.grp.inv<br>invite Bob to group<br>(via contact connection)
B ->> A: x.grp.acpt<br>accept invitation<br>(via member connection) B ->> A: x.grp.acpt<br>accept invitation<br>(via member connection)<br>establish group member connection
B ->> A: establish group member connection
note over M, B: 2. introduce new member Bob to all existing members note over M, B: 2. introduce new member Bob to all existing members
A ->> M: x.grp.mem.new<br>"announce" Bob<br>to existing members<br>(via member connections) A ->> M: x.grp.mem.new<br>"announce" Bob<br>to existing members<br>(via member connections)
A ->> B: x.grp.mem.intro * N<br>"introduce" members<br>(via member connection) loop batched
B ->> A: x.grp.mem.inv * N<br>"invitations" to connect<br>for all members<br>(via member connection) A ->> B: x.grp.mem.intro * N<br>"introduce" members and<br>their chat protocol versions<br>(via member connection)
A ->> M: x.grp.mem.fwd<br>forward "invitations"<br>to all members<br>(via member connections) 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<br>"invitations" to connect<br>for all members<br>(via member connection)
end
A ->> M: x.grp.mem.fwd<br>forward "invitations" and<br>Bob's chat protocol version<br>to all members<br>(via member connections)
note over M, B: 3. establish direct and group member connections note over M, B: 3. establish direct and group member connections
M ->> B: establish group member connection M ->> B: establish group member connection
M ->> B: establish direct connection
note over M, C: 4. deduplicate new contact opt chat protocol compatible version < 2
B ->> M: x.info.probe<br>"probe" is sent to all new members M ->> B: establish direct connection
B ->> C: x.info.probe.check<br>"probe" hash,<br>in case contact and<br>member profiles match note over M, C: 4. deduplicate new contact
C ->> B: x.info.probe.ok<br> original "probe",<br> in case contact and member<br>are the same user B ->> M: x.info.probe<br>"probe" is sent to all new members
note over B: merge existing and new contacts if received and sent probe hashes match B ->> C: x.info.probe.check<br>"probe" hash,<br>in case contact and<br>member profiles match
C ->> B: x.info.probe.ok<br> original "probe",<br> in case contact and member<br>are the same user
note over B: merge existing and new contacts if received and sent probe hashes match
end

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

View File

@ -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/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
"https://github.com/kazu-yamamoto/http2.git"."b5a1b7200cf5bc7044af34ba325284271f6dff25" = "0dqb50j57an64nf4qcf5vcz4xkd1vzvghvf8bk529c1k30r9nfzb"; "https://github.com/kazu-yamamoto/http2.git"."b5a1b7200cf5bc7044af34ba325284271f6dff25" = "0dqb50j57an64nf4qcf5vcz4xkd1vzvghvf8bk529c1k30r9nfzb";
"https://github.com/simplex-chat/direct-sqlcipher.git"."34309410eb2069b029b8fc1872deb1e0db123294" = "0kwkmhyfsn2lixdlgl15smgr1h5gjk7fky6abzh8rng2h5ymnffd"; "https://github.com/simplex-chat/direct-sqlcipher.git"."34309410eb2069b029b8fc1872deb1e0db123294" = "0kwkmhyfsn2lixdlgl15smgr1h5gjk7fky6abzh8rng2h5ymnffd";

View File

@ -109,6 +109,7 @@ library
Simplex.Chat.Migrations.M20230721_group_snd_item_statuses Simplex.Chat.Migrations.M20230721_group_snd_item_statuses
Simplex.Chat.Migrations.M20230814_indexes Simplex.Chat.Migrations.M20230814_indexes
Simplex.Chat.Migrations.M20230827_file_encryption Simplex.Chat.Migrations.M20230827_file_encryption
Simplex.Chat.Migrations.M20230829_connections_chat_vrange
Simplex.Chat.Mobile Simplex.Chat.Mobile
Simplex.Chat.Mobile.File Simplex.Chat.Mobile.File
Simplex.Chat.Mobile.Shared Simplex.Chat.Mobile.Shared

View File

@ -94,6 +94,7 @@ import qualified Simplex.Messaging.Protocol as SMP
import qualified Simplex.Messaging.TMap as TM import qualified Simplex.Messaging.TMap as TM
import Simplex.Messaging.Transport.Client (defaultSocksProxy) import Simplex.Messaging.Transport.Client (defaultSocksProxy)
import Simplex.Messaging.Util import Simplex.Messaging.Util
import Simplex.Messaging.Version
import System.Exit (exitFailure, exitSuccess) import System.Exit (exitFailure, exitSuccess)
import System.FilePath (combine, splitExtensions, takeFileName, (</>)) import System.FilePath (combine, splitExtensions, takeFileName, (</>))
import System.IO (Handle, IOMode (..), SeekMode (..), hFlush, stdout) import System.IO (Handle, IOMode (..), SeekMode (..), hFlush, stdout)
@ -113,6 +114,7 @@ defaultChatConfig =
{ tcpPort = undefined, -- agent does not listen to TCP { tcpPort = undefined, -- agent does not listen to TCP
tbqSize = 1024 tbqSize = 1024
}, },
chatVRange = supportedChatVRange,
confirmMigrations = MCConsole, confirmMigrations = MCConsole,
defaultServers = defaultServers =
DefaultAgentServers DefaultAgentServers
@ -1296,7 +1298,8 @@ processChatCommand = \case
-- [incognito] generate profile to send -- [incognito] generate profile to send
incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing
let profileToSend = userProfileToSend user incognitoProfile 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 conn <- withStore' $ \db -> createDirectConnection db user connId cReq ConnJoined $ incognitoProfile $> profileToSend
toView $ CRNewContactConnection user conn toView $ CRNewContactConnection user conn
pure $ CRSentConfirmation user pure $ CRSentConfirmation user
@ -1434,11 +1437,16 @@ processChatCommand = \case
Nothing -> throwChatError $ CEGroupCantResendInvitation gInfo cName Nothing -> throwChatError $ CEGroupCantResendInvitation gInfo cName
| otherwise -> throwChatError $ CEGroupDuplicateMember cName | otherwise -> throwChatError $ CEGroupDuplicateMember cName
APIJoinGroup groupId -> withUser $ \user@User {userId} -> do 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 {peerChatVRange}} = ct
withChatLock "joinGroup" . procCmd $ do 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 withStore' $ \db -> do
createMemberConnection db userId fromMember agentConnId createMemberConnection db userId fromMember agentConnId peerChatVRange
updateGroupMemberStatus db userId fromMember GSMemAccepted updateGroupMemberStatus db userId fromMember GSMemAccepted
updateGroupMemberStatus db userId membership GSMemAccepted updateGroupMemberStatus db userId membership GSMemAccepted
updateCIGroupInvitationStatus user updateCIGroupInvitationStatus user
@ -1840,7 +1848,8 @@ processChatCommand = \case
-- [incognito] generate profile to send -- [incognito] generate profile to send
incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing
let profileToSend = userProfileToSend user incognitoProfile 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 let groupLinkId = crClientData >>= decodeJSON >>= \(CRDataGroup gli) -> Just gli
conn <- withStore' $ \db -> createConnReqConnection db userId connId cReqHash xContactId incognitoProfile groupLinkId conn <- withStore' $ \db -> createConnReqConnection db userId connId cReqHash xContactId incognitoProfile groupLinkId
toView $ CRNewContactConnection user conn toView $ CRNewContactConnection user conn
@ -1857,7 +1866,7 @@ processChatCommand = \case
xftpCfg <- readTVarIO =<< asks userXFTPFileConfig xftpCfg <- readTVarIO =<< asks userXFTPFileConfig
fileSize <- liftIO $ CF.getFileContentsSize $ CryptoFile fsFilePath cfArgs fileSize <- liftIO $ CF.getFileContentsSize $ CryptoFile fsFilePath cfArgs
when (fromInteger fileSize > maxFileSize) $ throwChatError $ CEFileSize f when (fromInteger fileSize > maxFileSize) $ throwChatError $ CEFileSize f
let chunks = -((-fileSize) `div` fileChunkSize) let chunks = - ((- fileSize) `div` fileChunkSize)
fileInline = inlineFileMode mc inlineFiles chunks n fileInline = inlineFileMode mc inlineFiles chunks n
fileMode = case xftpCfg of fileMode = case xftpCfg of
Just cfg Just cfg
@ -2231,7 +2240,7 @@ acceptFileReceive user@User {userId} RcvFileTransfer {fileId, xftpRcvFile, fileI
case (xftpRcvFile, fileConnReq) of case (xftpRcvFile, fileConnReq) of
-- direct file protocol -- direct file protocol
(Nothing, Just connReq) -> do (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 filePath <- getRcvFilePath fileId filePath_ fName True
withStoreCtx (Just "acceptFileReceive, acceptRcvFileTransfer") $ \db -> acceptRcvFileTransfer db user fileId connIds ConnJoined filePath withStoreCtx (Just "acceptFileReceive, acceptRcvFileTransfer") $ \db -> acceptRcvFileTransfer db user fileId connIds ConnJoined filePath
-- XFTP -- XFTP
@ -2346,17 +2355,18 @@ getRcvFilePath fileId fPath_ fn keepHandle = case fPath_ of
in ifM (doesFileExist f) (tryCombine $ n + 1) (pure f) in ifM (doesFileExist f) (tryCombine $ n + 1) (pure f)
acceptContactRequest :: ChatMonad m => User -> UserContactRequest -> Maybe IncognitoProfile -> m Contact 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 let profileToSend = profileToSendOnAccept user incognitoProfile
acId <- withAgent $ \a -> acceptContact a True invId . directMessage $ XInfo profileToSend dm <- directMessage $ XInfo profileToSend
withStore' $ \db -> createAcceptedContact db user acId cName profileId cp userContactLinkId xContactId incognitoProfile 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 :: 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 let profileToSend = profileToSendOnAccept user incognitoProfile
(cmdId, acId) <- agentAcceptContactAsync user True invId $ XInfo profileToSend (cmdId, acId) <- agentAcceptContactAsync user True invId $ XInfo profileToSend
withStore' $ \db -> do 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 setCommandConnId db user cmdId connId
pure ct pure ct
@ -2556,7 +2566,7 @@ cleanupManager = do
`catchChatError` (toView . CRChatError (Just user)) `catchChatError` (toView . CRChatError (Just user))
cleanupMessages = do cleanupMessages = do
ts <- liftIO getCurrentTime ts <- liftIO getCurrentTime
let cutoffTs = addUTCTime (-(30 * nominalDay)) ts let cutoffTs = addUTCTime (- (30 * nominalDay)) ts
withStoreCtx' (Just "cleanupManager, deleteOldMessages") (`deleteOldMessages` cutoffTs) withStoreCtx' (Just "cleanupManager, deleteOldMessages") (`deleteOldMessages` cutoffTs)
startProximateTimedItemThread :: ChatMonad m => User -> (ChatRef, ChatItemId) -> UTCTime -> m () startProximateTimedItemThread :: ChatMonad m => User -> (ChatRef, ChatItemId) -> UTCTime -> m ()
@ -2840,21 +2850,23 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
_ -> Nothing _ -> Nothing
processDirectMessage :: ACommand 'Agent e -> ConnectionEntity -> Connection -> Maybe Contact -> m () 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, peerChatVRange, viaUserContactLink, groupLinkId, customUserProfileId, connectionCode} = \case
Nothing -> case agentMsg of Nothing -> case agentMsg of
CONF confId _ connInfo -> do CONF confId _ connInfo -> do
-- [incognito] send saved profile -- [incognito] send saved profile
incognitoProfile <- forM customUserProfileId $ \profileId -> withStore (\db -> getProfileById db userId profileId) incognitoProfile <- forM customUserProfileId $ \profileId -> withStore (\db -> getProfileById db userId profileId)
let profileToSend = userProfileToSend user (fromLocalProfile <$> incognitoProfile) Nothing 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 -- [async agent commands] no continuation needed, but command should be asynchronous for stability
allowAgentConnectionAsync user conn confId $ XInfo profileToSend allowAgentConnectionAsync user conn' confId $ XInfo profileToSend
INFO connInfo -> INFO connInfo -> do
saveConnInfo conn connInfo _conn' <- saveConnInfo conn connInfo
pure ()
MSG meta _msgFlags msgBody -> do MSG meta _msgFlags msgBody -> do
cmdId <- createAckCmd conn cmdId <- createAckCmd conn
withAckMessage agentConnId cmdId meta $ withAckMessage agentConnId cmdId meta $ do
saveRcvMSG conn (ConnectionId connId) meta msgBody cmdId $> False (_conn', _) <- saveRcvMSG conn (ConnectionId connId) meta msgBody cmdId
pure False
SENT msgId -> SENT msgId ->
sentMsgDeliveryEvent conn msgId sentMsgDeliveryEvent conn msgId
OK -> OK ->
@ -2879,54 +2891,57 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
setConnConnReqInv db user connId cReq setConnConnReqInv db user connId cReq
getXGrpMemIntroContDirect db user ct getXGrpMemIntroContDirect db user ct
forM_ contData $ \(hostConnId, xGrpMemIntroCont) -> forM_ contData $ \(hostConnId, xGrpMemIntroCont) ->
sendXGrpMemInv hostConnId directConnReq xGrpMemIntroCont sendXGrpMemInv hostConnId (Just directConnReq) xGrpMemIntroCont
CRContactUri _ -> throwChatError $ CECommandError "unexpected ConnectionRequestUri type" CRContactUri _ -> throwChatError $ CECommandError "unexpected ConnectionRequestUri type"
MSG msgMeta _msgFlags msgBody -> do MSG msgMeta _msgFlags msgBody -> do
cmdId <- createAckCmd conn cmdId <- createAckCmd conn
withAckMessage agentConnId cmdId msgMeta $ do withAckMessage agentConnId cmdId msgMeta $ do
msg@RcvMessage {chatMsgEvent = ACME _ event} <- saveRcvMSG conn (ConnectionId connId) msgMeta msgBody cmdId (conn', msg@RcvMessage {chatMsgEvent = ACME _ event}) <- saveRcvMSG conn (ConnectionId connId) msgMeta msgBody cmdId
assertDirectAllowed user MDRcv ct $ toCMEventTag event let ct' = ct {activeConn = conn'} :: Contact
assertDirectAllowed user MDRcv ct' $ toCMEventTag event
updateChatLock "directMessage" event updateChatLock "directMessage" event
case event of case event of
XMsgNew mc -> newContentMessage ct mc msg msgMeta XMsgNew mc -> newContentMessage ct' mc msg msgMeta
XMsgFileDescr sharedMsgId fileDescr -> messageFileDescription ct sharedMsgId fileDescr msgMeta XMsgFileDescr sharedMsgId fileDescr -> messageFileDescription ct' sharedMsgId fileDescr msgMeta
XMsgFileCancel sharedMsgId -> cancelMessageFile ct sharedMsgId msgMeta XMsgFileCancel sharedMsgId -> cancelMessageFile ct' sharedMsgId msgMeta
XMsgUpdate sharedMsgId mContent ttl live -> messageUpdate ct sharedMsgId mContent msg msgMeta ttl live XMsgUpdate sharedMsgId mContent ttl live -> messageUpdate ct' sharedMsgId mContent msg msgMeta ttl live
XMsgDel sharedMsgId _ -> messageDelete ct sharedMsgId msg msgMeta XMsgDel sharedMsgId _ -> messageDelete ct' sharedMsgId msg msgMeta
XMsgReact sharedMsgId _ reaction add -> directMsgReaction ct sharedMsgId reaction add msg msgMeta XMsgReact sharedMsgId _ reaction add -> directMsgReaction ct' sharedMsgId reaction add msg msgMeta
-- TODO discontinue XFile -- TODO discontinue XFile
XFile fInv -> processFileInvitation' ct fInv msg msgMeta XFile fInv -> processFileInvitation' ct' fInv msg msgMeta
XFileCancel sharedMsgId -> xFileCancel ct sharedMsgId msgMeta XFileCancel sharedMsgId -> xFileCancel ct' sharedMsgId msgMeta
XFileAcptInv sharedMsgId fileConnReq_ fName -> xFileAcptInv ct sharedMsgId fileConnReq_ fName msgMeta XFileAcptInv sharedMsgId fileConnReq_ fName -> xFileAcptInv ct' sharedMsgId fileConnReq_ fName msgMeta
XInfo p -> xInfo ct p XInfo p -> xInfo ct' p
XGrpInv gInv -> processGroupInvitation ct gInv msg msgMeta XGrpInv gInv -> processGroupInvitation ct' gInv msg msgMeta
XInfoProbe probe -> xInfoProbe ct probe XInfoProbe probe -> xInfoProbe ct' probe
XInfoProbeCheck probeHash -> xInfoProbeCheck ct probeHash XInfoProbeCheck probeHash -> xInfoProbeCheck ct' probeHash
XInfoProbeOk probe -> xInfoProbeOk ct probe XInfoProbeOk probe -> xInfoProbeOk ct' probe
XCallInv callId invitation -> xCallInv ct callId invitation msg msgMeta XCallInv callId invitation -> xCallInv ct' callId invitation msg msgMeta
XCallOffer callId offer -> xCallOffer ct callId offer msg msgMeta XCallOffer callId offer -> xCallOffer ct' callId offer msg msgMeta
XCallAnswer callId answer -> xCallAnswer ct callId answer msg msgMeta XCallAnswer callId answer -> xCallAnswer ct' callId answer msg msgMeta
XCallExtra callId extraInfo -> xCallExtra ct callId extraInfo msg msgMeta XCallExtra callId extraInfo -> xCallExtra ct' callId extraInfo msg msgMeta
XCallEnd callId -> xCallEnd ct callId msg msgMeta XCallEnd callId -> xCallEnd ct' callId msg msgMeta
BFileChunk sharedMsgId chunk -> bFileChunk ct sharedMsgId chunk msgMeta BFileChunk sharedMsgId chunk -> bFileChunk ct' sharedMsgId chunk msgMeta
_ -> messageError $ "unsupported message: " <> T.pack (show event) _ -> 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) pure $ fromMaybe (sendRcptsContacts user) sendRcpts && hasDeliveryReceipt (toCMEventTag event)
RCVD msgMeta msgRcpt -> RCVD msgMeta msgRcpt ->
withAckMessage' agentConnId conn msgMeta $ withAckMessage' agentConnId conn msgMeta $
directMsgReceived ct conn msgMeta msgRcpt directMsgReceived ct conn msgMeta msgRcpt
CONF confId _ connInfo -> do CONF confId _ connInfo -> do
-- confirming direct connection with a member -- confirming direct connection with a member
ChatMessage {chatMsgEvent} <- parseChatMessage conn connInfo ChatMessage {chatVRange, chatMsgEvent} <- parseChatMessage conn connInfo
conn' <- updatePeerChatVRange conn chatVRange
case chatMsgEvent of case chatMsgEvent of
XGrpMemInfo _memId _memProfile -> do XGrpMemInfo _memId _memProfile -> do
-- TODO check member ID -- TODO check member ID
-- TODO update member profile -- TODO update member profile
-- [async agent commands] no continuation needed, but command should be asynchronous for stability -- [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" _ -> messageError "CONF from member must have x.grp.mem.info"
INFO connInfo -> do INFO connInfo -> do
ChatMessage {chatMsgEvent} <- parseChatMessage conn connInfo ChatMessage {chatVRange, chatMsgEvent} <- parseChatMessage conn connInfo
_conn' <- updatePeerChatVRange conn chatVRange
case chatMsgEvent of case chatMsgEvent of
XGrpMemInfo _memId _memProfile -> do XGrpMemInfo _memId _memProfile -> do
-- TODO check member ID -- TODO check member ID
@ -2958,7 +2973,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
forM_ groupId_ $ \groupId -> do forM_ groupId_ $ \groupId -> do
gVar <- asks idsDrg gVar <- asks idsDrg
groupConnIds <- createAgentConnectionAsync user CFCreateConnGrpInv True SCMInvitation 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 peerChatVRange
_ -> pure () _ -> pure ()
Just (gInfo@GroupInfo {membership}, m@GroupMember {activeConn}) -> Just (gInfo@GroupInfo {membership}, m@GroupMember {activeConn}) ->
when (maybe False ((== ConnReady) . connStatus) activeConn) $ do when (maybe False ((== ConnReady) . connStatus) activeConn) $ do
@ -3025,22 +3040,30 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
case cReq of case cReq of
groupConnReq@(CRInvitationUri _ _) -> case cmdFunction of groupConnReq@(CRInvitationUri _ _) -> case cmdFunction of
-- [async agent commands] XGrpMemIntro continuation on receiving INV -- [async agent commands] XGrpMemIntro continuation on receiving INV
CFCreateConnGrpMemInv -> do CFCreateConnGrpMemInv
contData <- withStore' $ \db -> do | isCompatibleRange (peerChatVRange conn) groupNoDirectVRange -> sendWithDirectCReq -- sendWithoutDirectCReq
setConnConnReqInv db user connId cReq | otherwise -> sendWithDirectCReq
getXGrpMemIntroContGroup db user m where
forM_ contData $ \(hostConnId, directConnReq) -> do sendWithoutDirectCReq = do
let GroupMember {groupMemberId, memberId} = m let GroupMember {groupMemberId, memberId} = m
sendXGrpMemInv hostConnId directConnReq XGrpMemIntroCont {groupId, groupMemberId, memberId, groupConnReq} 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 -- [async agent commands] group link auto-accept continuation on receiving INV
CFCreateConnGrpInv -> CFCreateConnGrpInv -> do
withStore' (\db -> getContactViaMember db user m) >>= \case ct <- withStore $ \db -> getContactViaMember db user m
Nothing -> messageError "implementation error: invitee does not have contact" withStore' $ \db -> setNewContactMemberConnRequest db user m cReq
Just ct -> do groupLinkId <- withStore' $ \db -> getGroupLinkId db user gInfo
withStore' $ \db -> setNewContactMemberConnRequest db user m cReq sendGrpInvitation ct m groupLinkId
groupLinkId <- withStore' $ \db -> getGroupLinkId db user gInfo toView $ CRSentGroupInvitation user gInfo ct m
sendGrpInvitation ct m groupLinkId
toView $ CRSentGroupInvitation user gInfo ct m
where where
sendGrpInvitation :: Contact -> GroupMember -> Maybe GroupLinkId -> m () sendGrpInvitation :: Contact -> GroupMember -> Maybe GroupLinkId -> m ()
sendGrpInvitation ct GroupMember {memberId, memberRole = memRole} groupLinkId = do sendGrpInvitation ct GroupMember {memberId, memberRole = memRole} groupLinkId = do
@ -3052,7 +3075,8 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
_ -> throwChatError $ CECommandError "unexpected cmdFunction" _ -> throwChatError $ CECommandError "unexpected cmdFunction"
CRContactUri _ -> throwChatError $ CECommandError "unexpected ConnectionRequestUri type" CRContactUri _ -> throwChatError $ CECommandError "unexpected ConnectionRequestUri type"
CONF confId _ connInfo -> do CONF confId _ connInfo -> do
ChatMessage {chatMsgEvent} <- parseChatMessage conn connInfo ChatMessage {chatVRange, chatMsgEvent} <- parseChatMessage conn connInfo
conn' <- updatePeerChatVRange conn chatVRange
case memberCategory m of case memberCategory m of
GCInviteeMember -> GCInviteeMember ->
case chatMsgEvent of case chatMsgEvent of
@ -3060,7 +3084,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
| sameMemberId memId m -> do | sameMemberId memId m -> do
withStore $ \db -> liftIO $ updateGroupMemberStatus db userId m GSMemAccepted withStore $ \db -> liftIO $ updateGroupMemberStatus db userId m GSMemAccepted
-- [async agent commands] no continuation needed, but command should be asynchronous for stability -- [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" | otherwise -> messageError "x.grp.acpt: memberId is different from expected"
_ -> messageError "CONF from invited member must have x.grp.acpt" _ -> messageError "CONF from invited member must have x.grp.acpt"
_ -> _ ->
@ -3069,11 +3093,12 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
| sameMemberId memId m -> do | sameMemberId memId m -> do
-- TODO update member profile -- TODO update member profile
-- [async agent commands] no continuation needed, but command should be asynchronous for stability -- [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" | otherwise -> messageError "x.grp.mem.info: memberId is different from expected"
_ -> messageError "CONF from member must have x.grp.mem.info" _ -> messageError "CONF from member must have x.grp.mem.info"
INFO connInfo -> do INFO connInfo -> do
ChatMessage {chatMsgEvent} <- parseChatMessage conn connInfo ChatMessage {chatVRange, chatMsgEvent} <- parseChatMessage conn connInfo
_conn' <- updatePeerChatVRange conn chatVRange
case chatMsgEvent of case chatMsgEvent of
XGrpMemInfo memId _memProfile XGrpMemInfo memId _memProfile
| sameMemberId memId m -> do | sameMemberId memId m -> do
@ -3114,7 +3139,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
processIntro intro `catchChatError` (toView . CRChatError (Just user)) processIntro intro `catchChatError` (toView . CRChatError (Just user))
where where
processIntro intro@GroupMemberIntro {introId} = do 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 withStore' $ \db -> updateIntroStatus db introId GMIntroSent
_ -> do _ -> do
-- TODO send probe and decide whether to use existing contact connection or the new contact connection -- TODO send probe and decide whether to use existing contact connection or the new contact connection
@ -3131,28 +3156,29 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
MSG msgMeta _msgFlags msgBody -> do MSG msgMeta _msgFlags msgBody -> do
cmdId <- createAckCmd conn cmdId <- createAckCmd conn
withAckMessage agentConnId cmdId msgMeta $ do 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 updateChatLock "groupMessage" event
case event of case event of
XMsgNew mc -> canSend $ newGroupContentMessage gInfo m mc msg msgMeta XMsgNew mc -> canSend m' $ newGroupContentMessage gInfo m' mc msg msgMeta
XMsgFileDescr sharedMsgId fileDescr -> canSend $ groupMessageFileDescription gInfo m sharedMsgId fileDescr msgMeta XMsgFileDescr sharedMsgId fileDescr -> canSend m' $ groupMessageFileDescription gInfo m' sharedMsgId fileDescr msgMeta
XMsgFileCancel sharedMsgId -> cancelGroupMessageFile gInfo m sharedMsgId msgMeta XMsgFileCancel sharedMsgId -> cancelGroupMessageFile gInfo m' sharedMsgId msgMeta
XMsgUpdate sharedMsgId mContent ttl live -> canSend $ groupMessageUpdate gInfo m sharedMsgId mContent msg msgMeta ttl live 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 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 XMsgReact sharedMsgId (Just memberId) reaction add -> groupMsgReaction gInfo m' sharedMsgId memberId reaction add msg msgMeta
-- TODO discontinue XFile -- TODO discontinue XFile
XFile fInv -> processGroupFileInvitation' gInfo m fInv msg msgMeta XFile fInv -> processGroupFileInvitation' gInfo m' fInv msg msgMeta
XFileCancel sharedMsgId -> xFileCancelGroup gInfo m sharedMsgId msgMeta XFileCancel sharedMsgId -> xFileCancelGroup gInfo m' sharedMsgId msgMeta
XFileAcptInv sharedMsgId fileConnReq_ fName -> xFileAcptInvGroup gInfo m sharedMsgId fileConnReq_ fName msgMeta XFileAcptInv sharedMsgId fileConnReq_ fName -> xFileAcptInvGroup gInfo m' sharedMsgId fileConnReq_ fName msgMeta
XGrpMemNew memInfo -> xGrpMemNew gInfo m memInfo msg msgMeta XGrpMemNew memInfo -> xGrpMemNew gInfo m' memInfo msg msgMeta
XGrpMemIntro memInfo -> xGrpMemIntro gInfo m memInfo XGrpMemIntro memInfo -> xGrpMemIntro gInfo m' memInfo
XGrpMemInv memId introInv -> xGrpMemInv gInfo m memId introInv XGrpMemInv memId introInv -> xGrpMemInv gInfo m' memId introInv
XGrpMemFwd memInfo introInv -> xGrpMemFwd gInfo m memInfo introInv XGrpMemFwd memInfo introInv -> xGrpMemFwd gInfo m' memInfo introInv
XGrpMemRole memId memRole -> xGrpMemRole gInfo m memId memRole msg msgMeta XGrpMemRole memId memRole -> xGrpMemRole gInfo m' memId memRole msg msgMeta
XGrpMemDel memId -> xGrpMemDel gInfo m memId msg msgMeta XGrpMemDel memId -> xGrpMemDel gInfo m' memId msg msgMeta
XGrpLeave -> xGrpLeave gInfo m msg msgMeta XGrpLeave -> xGrpLeave gInfo m' msg msgMeta
XGrpDel -> xGrpDel gInfo m msg msgMeta XGrpDel -> xGrpDel gInfo m' msg msgMeta
XGrpInfo p' -> xGrpInfo gInfo m p' msg msgMeta XGrpInfo p' -> xGrpInfo gInfo m' p' msg msgMeta
BFileChunk sharedMsgId chunk -> bFileChunkGroup gInfo sharedMsgId chunk msgMeta BFileChunk sharedMsgId chunk -> bFileChunkGroup gInfo sharedMsgId chunk msgMeta
_ -> messageError $ "unsupported message: " <> T.pack (show event) _ -> messageError $ "unsupported message: " <> T.pack (show event)
currentMemCount <- withStore' $ \db -> getGroupCurrentMembersCount db user gInfo currentMemCount <- withStore' $ \db -> getGroupCurrentMembersCount db user gInfo
@ -3162,8 +3188,8 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
&& hasDeliveryReceipt (toCMEventTag event) && hasDeliveryReceipt (toCMEventTag event)
&& currentMemCount <= smallGroupsRcptsMemLimit && currentMemCount <= smallGroupsRcptsMemLimit
where where
canSend a canSend mem a
| memberRole (m :: GroupMember) <= GRObserver = messageError "member is not allowed to send messages" | memberRole (mem :: GroupMember) <= GRObserver = messageError "member is not allowed to send messages"
| otherwise = a | otherwise = a
RCVD msgMeta msgRcpt -> RCVD msgMeta msgRcpt ->
withAckMessage' agentConnId conn msgMeta $ withAckMessage' agentConnId conn msgMeta $
@ -3248,14 +3274,15 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
-- SMP CONF for SndFileConnection happens for direct file protocol -- SMP CONF for SndFileConnection happens for direct file protocol
-- when recipient of the file "joins" connection created by the sender -- when recipient of the file "joins" connection created by the sender
CONF confId _ connInfo -> do CONF confId _ connInfo -> do
ChatMessage {chatMsgEvent} <- parseChatMessage conn connInfo ChatMessage {chatVRange, chatMsgEvent} <- parseChatMessage conn connInfo
conn' <- updatePeerChatVRange conn chatVRange
case chatMsgEvent of case chatMsgEvent of
-- TODO save XFileAcpt message -- TODO save XFileAcpt message
XFileAcpt name XFileAcpt name
| name == fileName -> do | name == fileName -> do
withStore' $ \db -> updateSndFileStatus db ft FSAccepted withStore' $ \db -> updateSndFileStatus db ft FSAccepted
-- [async agent commands] no continuation needed, but command should be asynchronous for stability -- [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" | otherwise -> messageError "x.file.acpt: fileName is different from expected"
_ -> messageError "CONF from file connection must have x.file.acpt" _ -> messageError "CONF from file connection must have x.file.acpt"
CON -> do CON -> do
@ -3316,9 +3343,10 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
-- when sender of the file "joins" connection created by the recipient -- when sender of the file "joins" connection created by the recipient
-- (sender doesn't create connections for all group members) -- (sender doesn't create connections for all group members)
CONF confId _ connInfo -> do CONF confId _ connInfo -> do
ChatMessage {chatMsgEvent} <- parseChatMessage conn connInfo ChatMessage {chatVRange, chatMsgEvent} <- parseChatMessage conn connInfo
conn' <- updatePeerChatVRange conn chatVRange
case chatMsgEvent of 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 () _ -> pure ()
CON -> startReceivingFile user fileId CON -> startReceivingFile user fileId
MSG meta _ msgBody -> do MSG meta _ msgBody -> do
@ -3377,10 +3405,10 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
processUserContactRequest :: ACommand 'Agent e -> ConnectionEntity -> Connection -> UserContact -> m () processUserContactRequest :: ACommand 'Agent e -> ConnectionEntity -> Connection -> UserContact -> m ()
processUserContactRequest agentMsg connEntity conn UserContact {userContactLinkId} = case agentMsg of processUserContactRequest agentMsg connEntity conn UserContact {userContactLinkId} = case agentMsg of
REQ invId _ connInfo -> do REQ invId _ connInfo -> do
ChatMessage {chatMsgEvent} <- parseChatMessage conn connInfo ChatMessage {chatVRange, chatMsgEvent} <- parseChatMessage conn connInfo
case chatMsgEvent of case chatMsgEvent of
XContact p xContactId_ -> profileContactRequest invId p xContactId_ XContact p xContactId_ -> profileContactRequest invId chatVRange p xContactId_
XInfo p -> profileContactRequest invId p Nothing XInfo p -> profileContactRequest invId chatVRange p Nothing
-- TODO show/log error, other events in contact request -- TODO show/log error, other events in contact request
_ -> pure () _ -> pure ()
MERR _ err -> do MERR _ err -> do
@ -3392,9 +3420,9 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
-- TODO add debugging output -- TODO add debugging output
_ -> pure () _ -> pure ()
where where
profileContactRequest :: InvitationId -> Profile -> Maybe XContactId -> m () profileContactRequest :: InvitationId -> VersionRange -> Profile -> Maybe XContactId -> m ()
profileContactRequest invId p xContactId_ = do profileContactRequest invId chatVRange p xContactId_ = do
withStore (\db -> createOrUpdateContactRequest db user userContactLinkId invId p xContactId_) >>= \case withStore (\db -> createOrUpdateContactRequest db user userContactLinkId invId chatVRange p xContactId_) >>= \case
CORContact contact -> toView $ CRContactRequestAlreadyAccepted user contact CORContact contact -> toView $ CRContactRequestAlreadyAccepted user contact
CORRequest cReq@UserContactRequest {localDisplayName} -> do CORRequest cReq@UserContactRequest {localDisplayName} -> do
withStore' (\db -> getUserContactLinkById db userId userContactLinkId) >>= \case withStore' (\db -> getUserContactLinkById db userId userContactLinkId) >>= \case
@ -3892,7 +3920,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
then unless cancelled $ case fileConnReq_ of then unless cancelled $ case fileConnReq_ of
-- receiving via a separate connection -- receiving via a separate connection
Just fileConnReq -> do Just fileConnReq -> do
connIds <- joinAgentConnectionAsync user True fileConnReq $ directMessage XOk connIds <- joinAgentConnectionAsync user True fileConnReq =<< directMessage XOk
withStore' $ \db -> createSndDirectFTConnection db user fileId connIds withStore' $ \db -> createSndDirectFTConnection db user fileId connIds
-- receiving inline -- receiving inline
_ -> do _ -> do
@ -3989,7 +4017,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
(Just fileConnReq, _) -> do (Just fileConnReq, _) -> do
-- receiving via a separate connection -- receiving via a separate connection
-- [async agent commands] no continuation needed, but command should be asynchronous for stability -- [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 withStore' $ \db -> createSndGroupFileTransferConnection db user fileId connIds m
(_, Just conn) -> do (_, Just conn) -> do
-- receiving inline -- receiving inline
@ -4012,7 +4040,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
processGroupInvitation :: Contact -> GroupInvitation -> RcvMessage -> MsgMeta -> m () processGroupInvitation :: Contact -> GroupInvitation -> RcvMessage -> MsgMeta -> m ()
processGroupInvitation ct inv msg msgMeta = do processGroupInvitation ct inv msg msgMeta = do
let Contact {localDisplayName = c, activeConn = Connection {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 GroupInvitation {fromMember = (MemberIdRole fromMemId fromRole), invitedMember = (MemberIdRole memId memRole), connRequest, groupLinkId} = inv
checkIntegrityCreateItem (CDDirectRcv ct) msgMeta checkIntegrityCreateItem (CDDirectRcv ct) msgMeta
when (fromRole < GRAdmin || fromRole < memRole) $ throwChatError (CEGroupContactRole c) when (fromRole < GRAdmin || fromRole < memRole) $ throwChatError (CEGroupContactRole c)
@ -4021,9 +4049,9 @@ 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 (gInfo@GroupInfo {groupId, localDisplayName, groupProfile, membership = membership@GroupMember {groupMemberId, memberId}}, hostId) <- withStore $ \db -> createGroupInvitation db user ct inv customUserProfileId
if sameGroupLinkId groupLinkId groupLinkId' if sameGroupLinkId groupLinkId groupLinkId'
then do then do
connIds <- joinAgentConnectionAsync user True connRequest . directMessage $ XGrpAcpt memberId connIds <- joinAgentConnectionAsync user True connRequest =<< directMessage (XGrpAcpt memberId)
withStore' $ \db -> do withStore' $ \db -> do
createMemberConnectionAsync db user hostId connIds createMemberConnectionAsync db user hostId connIds peerChatVRange
updateGroupMemberStatusById db userId hostId GSMemAccepted updateGroupMemberStatusById db userId hostId GSMemAccepted
updateGroupMemberStatus db userId membership GSMemAccepted updateGroupMemberStatus db userId membership GSMemAccepted
toView $ CRUserAcceptedGroupSent user gInfo {membership = membership {memberStatus = GSMemAccepted}} (Just ct) toView $ CRUserAcceptedGroupSent user gInfo {membership = membership {memberStatus = GSMemAccepted}} (Just ct)
@ -4223,18 +4251,20 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
withStore' $ \db -> mergeContactRecords db userId c1 c2 withStore' $ \db -> mergeContactRecords db userId c1 c2
toView $ CRContactsMerged user c1 c2 toView $ CRContactsMerged user c1 c2
saveConnInfo :: Connection -> ConnInfo -> m () saveConnInfo :: Connection -> ConnInfo -> m Connection
saveConnInfo activeConn connInfo = do saveConnInfo activeConn connInfo = do
ChatMessage {chatMsgEvent} <- parseChatMessage activeConn connInfo ChatMessage {chatVRange, chatMsgEvent} <- parseChatMessage activeConn connInfo
conn' <- updatePeerChatVRange activeConn chatVRange
case chatMsgEvent of case chatMsgEvent of
XInfo p -> do XInfo p -> do
ct <- withStore $ \db -> createDirectContact db user activeConn p ct <- withStore $ \db -> createDirectContact db user conn' p
toView $ CRContactConnecting user ct toView $ CRContactConnecting user ct
pure conn'
-- TODO show/log error, other events in SMP confirmation -- TODO show/log error, other events in SMP confirmation
_ -> pure () _ -> pure conn'
xGrpMemNew :: GroupInfo -> GroupMember -> MemberInfo -> RcvMessage -> MsgMeta -> m () 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 checkHostRole m memRole
members <- withStore' $ \db -> getGroupMembers db user gInfo members <- withStore' $ \db -> getGroupMembers db user gInfo
unless (sameMemberId memId $ membership gInfo) $ unless (sameMemberId memId $ membership gInfo) $
@ -4247,7 +4277,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
toView $ CRJoinedGroupMemberConnecting user gInfo m newMember toView $ CRJoinedGroupMemberConnecting user gInfo m newMember
xGrpMemIntro :: GroupInfo -> GroupMember -> MemberInfo -> m () 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 case memberCategory m of
GCHostMember -> do GCHostMember -> do
members <- withStore' $ \db -> getGroupMembers db user gInfo members <- withStore' $ \db -> getGroupMembers db user gInfo
@ -4256,14 +4286,19 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
else do else do
when (memberRole < GRAdmin) $ throwChatError (CEGroupContactRole c) 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 -- [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 groupConnIds <- createConn
directConnIds <- createAgentConnectionAsync user CFCreateConnGrpMemInv enableNtfs SCMInvitation directConnIds <- case memberChatVRange of
-- [incognito] direct connection with member has to be established using the same incognito profile [that was known to host and used for group membership] Nothing -> Just <$> createConn
Just mcvr
| isCompatibleRange (fromChatVRange mcvr) groupNoDirectVRange -> Just <$> createConn -- pure Nothing
| otherwise -> Just <$> createConn
let customUserProfileId = if memberIncognito membership then Just (localProfileId $ memberProfile membership) else Nothing let customUserProfileId = if memberIncognito membership then Just (localProfileId $ memberProfile membership) else Nothing
void $ withStore $ \db -> createIntroReMember db user gInfo m memInfo groupConnIds directConnIds customUserProfileId void $ withStore $ \db -> createIntroReMember db user gInfo m memInfo groupConnIds directConnIds customUserProfileId
_ -> messageError "x.grp.mem.intro can be only sent by host member" _ -> 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 sendXGrpMemInv hostConnId directConnReq XGrpMemIntroCont {groupId, groupMemberId, memberId, groupConnReq} = do
hostConn <- withStore $ \db -> getConnectionById db user hostConnId hostConn <- withStore $ \db -> getConnectionById db user hostConnId
let msg = XGrpMemInv memberId IntroInvitation {groupConnReq, directConnReq} let msg = XGrpMemInv memberId IntroInvitation {groupConnReq, directConnReq}
@ -4284,7 +4319,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
_ -> messageError "x.grp.mem.inv can be only sent by invitee member" _ -> messageError "x.grp.mem.inv can be only sent by invitee member"
xGrpMemFwd :: GroupInfo -> GroupMember -> MemberInfo -> IntroInvitation -> m () 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 checkHostRole m memRole
members <- withStore' $ \db -> getGroupMembers db user gInfo members <- withStore' $ \db -> getGroupMembers db user gInfo
toMember <- case find (sameMemberId memId) members of toMember <- case find (sameMemberId memId) members of
@ -4296,12 +4331,13 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
Just m' -> pure m' Just m' -> pure m'
withStore' $ \db -> saveMemberInvitation db toMember introInv withStore' $ \db -> saveMemberInvitation db toMember introInv
-- [incognito] send membership incognito profile, create direct connection as incognito -- [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 -- [async agent commands] no continuation needed, but commands should be asynchronous for stability
groupConnIds <- joinAgentConnectionAsync user enableNtfs groupConnReq $ directMessage msg groupConnIds <- joinAgentConnectionAsync user enableNtfs groupConnReq dm
directConnIds <- joinAgentConnectionAsync user enableNtfs directConnReq $ directMessage msg directConnIds <- forM directConnReq $ \dcr -> joinAgentConnectionAsync user enableNtfs dcr dm
let customUserProfileId = if memberIncognito membership then Just (localProfileId $ memberProfile membership) else Nothing 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 :: GroupInfo -> GroupMember -> MemberId -> GroupMemberRole -> RcvMessage -> MsgMeta -> m ()
xGrpMemRole gInfo@GroupInfo {membership} m@GroupMember {memberRole = senderRole} memId memRole msg msgMeta xGrpMemRole gInfo@GroupInfo {membership} m@GroupMember {memberRole = senderRole} memId memRole msg msgMeta
@ -4441,6 +4477,13 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
toView $ CRChatItemStatusUpdated user (AChatItem SCTGroup SMDSnd (GroupChat gInfo) chatItem) toView $ CRChatItemStatusUpdated user (AChatItem SCTGroup SMDSnd (GroupChat gInfo) chatItem)
_ -> pure () _ -> pure ()
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
parseFileDescription :: (ChatMonad m, FilePartyI p) => Text -> m (ValidFileDescription p) parseFileDescription :: (ChatMonad m, FilePartyI p) => Text -> m (ValidFileDescription p)
parseFileDescription = parseFileDescription =
liftEither . first (ChatError . CEInvalidFileDescription) . (strDecode . encodeUtf8) liftEither . first (ChatError . CEInvalidFileDescription) . (strDecode . encodeUtf8)
@ -4639,12 +4682,15 @@ sendDirectMessage conn chatMsgEvent connOrGroupId = do
createSndMessage :: (MsgEncodingI e, ChatMonad m) => ChatMsgEvent e -> ConnOrGroupId -> m SndMessage createSndMessage :: (MsgEncodingI e, ChatMonad m) => ChatMsgEvent e -> ConnOrGroupId -> m SndMessage
createSndMessage chatMsgEvent connOrGroupId = do createSndMessage chatMsgEvent connOrGroupId = do
gVar <- asks idsDrg gVar <- asks idsDrg
ChatConfig {chatVRange} <- asks config
withStore $ \db -> createNewSndMessage db gVar connOrGroupId $ \sharedMsgId -> 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} in NewMessage {chatMsgEvent, msgBody}
directMessage :: MsgEncodingI e => ChatMsgEvent e -> ByteString directMessage :: (MsgEncodingI e, ChatMonad m) => ChatMsgEvent e -> m ByteString
directMessage chatMsgEvent = strEncode ChatMessage {msgId = Nothing, chatMsgEvent} 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 :: ChatMonad m => Connection -> CMEventTag e -> MsgBody -> MessageId -> m Int64
deliverMessage conn@Connection {connId} cmEventTag msgBody msgId = do deliverMessage conn@Connection {connId} cmEventTag msgBody msgId = do
@ -4699,15 +4745,17 @@ sendPendingGroupMessages user GroupMember {groupMemberId, localDisplayName} conn
_ -> throwChatError $ CEGroupMemberIntroNotFound localDisplayName _ -> throwChatError $ CEGroupMemberIntroNotFound localDisplayName
_ -> pure () _ -> 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 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' <- updatePeerChatVRange conn chatVRange
let agentMsgId = fst $ recipient agentMsgMeta let agentMsgId = fst $ recipient agentMsgMeta
newMsg = NewMessage {chatMsgEvent, msgBody} newMsg = NewMessage {chatMsgEvent, msgBody}
rcvMsgDelivery = RcvMsgDelivery {connId, agentMsgId, agentMsgMeta, agentAckCmdId} rcvMsgDelivery = RcvMsgDelivery {connId, agentMsgId, agentMsgMeta, agentAckCmdId}
withStoreCtx' msg <- withStoreCtx'
(Just $ "createNewMessageAndRcvMsgDelivery, rcvMsgDelivery: " <> show rcvMsgDelivery <> ", sharedMsgId_: " <> show sharedMsgId_ <> ", msgDeliveryStatus: MDSRcvAgent") (Just $ "createNewMessageAndRcvMsgDelivery, rcvMsgDelivery: " <> show rcvMsgDelivery <> ", sharedMsgId_: " <> show sharedMsgId_ <> ", msgDeliveryStatus: MDSRcvAgent")
$ \db -> createNewMessageAndRcvMsgDelivery db connOrGroupId newMsg sharedMsgId_ rcvMsgDelivery $ \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 :: 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 saveSndChatItem user cd msg content = saveSndChatItem' user cd msg content Nothing Nothing Nothing False
@ -4805,13 +4853,15 @@ joinAgentConnectionAsync user enableNtfs cReqUri cInfo = do
allowAgentConnectionAsync :: (MsgEncodingI e, ChatMonad m) => User -> Connection -> ConfirmationId -> ChatMsgEvent e -> m () allowAgentConnectionAsync :: (MsgEncodingI e, ChatMonad m) => User -> Connection -> ConfirmationId -> ChatMsgEvent e -> m ()
allowAgentConnectionAsync user conn@Connection {connId} confId msg = do allowAgentConnectionAsync user conn@Connection {connId} confId msg = do
cmdId <- withStore' $ \db -> createCommand db user (Just connId) CFAllowConn 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 withStore' $ \db -> updateConnectionStatus db conn ConnAccepted
agentAcceptContactAsync :: (MsgEncodingI e, ChatMonad m) => User -> Bool -> InvitationId -> ChatMsgEvent e -> m (CommandId, ConnId) agentAcceptContactAsync :: (MsgEncodingI e, ChatMonad m) => User -> Bool -> InvitationId -> ChatMsgEvent e -> m (CommandId, ConnId)
agentAcceptContactAsync user enableNtfs invId msg = do agentAcceptContactAsync user enableNtfs invId msg = do
cmdId <- withStore' $ \db -> createCommand db user Nothing CFAcceptContact 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) pure (cmdId, connId)
deleteAgentConnectionAsync :: ChatMonad m => User -> ConnId -> m () deleteAgentConnectionAsync :: ChatMonad m => User -> ConnId -> m ()

View File

@ -67,6 +67,7 @@ import Simplex.Messaging.TMap (TMap)
import Simplex.Messaging.Transport (simplexMQVersion) import Simplex.Messaging.Transport (simplexMQVersion)
import Simplex.Messaging.Transport.Client (TransportHost) import Simplex.Messaging.Transport.Client (TransportHost)
import Simplex.Messaging.Util (allFinally, catchAllErrors, tryAllErrors, (<$$>)) import Simplex.Messaging.Util (allFinally, catchAllErrors, tryAllErrors, (<$$>))
import Simplex.Messaging.Version
import System.IO (Handle) import System.IO (Handle)
import System.Mem.Weak (Weak) import System.Mem.Weak (Weak)
import UnliftIO.STM import UnliftIO.STM
@ -75,7 +76,7 @@ versionNumber :: String
versionNumber = showVersion SC.version versionNumber = showVersion SC.version
versionString :: String -> String versionString :: String -> String
versionString version = "SimpleX Chat v" <> version versionString ver = "SimpleX Chat v" <> ver
updateStr :: String updateStr :: String
updateStr = "To update run: curl -o- https://raw.githubusercontent.com/simplex-chat/simplex-chat/master/install.sh | bash" updateStr = "To update run: curl -o- https://raw.githubusercontent.com/simplex-chat/simplex-chat/master/install.sh | bash"
@ -104,6 +105,7 @@ coreVersionInfo simplexmqCommit =
data ChatConfig = ChatConfig data ChatConfig = ChatConfig
{ agentConfig :: AgentConfig, { agentConfig :: AgentConfig,
chatVRange :: VersionRange,
confirmMigrations :: MigrationConfirmation, confirmMigrations :: MigrationConfirmation,
defaultServers :: DefaultAgentServers, defaultServers :: DefaultAgentServers,
tbqSize :: Natural, tbqSize :: Natural,

View File

@ -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 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 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 peer_chat_max_version;
ALTER TABLE contact_requests DROP COLUMN peer_chat_min_version;
ALTER TABLE connections DROP COLUMN peer_chat_max_version;
ALTER TABLE connections DROP COLUMN peer_chat_min_version;
|]

View File

@ -285,6 +285,8 @@ CREATE TABLE connections(
security_code TEXT NULL, security_code TEXT NULL,
security_code_verified_at TEXT NULL, security_code_verified_at TEXT NULL,
auth_err_counter INTEGER DEFAULT 0 CHECK(auth_err_counter NOT NULL), auth_err_counter INTEGER DEFAULT 0 CHECK(auth_err_counter NOT NULL),
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) FOREIGN KEY(snd_file_id, connection_id)
REFERENCES snd_files(file_id, connection_id) REFERENCES snd_files(file_id, connection_id)
ON DELETE CASCADE ON DELETE CASCADE
@ -318,6 +320,8 @@ CREATE TABLE contact_requests(
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE,
updated_at TEXT CHECK(updated_at NOT NULL), updated_at TEXT CHECK(updated_at NOT NULL),
xcontact_id BLOB, xcontact_id BLOB,
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) FOREIGN KEY(user_id, local_display_name)
REFERENCES display_names(user_id, local_display_name) REFERENCES display_names(user_id, local_display_name)
ON UPDATE CASCADE ON UPDATE CASCADE

View File

@ -46,6 +46,17 @@ import Simplex.Messaging.Encoding
import Simplex.Messaging.Encoding.String import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (dropPrefix, fromTextField_, fstToLower, parseAll, sumTypeJSON, taggedObjectJSON) import Simplex.Messaging.Parsers (dropPrefix, fromTextField_, fstToLower, parseAll, sumTypeJSON, taggedObjectJSON)
import Simplex.Messaging.Util (eitherToMaybe, safeDecodeUtf8, (<$?>)) import Simplex.Messaging.Util (eitherToMaybe, safeDecodeUtf8, (<$?>))
import Simplex.Messaging.Version hiding (version)
currentChatVersion :: Version
currentChatVersion = 2
supportedChatVRange :: VersionRange
supportedChatVRange = mkVersionRange 1 currentChatVersion
-- version range that supports skipping establishing direct connections in a group
groupNoDirectVRange :: VersionRange
groupNoDirectVRange = mkVersionRange 2 currentChatVersion
data ConnectionEntity data ConnectionEntity
= RcvDirectMsgConnection {entityConnection :: Connection, contact :: Maybe Contact} = RcvDirectMsgConnection {entityConnection :: Connection, contact :: Maybe Contact}
@ -102,7 +113,8 @@ data AppMessage (e :: MsgEncoding) where
-- chat message is sent as JSON with these properties -- chat message is sent as JSON with these properties
data AppMessageJson = AppMessageJson data AppMessageJson = AppMessageJson
{ msgId :: Maybe SharedMsgId, { v :: Maybe ChatVersionRange,
msgId :: Maybe SharedMsgId,
event :: Text, event :: Text,
params :: J.Object params :: J.Object
} }
@ -161,7 +173,11 @@ instance ToJSON MsgRef where
toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True} toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True}
toEncoding = J.genericToEncoding 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) deriving (Eq, Show)
data AChatMessage = forall e. MsgEncodingI e => ACMsg (SMsgEncoding e) (ChatMessage e) data AChatMessage = forall e. MsgEncodingI e => ACMsg (SMsgEncoding e) (ChatMessage e)
@ -724,17 +740,17 @@ appBinaryToCM :: AppMessageBinary -> Either String (ChatMessage 'Binary)
appBinaryToCM AppMessageBinary {msgId, tag, body} = do appBinaryToCM AppMessageBinary {msgId, tag, body} = do
eventTag <- strDecode $ B.singleton tag eventTag <- strDecode $ B.singleton tag
chatMsgEvent <- parseAll (msg eventTag) body chatMsgEvent <- parseAll (msg eventTag) body
pure ChatMessage {msgId, chatMsgEvent} pure ChatMessage {chatVRange = chatInitialVRange, msgId, chatMsgEvent}
where where
msg :: CMEventTag 'Binary -> A.Parser (ChatMsgEvent 'Binary) msg :: CMEventTag 'Binary -> A.Parser (ChatMsgEvent 'Binary)
msg = \case msg = \case
BFileChunk_ -> BFileChunk <$> (SharedMsgId <$> smpP) <*> (unIFC <$> smpP) BFileChunk_ -> BFileChunk <$> (SharedMsgId <$> smpP) <*> (unIFC <$> smpP)
appJsonToCM :: AppMessageJson -> Either String (ChatMessage 'Json) appJsonToCM :: AppMessageJson -> Either String (ChatMessage 'Json)
appJsonToCM AppMessageJson {msgId, event, params} = do appJsonToCM AppMessageJson {v, msgId, event, params} = do
eventTag <- strDecode $ encodeUtf8 event eventTag <- strDecode $ encodeUtf8 event
chatMsgEvent <- msg eventTag chatMsgEvent <- msg eventTag
pure ChatMessage {msgId, chatMsgEvent} pure ChatMessage {chatVRange = maybe chatInitialVRange fromChatVRange v, msgId, chatMsgEvent}
where where
p :: FromJSON a => J.Key -> Either String a p :: FromJSON a => J.Key -> Either String a
p key = JT.parseEither (.: key) params p key = JT.parseEither (.: key) params
@ -784,11 +800,11 @@ appJsonToCM AppMessageJson {msgId, event, params} = do
key .=? value = maybe id ((:) . (key .=)) value key .=? value = maybe id ((:) . (key .=)) value
chatToAppMessage :: forall e. MsgEncodingI e => ChatMessage e -> AppMessage e 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 -> SBinary ->
let (binaryMsgId, body) = toBody chatMsgEvent let (binaryMsgId, body) = toBody chatMsgEvent
in AMBinary AppMessageBinary {msgId = binaryMsgId, tag = B.head $ strEncode tag, body} 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 where
tag = toCMEventTag chatMsgEvent tag = toCMEventTag chatMsgEvent
o :: [(J.Key, J.Value)] -> J.Object o :: [(J.Key, J.Value)] -> J.Object
@ -804,7 +820,7 @@ chatToAppMessage ChatMessage {msgId, chatMsgEvent} = case encoding @e of
XMsgUpdate msgId' content ttl live -> o $ ("ttl" .=? ttl) $ ("live" .=? live) ["msgId" .= msgId', "content" .= content] XMsgUpdate msgId' content ttl live -> o $ ("ttl" .=? ttl) $ ("live" .=? live) ["msgId" .= msgId', "content" .= content]
XMsgDel msgId' memberId -> o $ ("memberId" .=? memberId) ["msgId" .= msgId'] XMsgDel msgId' memberId -> o $ ("memberId" .=? memberId) ["msgId" .= msgId']
XMsgDeleted -> JM.empty 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] XFile fileInv -> o ["file" .= fileInv]
XFileAcpt fileName -> o ["fileName" .= fileName] XFileAcpt fileName -> o ["fileName" .= fileName]
XFileAcptInv sharedMsgId fileConnReq fileName -> o $ ("fileConnReq" .=? fileConnReq) ["msgId" .= sharedMsgId, "fileName" .= fileName] XFileAcptInv sharedMsgId fileConnReq fileName -> o $ ("fileConnReq" .=? fileConnReq) ["msgId" .= sharedMsgId, "fileName" .= fileName]

View File

@ -49,7 +49,8 @@ getConnectionEntity db user@User {userId, userContactId} agentConnId = do
db db
[sql| [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, 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,
peer_chat_min_version, peer_chat_max_version
FROM connections FROM connections
WHERE user_id = ? AND agent_conn_id = ? WHERE user_id = ? AND agent_conn_id = ?
|] |]

View File

@ -75,6 +75,7 @@ import Simplex.Chat.Types.Preferences
import Simplex.Messaging.Agent.Protocol (ConnId, InvitationId, UserId) import Simplex.Messaging.Agent.Protocol (ConnId, InvitationId, UserId)
import Simplex.Messaging.Agent.Store.SQLite (firstRow, maybeFirstRow) import Simplex.Messaging.Agent.Store.SQLite (firstRow, maybeFirstRow)
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB 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.Connection -> UserId -> Int64 -> ExceptT StoreError IO PendingContactConnection
getPendingContactConnection db userId connId = do 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, cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts,
-- Connection -- 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.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.peer_chat_min_version, c.peer_chat_max_version
FROM contacts ct FROM contacts ct
JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id
JOIN connections c ON c.contact_id = ct.contact_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) 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 rights <$> mapM (runExceptT . getContact db user) contactIds
createOrUpdateContactRequest :: DB.Connection -> User -> Int64 -> InvitationId -> Profile -> Maybe XContactId -> ExceptT StoreError IO ContactOrRequest createOrUpdateContactRequest :: DB.Connection -> User -> Int64 -> InvitationId -> VersionRange -> Profile -> Maybe XContactId -> ExceptT StoreError IO ContactOrRequest
createOrUpdateContactRequest db user@User {userId} userContactLinkId invId Profile {displayName, fullName, image, contactLink, preferences} xContactId_ = createOrUpdateContactRequest db user@User {userId} userContactLinkId invId (VersionRange minV maxV) Profile {displayName, fullName, image, contactLink, preferences} xContactId_ =
liftIO (maybeM getContact' xContactId_) >>= \case liftIO (maybeM getContact' xContactId_) >>= \case
Just contact -> pure $ CORContact contact Just contact -> pure $ CORContact contact
Nothing -> CORRequest <$> createOrUpdate_ Nothing -> CORRequest <$> createOrUpdate_
@ -441,10 +443,10 @@ createOrUpdateContactRequest db user@User {userId} userContactLinkId invId Profi
db db
[sql| [sql|
INSERT INTO contact_requests 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) (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 (?,?,?,?,?,?,?,?) VALUES (?,?,?,?,?,?,?,?,?,?)
|] |]
(userContactLinkId, invId, profileId, ldn, userId, currentTs, currentTs, xContactId_) (userContactLinkId, invId, minV, maxV, profileId, ldn, userId, currentTs, currentTs, xContactId_)
insertedRowId db insertedRowId db
getContact' :: XContactId -> IO (Maybe Contact) getContact' :: XContactId -> IO (Maybe Contact)
getContact' xContactId = 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, cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts,
-- Connection -- 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.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.peer_chat_min_version, c.peer_chat_max_version
FROM contacts ct FROM contacts ct
JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id
LEFT JOIN connections c ON c.contact_id = ct.contact_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| [sql|
SELECT SELECT
cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.user_contact_link_id, 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.peer_chat_min_version, cr.peer_chat_max_version
FROM contact_requests cr FROM contact_requests cr
JOIN connections c USING (user_contact_link_id) JOIN connections c USING (user_contact_link_id)
JOIN contact_profiles p USING (contact_profile_id) JOIN contact_profiles p USING (contact_profile_id)
@ -489,10 +493,26 @@ createOrUpdateContactRequest db user@User {userId} userContactLinkId invId Profi
currentTs <- liftIO getCurrentTime currentTs <- liftIO getCurrentTime
updateProfile currentTs updateProfile currentTs
if displayName == oldDisplayName 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 = ?, peer_chat_min_version = ?, peer_chat_max_version = ?, updated_at = ?
WHERE user_id = ? AND contact_request_id = ?
|]
(invId, minV, maxV, currentTs, userId, cReqId)
else withLocalDisplayName db userId displayName $ \ldn -> else withLocalDisplayName db userId displayName $ \ldn ->
Right <$> do 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 = ?, 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)
DB.execute db "DELETE FROM display_names WHERE local_display_name = ? AND user_id = ?" (oldLdn, userId) DB.execute db "DELETE FROM display_names WHERE local_display_name = ? AND user_id = ?" (oldLdn, userId)
where where
updateProfile currentTs = updateProfile currentTs =
@ -527,7 +547,8 @@ getContactRequest db User {userId} contactRequestId =
[sql| [sql|
SELECT SELECT
cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.user_contact_link_id, 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.peer_chat_min_version, cr.peer_chat_max_version
FROM contact_requests cr FROM contact_requests cr
JOIN connections c USING (user_contact_link_id) JOIN connections c USING (user_contact_link_id)
JOIN contact_profiles p USING (contact_profile_id) JOIN contact_profiles p USING (contact_profile_id)
@ -566,8 +587,8 @@ deleteContactRequest db User {userId} contactRequestId = do
(userId, userId, contactRequestId) (userId, userId, contactRequestId)
DB.execute db "DELETE FROM contact_requests WHERE user_id = ? AND contact_request_id = ?" (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.Connection -> User -> ConnId -> VersionRange -> 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 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) DB.execute db "DELETE FROM contact_requests WHERE user_id = ? AND local_display_name = ?" (userId, localDisplayName)
createdAt <- getCurrentTime createdAt <- getCurrentTime
customUserProfileId <- forM incognitoProfile $ \case 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 (?,?,?,?,?,?,?,?,?)" "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) (userId, localDisplayName, profileId, True, userPreferences, createdAt, createdAt, createdAt, xContactId)
contactId <- insertedRowId db 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 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} 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, cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts,
-- Connection -- 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.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.peer_chat_min_version, c.peer_chat_max_version
FROM contacts ct FROM contacts ct
JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id
LEFT JOIN connections c ON c.contact_id = ct.contact_id LEFT JOIN connections c ON c.contact_id = ct.contact_id
@ -651,7 +673,8 @@ getContactConnections db userId Contact {contactId} =
db db
[sql| [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, 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.peer_chat_min_version, c.peer_chat_max_version
FROM connections c FROM connections c
JOIN contacts ct ON ct.contact_id = c.contact_id JOIN contacts ct ON ct.contact_id = c.contact_id
WHERE c.user_id = ? AND ct.user_id = ? AND ct.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 db
[sql| [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, 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,
peer_chat_min_version, peer_chat_max_version
FROM connections FROM connections
WHERE user_id = ? AND connection_id = ? WHERE user_id = ? AND connection_id = ?
|] |]

View File

@ -425,7 +425,7 @@ getChatRefByFileId db User {userId} fileId =
createSndFileConnection_ :: DB.Connection -> UserId -> Int64 -> ConnId -> IO Connection createSndFileConnection_ :: DB.Connection -> UserId -> Int64 -> ConnId -> IO Connection
createSndFileConnection_ db userId fileId agentConnId = do createSndFileConnection_ db userId fileId agentConnId = do
currentTs <- getCurrentTime 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.Connection -> SndFileTransfer -> FileStatus -> IO ()
updateSndFileStatus db SndFileTransfer {fileId, connId} status = do updateSndFileStatus db SndFileTransfer {fileId, connId} status = do

View File

@ -83,6 +83,7 @@ module Simplex.Chat.Store.Groups
updateGroupSettings, updateGroupSettings,
getXGrpMemIntroContDirect, getXGrpMemIntroContDirect,
getXGrpMemIntroContGroup, getXGrpMemIntroContGroup,
getHostConnId,
) )
where where
@ -105,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.Agent.Store.SQLite.DB as DB
import qualified Simplex.Messaging.Crypto as C import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Util (eitherToMaybe) import Simplex.Messaging.Util (eitherToMaybe)
import Simplex.Messaging.Version
import UnliftIO.STM 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 type GroupInfoRow = (Int64, GroupName, GroupName, Text, Maybe Text, Maybe ImageData, Maybe ProfileId, Maybe Bool, Maybe Bool, Bool, Maybe GroupPreferences) :. (UTCTime, UTCTime, Maybe UTCTime) :. GroupMemberRow
@ -142,7 +144,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 (?,?,?,?,?,?,?,?,?)" "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) (userId, groupId, groupLinkId, "group_link_" <> localDisplayName, cReq, memberRole, True, currentTs, currentTs)
userContactLinkId <- insertedRowId db 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.Connection -> User -> GroupInfo -> ExceptT StoreError IO Connection
getGroupLinkConnection db User {userId} groupInfo@GroupInfo {groupId} = getGroupLinkConnection db User {userId} groupInfo@GroupInfo {groupId} =
@ -151,7 +153,8 @@ getGroupLinkConnection db User {userId} groupInfo@GroupInfo {groupId} =
db db
[sql| [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, 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.peer_chat_min_version, c.peer_chat_max_version
FROM connections c FROM connections c
JOIN user_contact_links uc ON c.user_contact_link_id = uc.user_contact_link_id 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 = ? WHERE c.user_id = ? AND uc.user_id = ? AND uc.group_id = ?
@ -232,7 +235,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.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, 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.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.peer_chat_min_version, c.peer_chat_max_version
FROM group_members m FROM group_members m
JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) 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 JOIN groups g ON g.group_id = m.group_id
@ -478,20 +482,21 @@ getUserGroupsWithSummary db user _contactId_ search_ =
-- the statuses on non-current members should match memberCurrent' function -- the statuses on non-current members should match memberCurrent' function
getGroupSummary :: DB.Connection -> User -> GroupId -> IO GroupSummary getGroupSummary :: DB.Connection -> User -> GroupId -> IO GroupSummary
getGroupSummary db User {userId} groupId = do getGroupSummary db User {userId} groupId = do
currentMembers_ <- maybeFirstRow fromOnly $ currentMembers_ <-
DB.query maybeFirstRow fromOnly $
db DB.query
[sql| db
SELECT count (m.group_member_id) [sql|
FROM groups g SELECT count (m.group_member_id)
JOIN group_members m USING (group_id) FROM groups g
WHERE g.user_id = ? JOIN group_members m USING (group_id)
AND g.group_id = ? WHERE g.user_id = ?
AND m.member_status != ? AND g.group_id = ?
AND m.member_status != ? AND m.member_status != ?
AND m.member_status != ? AND m.member_status != ?
|] AND m.member_status != ?
(userId, groupId, GSMemRemoved, GSMemLeft, GSMemInvited) |]
(userId, groupId, GSMemRemoved, GSMemLeft, GSMemInvited)
pure GroupSummary {currentMembers = fromMaybe 0 currentMembers_} pure GroupSummary {currentMembers = fromMaybe 0 currentMembers_}
getContactGroupPreferences :: DB.Connection -> User -> Contact -> IO [FullGroupPreferences] getContactGroupPreferences :: DB.Connection -> User -> Contact -> IO [FullGroupPreferences]
@ -524,7 +529,8 @@ groupMemberQuery =
m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, m.member_status, 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, 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.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.peer_chat_min_version, c.peer_chat_max_version
FROM group_members m FROM group_members m
JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) 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 = ( LEFT JOIN connections c ON c.connection_id = (
@ -609,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) 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.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 {peerChatVRange}} memberRole agentConnId connRequest =
createWithRandomId gVar $ \memId -> do createWithRandomId gVar $ \memId -> do
createdAt <- liftIO getCurrentTime createdAt <- liftIO getCurrentTime
member@GroupMember {groupMemberId} <- createMember_ (MemberId memId) createdAt member@GroupMember {groupMemberId} <- createMember_ (MemberId memId) createdAt
void $ createMemberConnection_ db userId groupMemberId agentConnId Nothing 0 createdAt void $ createMemberConnection_ db userId groupMemberId agentConnId peerChatVRange Nothing 0 createdAt
pure member pure member
where where
createMember_ memberId createdAt = do createMember_ memberId createdAt = do
@ -648,13 +654,13 @@ createNewContactMember db gVar User {userId, userContactId} groupId Contact {con
:. (userId, localDisplayName, contactId, localProfileId profile, connRequest, createdAt, createdAt) :. (userId, localDisplayName, contactId, localProfileId profile, connRequest, createdAt, createdAt)
) )
createNewContactMemberAsync :: DB.Connection -> TVar ChaChaDRG -> User -> GroupId -> Contact -> GroupMemberRole -> (CommandId, ConnId) -> ExceptT StoreError IO () 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) = createNewContactMemberAsync db gVar user@User {userId, userContactId} groupId Contact {contactId, localDisplayName, profile} memberRole (cmdId, agentConnId) peerChatVRange =
createWithRandomId gVar $ \memId -> do createWithRandomId gVar $ \memId -> do
createdAt <- liftIO getCurrentTime createdAt <- liftIO getCurrentTime
insertMember_ (MemberId memId) createdAt insertMember_ (MemberId memId) createdAt
groupMemberId <- liftIO $ insertedRowId db groupMemberId <- liftIO $ insertedRowId db
Connection {connId} <- createMemberConnection_ db userId groupMemberId agentConnId Nothing 0 createdAt Connection {connId} <- createMemberConnection_ db userId groupMemberId agentConnId peerChatVRange Nothing 0 createdAt
setCommandConnId db user cmdId connId setCommandConnId db user cmdId connId
where where
insertMember_ memberId createdAt = insertMember_ memberId createdAt =
@ -670,30 +676,32 @@ createNewContactMemberAsync db gVar user@User {userId, userContactId} groupId Co
:. (userId, localDisplayName, contactId, localProfileId profile, createdAt, createdAt) :. (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} = getContactViaMember db user@User {userId} GroupMember {groupMemberId} =
maybeFirstRow (toContact user) $ ExceptT $
DB.query firstRow (toContact user) (SEContactNotFoundByMemberId groupMemberId) $
db DB.query
[sql| db
SELECT [sql|
-- Contact SELECT
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, -- Contact
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, 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,
-- Connection cp.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, -- Connection
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.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,
FROM contacts ct 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,
JOIN contact_profiles cp ON cp.contact_profile_id = ct.contact_profile_id c.peer_chat_min_version, c.peer_chat_max_version
JOIN connections c ON c.connection_id = ( FROM contacts ct
SELECT max(cc.connection_id) JOIN contact_profiles cp ON cp.contact_profile_id = ct.contact_profile_id
FROM connections cc JOIN connections c ON c.connection_id = (
where cc.contact_id = ct.contact_id SELECT max(cc.connection_id)
) FROM connections cc
JOIN group_members m ON m.contact_id = ct.contact_id where cc.contact_id = ct.contact_id
WHERE ct.user_id = ? AND m.group_member_id = ? AND ct.deleted = 0 )
|] JOIN group_members m ON m.contact_id = ct.contact_id
(userId, groupMemberId) WHERE ct.user_id = ? AND m.group_member_id = ? AND ct.deleted = 0
|]
(userId, groupMemberId)
setNewContactMemberConnRequest :: DB.Connection -> User -> GroupMember -> ConnReqInvitation -> IO () setNewContactMemberConnRequest :: DB.Connection -> User -> GroupMember -> ConnReqInvitation -> IO ()
setNewContactMemberConnRequest db User {userId} GroupMember {groupMemberId} connRequest = do setNewContactMemberConnRequest db User {userId} GroupMember {groupMemberId} connRequest = do
@ -705,15 +713,15 @@ getMemberInvitation db User {userId} groupMemberId =
fmap join . maybeFirstRow fromOnly $ fmap join . maybeFirstRow fromOnly $
DB.query db "SELECT sent_inv_queue_info FROM group_members WHERE group_member_id = ? AND user_id = ?" (groupMemberId, userId) 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.Connection -> UserId -> GroupMember -> ConnId -> VersionRange -> IO ()
createMemberConnection db userId GroupMember {groupMemberId} agentConnId = do createMemberConnection db userId GroupMember {groupMemberId} agentConnId peerChatVRange = do
currentTs <- getCurrentTime currentTs <- getCurrentTime
void $ createMemberConnection_ db userId groupMemberId agentConnId Nothing 0 currentTs void $ createMemberConnection_ db userId groupMemberId agentConnId peerChatVRange Nothing 0 currentTs
createMemberConnectionAsync :: DB.Connection -> User -> GroupMemberId -> (CommandId, ConnId) -> IO () createMemberConnectionAsync :: DB.Connection -> User -> GroupMemberId -> (CommandId, ConnId) -> VersionRange -> IO ()
createMemberConnectionAsync db user@User {userId} groupMemberId (cmdId, agentConnId) = do createMemberConnectionAsync db user@User {userId} groupMemberId (cmdId, agentConnId) peerChatVRange = do
currentTs <- getCurrentTime currentTs <- getCurrentTime
Connection {connId} <- createMemberConnection_ db userId groupMemberId agentConnId Nothing 0 currentTs Connection {connId} <- createMemberConnection_ db userId groupMemberId agentConnId peerChatVRange Nothing 0 currentTs
setCommandConnId db user cmdId connId setCommandConnId db user cmdId connId
updateGroupMemberStatus :: DB.Connection -> UserId -> GroupMember -> GroupMemberStatus -> IO () updateGroupMemberStatus :: DB.Connection -> UserId -> GroupMember -> GroupMemberStatus -> IO ()
@ -733,25 +741,30 @@ updateGroupMemberStatusById db userId groupMemberId memStatus = do
-- | add new member with profile -- | add new member with profile
createNewGroupMember :: DB.Connection -> User -> GroupInfo -> MemberInfo -> GroupMemberCategory -> GroupMemberStatus -> ExceptT StoreError IO GroupMember 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 = createNewGroupMember db user gInfo memInfo memCategory memStatus = do
ExceptT . withLocalDisplayName db userId displayName $ \localDisplayName -> do currentTs <- liftIO getCurrentTime
currentTs <- 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.execute
db db
"INSERT INTO contact_profiles (display_name, full_name, image, contact_link, user_id, preferences, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)" "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) (displayName, fullName, image, contactLink, userId, preferences, createdAt, createdAt)
memProfileId <- insertedRowId db profileId <- insertedRowId db
let newMember = pure $ Right (ldn, profileId)
NewGroupMember
{ memInfo,
memCategory,
memStatus,
memInvitedBy = IBUnknown,
localDisplayName,
memContactId = Nothing,
memProfileId
}
Right <$> createNewMember_ db user gInfo newMember currentTs
createNewMember_ :: DB.Connection -> User -> GroupInfo -> NewGroupMember -> UTCTime -> IO GroupMember createNewMember_ :: DB.Connection -> User -> GroupInfo -> NewGroupMember -> UTCTime -> IO GroupMember
createNewMember_ createNewMember_
@ -759,7 +772,7 @@ createNewMember_
User {userId, userContactId} User {userId, userContactId}
GroupInfo {groupId} GroupInfo {groupId}
NewGroupMember NewGroupMember
{ memInfo = MemberInfo memberId memberRole memberProfile, { memInfo = MemberInfo memberId memberRole _ memberProfile,
memCategory = memberCategory, memCategory = memberCategory,
memStatus = memberStatus, memStatus = memberStatus,
memInvitedBy = invitedBy, memInvitedBy = invitedBy,
@ -903,43 +916,41 @@ getIntroduction_ db reMember toMember = ExceptT $ do
where where
toIntro :: [(Int64, Maybe ConnReqInvitation, Maybe ConnReqInvitation, GroupMemberIntroStatus)] -> Either StoreError GroupMemberIntro toIntro :: [(Int64, Maybe ConnReqInvitation, Maybe ConnReqInvitation, GroupMemberIntroStatus)] -> Either StoreError GroupMemberIntro
toIntro [(introId, groupConnReq, directConnReq, introStatus)] = toIntro [(introId, groupConnReq, directConnReq, introStatus)] =
let introInvitation = IntroInvitation <$> groupConnReq <*> directConnReq let introInvitation = IntroInvitation <$> groupConnReq <*> pure directConnReq
in Right GroupMemberIntro {introId, reMember, toMember, introStatus, introInvitation} in Right GroupMemberIntro {introId, reMember, toMember, introStatus, introInvitation}
toIntro _ = Left SEIntroNotFound toIntro _ = Left SEIntroNotFound
createIntroReMember :: DB.Connection -> User -> GroupInfo -> GroupMember -> MemberInfo -> (CommandId, ConnId) -> (CommandId, ConnId) -> Maybe ProfileId -> ExceptT StoreError IO GroupMember 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 _ _ memberProfile) (groupCmdId, groupAgentConnId) (directCmdId, directAgentConnId) customUserProfileId = do createIntroReMember db user@User {userId} gInfo@GroupInfo {groupId} _host@GroupMember {memberContactId, activeConn} memInfo@(MemberInfo _ _ memberChatVRange memberProfile) (groupCmdId, groupAgentConnId) directConnIds customUserProfileId = do
let cLevel = 1 + maybe 0 (connLevel :: Connection -> Int) activeConn let mcvr = maybe chatInitialVRange fromChatVRange memberChatVRange
cLevel = 1 + maybe 0 (connLevel :: Connection -> Int) activeConn
currentTs <- liftIO getCurrentTime currentTs <- liftIO getCurrentTime
Connection {connId = directConnId} <- liftIO $ createConnection_ db userId ConnContact Nothing directAgentConnId memberContactId Nothing customUserProfileId cLevel currentTs newMember <- case directConnIds of
liftIO $ setCommandConnId db user directCmdId directConnId Just (directCmdId, directAgentConnId) -> do
(localDisplayName, contactId, memProfileId) <- createContact_ db userId directConnId memberProfile "" (Just groupId) currentTs Nothing 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 liftIO $ do
let newMember =
NewGroupMember
{ memInfo,
memCategory = GCPreMember,
memStatus = GSMemIntroduced,
memInvitedBy = IBUnknown,
localDisplayName,
memContactId = Just contactId,
memProfileId
}
member <- createNewMember_ db user gInfo newMember currentTs 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 liftIO $ setCommandConnId db user groupCmdId groupConnId
pure (member :: GroupMember) {activeConn = Just conn} pure (member :: GroupMember) {activeConn = Just conn}
createIntroToMemberContact :: DB.Connection -> User -> GroupMember -> GroupMember -> (CommandId, ConnId) -> (CommandId, ConnId) -> Maybe ProfileId -> IO () 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} (groupCmdId, groupAgentConnId) (directCmdId, directAgentConnId) customUserProfileId = do 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 let cLevel = 1 + maybe 0 (connLevel :: Connection -> Int) activeConn
currentTs <- getCurrentTime 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 setCommandConnId db user groupCmdId groupConnId
Connection {connId = directConnId} <- createConnection_ db userId ConnContact Nothing directAgentConnId viaContactId Nothing customUserProfileId cLevel currentTs forM_ directConnIds $ \(directCmdId, directAgentConnId) -> do
setCommandConnId db user directCmdId directConnId Connection {connId = directConnId} <- createConnection_ db userId ConnContact Nothing directAgentConnId mcvr viaContactId Nothing customUserProfileId cLevel currentTs
contactId <- createMemberContact_ directConnId currentTs setCommandConnId db user directCmdId directConnId
updateMember_ contactId currentTs contactId <- createMemberContact_ directConnId currentTs
updateMember_ contactId currentTs
where where
createMemberContact_ :: Int64 -> UTCTime -> IO Int64 createMemberContact_ :: Int64 -> UTCTime -> IO Int64
createMemberContact_ connId ts = do createMemberContact_ connId ts = do
@ -966,8 +977,8 @@ createIntroToMemberContact db user@User {userId} GroupMember {memberContactId =
|] |]
[":contact_id" := contactId, ":updated_at" := ts, ":group_member_id" := groupMemberId] [":contact_id" := contactId, ":updated_at" := ts, ":group_member_id" := groupMemberId]
createMemberConnection_ :: DB.Connection -> UserId -> Int64 -> ConnId -> Maybe Int64 -> Int -> UTCTime -> IO Connection createMemberConnection_ :: DB.Connection -> UserId -> Int64 -> ConnId -> VersionRange -> 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 peerChatVRange viaContact = createConnection_ db userId ConnMember (Just groupMemberId) agentConnId peerChatVRange viaContact Nothing Nothing
getViaGroupMember :: DB.Connection -> User -> Contact -> IO (Maybe (GroupInfo, GroupMember)) getViaGroupMember :: DB.Connection -> User -> Contact -> IO (Maybe (GroupInfo, GroupMember))
getViaGroupMember db User {userId, userContactId} Contact {contactId} = getViaGroupMember db User {userId, userContactId} Contact {contactId} =
@ -987,7 +998,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.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, 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.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.peer_chat_min_version, c.peer_chat_max_version
FROM group_members m FROM group_members m
JOIN contacts ct ON ct.contact_id = m.contact_id 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) JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id)
@ -1020,7 +1032,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, 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, 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.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.peer_chat_min_version, c.peer_chat_max_version
FROM contacts ct FROM contacts ct
JOIN contact_profiles p ON ct.contact_profile_id = p.contact_profile_id JOIN contact_profiles p ON ct.contact_profile_id = p.contact_profile_id
JOIN connections c ON c.connection_id = ( JOIN connections c ON c.connection_id = (
@ -1336,3 +1349,9 @@ getXGrpMemIntroContGroup db User {userId} GroupMember {groupMemberId} = do
toCont (hostConnId, connReq_) = case connReq_ of toCont (hostConnId, connReq_) = case connReq_ of
Just connReq -> Just (hostConnId, connReq) Just connReq -> Just (hostConnId, connReq)
_ -> Nothing _ -> 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)

View File

@ -479,6 +479,7 @@ getDirectChatPreviews_ db user@User {userId} = do
-- Connection -- 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.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.peer_chat_min_version, c.peer_chat_max_version,
-- ChatStats -- ChatStats
COALESCE(ChatStats.UnreadCount, 0), COALESCE(ChatStats.MinUnread, 0), ct.unread_chat, COALESCE(ChatStats.UnreadCount, 0), COALESCE(ChatStats.MinUnread, 0), ct.unread_chat,
-- ChatItem -- ChatItem
@ -609,7 +610,8 @@ getContactRequestChatPreviews_ db User {userId} =
[sql| [sql|
SELECT SELECT
cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.user_contact_link_id, 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.peer_chat_min_version, cr.peer_chat_max_version
FROM contact_requests cr FROM contact_requests cr
JOIN connections c ON c.user_contact_link_id = cr.user_contact_link_id 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 JOIN contact_profiles p ON p.contact_profile_id = cr.contact_profile_id

View File

@ -77,6 +77,7 @@ import Simplex.Chat.Migrations.M20230705_delivery_receipts
import Simplex.Chat.Migrations.M20230721_group_snd_item_statuses import Simplex.Chat.Migrations.M20230721_group_snd_item_statuses
import Simplex.Chat.Migrations.M20230814_indexes import Simplex.Chat.Migrations.M20230814_indexes
import Simplex.Chat.Migrations.M20230827_file_encryption import Simplex.Chat.Migrations.M20230827_file_encryption
import Simplex.Chat.Migrations.M20230829_connections_chat_vrange
import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..)) import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..))
schemaMigrations :: [(String, Query, Maybe Query)] schemaMigrations :: [(String, Query, Maybe Query)]
@ -153,7 +154,8 @@ schemaMigrations =
("20230705_delivery_receipts", m20230705_delivery_receipts, Just down_m20230705_delivery_receipts), ("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), ("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),
("20230827_file_encryption", m20230827_file_encryption, Just down_m20230827_file_encryption) ("20230827_file_encryption", m20230827_file_encryption, Just down_m20230827_file_encryption),
("20230829_connections_chat_vrange", m20230829_connections_chat_vrange, Just down_m20230829_connections_chat_vrange)
] ]
-- | The list of migrations in ascending order by date -- | The list of migrations in ascending order by date

View File

@ -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 (?,?,?,?)" "INSERT INTO user_contact_links (user_id, conn_req_contact, created_at, updated_at) VALUES (?,?,?,?)"
(userId, cReq, currentTs, currentTs) (userId, cReq, currentTs, currentTs)
userContactLinkId <- insertedRowId db 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.Connection -> User -> ExceptT StoreError IO [Connection]
getUserAddressConnections db User {userId} = do getUserAddressConnections db User {userId} = do
@ -316,7 +316,8 @@ getUserAddressConnections db User {userId} = do
db db
[sql| [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, 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.peer_chat_min_version, c.peer_chat_max_version
FROM connections c FROM connections c
JOIN user_contact_links uc ON c.user_contact_link_id = uc.user_contact_link_id 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 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| [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, 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.peer_chat_min_version, c.peer_chat_max_version,
uc.user_contact_link_id, uc.conn_req_contact, uc.group_id uc.user_contact_link_id, uc.conn_req_contact, uc.group_id
FROM connections c FROM connections c
JOIN user_contact_links uc ON c.user_contact_link_id = uc.user_contact_link_id JOIN user_contact_links uc ON c.user_contact_link_id = uc.user_contact_link_id

View File

@ -17,8 +17,8 @@ import Control.Monad.Except
import Crypto.Random (ChaChaDRG, randomBytesGenerate) import Crypto.Random (ChaChaDRG, randomBytesGenerate)
import Data.Aeson (ToJSON) import Data.Aeson (ToJSON)
import qualified Data.Aeson as J import qualified Data.Aeson as J
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Base64 as B64 import qualified Data.ByteString.Base64 as B64
import Data.ByteString.Char8 (ByteString)
import Data.Int (Int64) import Data.Int (Int64)
import Data.Maybe (fromMaybe, isJust, listToMaybe) import Data.Maybe (fromMaybe, isJust, listToMaybe)
import Data.Text (Text) 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 qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON) import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON)
import Simplex.Messaging.Util (allFinally) import Simplex.Messaging.Util (allFinally)
import Simplex.Messaging.Version
import UnliftIO.STM import UnliftIO.STM
-- These error type constructors must be added to mobile apps -- These error type constructors must be added to mobile apps
@ -50,6 +51,7 @@ data StoreError
| SEUserNotFoundByContactRequestId {contactRequestId :: Int64} | SEUserNotFoundByContactRequestId {contactRequestId :: Int64}
| SEContactNotFound {contactId :: ContactId} | SEContactNotFound {contactId :: ContactId}
| SEContactNotFoundByName {contactName :: ContactName} | SEContactNotFoundByName {contactName :: ContactName}
| SEContactNotFoundByMemberId {groupMemberId :: GroupMemberId}
| SEContactNotReady {contactName :: ContactName} | SEContactNotReady {contactName :: ContactName}
| SEDuplicateContactLink | SEDuplicateContactLink
| SEUserContactLinkNotFound | SEUserContactLinkNotFound
@ -77,6 +79,7 @@ data StoreError
| SERcvFileNotFoundXFTP {agentRcvFileId :: AgentRcvFileId} | SERcvFileNotFoundXFTP {agentRcvFileId :: AgentRcvFileId}
| SEConnectionNotFound {agentConnId :: AgentConnId} | SEConnectionNotFound {agentConnId :: AgentConnId}
| SEConnectionNotFoundById {connId :: Int64} | SEConnectionNotFoundById {connId :: Int64}
| SEConnectionNotFoundByMemberId {groupMemberId :: GroupMemberId}
| SEPendingConnectionNotFound {connId :: Int64} | SEPendingConnectionNotFound {connId :: Int64}
| SEIntroNotFound | SEIntroNotFound
| SEUniqueID | SEUniqueID
@ -132,15 +135,16 @@ toFileInfo (fileId, fileStatus, filePath) = CIFileInfo {fileId, fileStatus, file
type EntityIdsRow = (Maybe Int64, Maybe Int64, Maybe Int64, Maybe Int64, Maybe Int64) 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 :: 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 let entityId = entityId_ connType
connectionCode = SecurityCode <$> code_ <*> verifiedAt_ connectionCode = SecurityCode <$> code_ <*> verifiedAt_
in Connection {connId, agentConnId = AgentConnId acId, 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 where
entityId_ :: ConnType -> Maybe Int64 entityId_ :: ConnType -> Maybe Int64
entityId_ ConnContact = contactId entityId_ ConnContact = contactId
@ -150,12 +154,12 @@ toConnection ((connId, acId, connLevel, viaContact, viaUserContactLink, viaGroup
entityId_ ConnUserContact = userContactLinkId entityId_ ConnUserContact = userContactLinkId
toMaybeConnection :: MaybeConnectionRow -> Maybe Connection 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)) = 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)) 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 toMaybeConnection _ = Nothing
createConnection_ :: DB.Connection -> UserId -> ConnType -> Maybe Int64 -> ConnId -> Maybe ContactId -> Maybe Int64 -> Maybe ProfileId -> Int -> UTCTime -> IO Connection 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 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 -> 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) 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 let viaGroupLink = isJust viaLinkGroupId
@ -164,17 +168,30 @@ createConnection_ db userId connType entityId acId viaContact viaUserContactLink
[sql| [sql|
INSERT INTO connections ( 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, 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 contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at, updated_at,
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) peer_chat_min_version, peer_chat_max_version
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|] |]
( (userId, acId, connLevel, viaContact, viaUserContactLink, viaGroupLink, customUserProfileId, ConnNew, connType) ( (userId, acId, connLevel, viaContact, viaUserContactLink, viaGroupLink, customUserProfileId, ConnNew, connType)
:. (ent ConnContact, ent ConnMember, ent ConnSndFile, ent ConnRcvFile, ent ConnUserContact, currentTs, currentTs) :. (ent ConnContact, ent ConnMember, ent ConnSndFile, ent ConnRcvFile, ent ConnUserContact, currentTs, currentTs)
:. (minV, maxV)
) )
connId <- insertedRowId db 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, peerChatVRange, connType, entityId, viaContact, viaUserContactLink, viaGroupLink, groupLinkId = Nothing, customUserProfileId, connLevel, connStatus = ConnNew, localAlias = "", createdAt = currentTs, connectionCode = Nothing, authErrCounter = 0}
where where
ent ct = if connType == ct then entityId else Nothing ent ct = if connType == ct then entityId else Nothing
setPeerChatVRange :: DB.Connection -> Int64 -> VersionRange -> IO ()
setPeerChatVRange db connId (VersionRange minVer maxVer) =
DB.execute
db
[sql|
UPDATE connections
SET peer_chat_min_version = ?, peer_chat_max_version = ?
WHERE connection_id = ?
|]
(minVer, maxVer, connId)
setCommandConnId :: DB.Connection -> User -> CommandId -> Int64 -> IO () setCommandConnId :: DB.Connection -> User -> CommandId -> Int64 -> IO ()
setCommandConnId db User {userId} cmdId connId = do setCommandConnId db User {userId} cmdId connId = do
updatedAt <- getCurrentTime updatedAt <- getCurrentTime
@ -256,12 +273,13 @@ getProfileById db userId profileId =
toProfile :: (ContactName, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Maybe Preferences) -> LocalProfile 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} 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 :: 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} 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 :: Query
userQuery = userQuery =

View File

@ -47,6 +47,7 @@ import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (dropPrefix, fromTextField_, sumTypeJSON, taggedObjectJSON) import Simplex.Messaging.Parsers (dropPrefix, fromTextField_, sumTypeJSON, taggedObjectJSON)
import Simplex.Messaging.Protocol (ProtoServerWithAuth, ProtocolTypeI) import Simplex.Messaging.Protocol (ProtoServerWithAuth, ProtocolTypeI)
import Simplex.Messaging.Util ((<$?>)) import Simplex.Messaging.Util ((<$?>))
import Simplex.Messaging.Version
class IsContact a where class IsContact a where
contactId' :: a -> ContactId contactId' :: a -> ContactId
@ -232,6 +233,7 @@ data UserContactRequest = UserContactRequest
agentInvitationId :: AgentInvId, agentInvitationId :: AgentInvId,
userContactLinkId :: Int64, userContactLinkId :: Int64,
agentContactConnId :: AgentConnId, -- connection id of user contact agentContactConnId :: AgentConnId, -- connection id of user contact
cReqChatVRange :: VersionRange,
localDisplayName :: ContactName, localDisplayName :: ContactName,
profileId :: Int64, profileId :: Int64,
profile :: Profile, profile :: Profile,
@ -538,24 +540,31 @@ instance ToJSON MemberIdRole where toEncoding = J.genericToEncoding J.defaultOpt
data IntroInvitation = IntroInvitation data IntroInvitation = IntroInvitation
{ groupConnReq :: ConnReqInvitation, { groupConnReq :: ConnReqInvitation,
directConnReq :: ConnReqInvitation directConnReq :: Maybe ConnReqInvitation
} }
deriving (Eq, Show, Generic, FromJSON) 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 data MemberInfo = MemberInfo
{ memberId :: MemberId, { memberId :: MemberId,
memberRole :: GroupMemberRole, memberRole :: GroupMemberRole,
v :: Maybe ChatVersionRange,
profile :: Profile profile :: Profile
} }
deriving (Eq, Show, Generic, FromJSON) 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 -> MemberInfo
memberInfo GroupMember {memberId, memberRole, memberProfile} = memberInfo GroupMember {memberId, memberRole, memberProfile, activeConn} =
MemberInfo memberId memberRole (fromLocalProfile memberProfile) MemberInfo memberId memberRole memberChatVRange (fromLocalProfile memberProfile)
where
memberChatVRange = ChatVersionRange . peerChatVRange <$> activeConn
data ReceivedGroupInvitation = ReceivedGroupInvitation data ReceivedGroupInvitation = ReceivedGroupInvitation
{ fromMember :: GroupMember, { fromMember :: GroupMember,
@ -1158,6 +1167,7 @@ type ConnReqContact = ConnectionRequestUri 'CMContact
data Connection = Connection data Connection = Connection
{ connId :: Int64, { connId :: Int64,
agentConnId :: AgentConnId, agentConnId :: AgentConnId,
peerChatVRange :: VersionRange,
connLevel :: Int, connLevel :: Int,
viaContact :: Maybe Int64, -- group member contact ID, if not direct connection viaContact :: Maybe Int64, -- group member contact ID, if not direct connection
viaUserContactLink :: Maybe Int64, -- user contact link ID, if connected via "user address" viaUserContactLink :: Maybe Int64, -- user contact link ID, if connected via "user address"
@ -1468,3 +1478,15 @@ instance ProtocolTypeI p => ToJSON (ServerCfg p) where
instance ProtocolTypeI p => FromJSON (ServerCfg p) where instance ProtocolTypeI p => FromJSON (ServerCfg p) where
parseJSON = J.genericParseJSON J.defaultOptions {J.omitNothingFields = True} 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

View File

@ -59,6 +59,7 @@ import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType, Pro
import qualified Simplex.Messaging.Protocol as SMP import qualified Simplex.Messaging.Protocol as SMP
import Simplex.Messaging.Transport.Client (TransportHost (..)) import Simplex.Messaging.Transport.Client (TransportHost (..))
import Simplex.Messaging.Util (bshow, tshow) import Simplex.Messaging.Util (bshow, tshow)
import Simplex.Messaging.Version hiding (version)
import System.Console.ANSI.Types import System.Console.ANSI.Types
type CurrentTime = UTCTime type CurrentTime = UTCTime
@ -952,7 +953,7 @@ viewNetworkConfig NetworkConfig {socksProxy, tcpTimeout} =
] ]
viewContactInfo :: Contact -> ConnectionStats -> Maybe Profile -> [StyledString] 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] ["contact ID: " <> sShow contactId]
<> viewConnectionStats stats <> viewConnectionStats stats
<> maybe [] (\l -> ["contact address: " <> (plain . strEncode) l]) contactLink <> maybe [] (\l -> ["contact address: " <> (plain . strEncode) l]) contactLink
@ -962,6 +963,7 @@ viewContactInfo ct@Contact {contactId, profile = LocalProfile {localAlias, conta
incognitoProfile incognitoProfile
<> ["alias: " <> plain localAlias | localAlias /= ""] <> ["alias: " <> plain localAlias | localAlias /= ""]
<> [viewConnectionVerified (contactSecurityCode ct)] <> [viewConnectionVerified (contactSecurityCode ct)]
<> [viewPeerChatVRange (peerChatVRange activeConn)]
viewGroupInfo :: GroupInfo -> GroupSummary -> [StyledString] viewGroupInfo :: GroupInfo -> GroupSummary -> [StyledString]
viewGroupInfo GroupInfo {groupId} s = viewGroupInfo GroupInfo {groupId} s =
@ -970,18 +972,22 @@ viewGroupInfo GroupInfo {groupId} s =
] ]
viewGroupMemberInfo :: GroupInfo -> GroupMember -> Maybe ConnectionStats -> [StyledString] 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, [ "group ID: " <> sShow groupId,
"member ID: " <> sShow groupMemberId "member ID: " <> sShow groupMemberId
] ]
<> maybe ["member not connected"] viewConnectionStats stats <> maybe ["member not connected"] viewConnectionStats stats
<> ["alias: " <> plain localAlias | localAlias /= ""] <> ["alias: " <> plain localAlias | localAlias /= ""]
<> [viewConnectionVerified (memberSecurityCode m) | isJust stats] <> [viewConnectionVerified (memberSecurityCode m) | isJust stats]
<> maybe [] (\ac -> [viewPeerChatVRange (peerChatVRange ac)]) activeConn
viewConnectionVerified :: Maybe SecurityCode -> StyledString viewConnectionVerified :: Maybe SecurityCode -> StyledString
viewConnectionVerified (Just _) = "connection verified" -- TODO show verification time? viewConnectionVerified (Just _) = "connection verified" -- TODO show verification time?
viewConnectionVerified _ = "connection not verified, use " <> highlight' "/code" <> " command to see security code" viewConnectionVerified _ = "connection not verified, use " <> highlight' "/code" <> " command to see security code"
viewPeerChatVRange :: VersionRange -> StyledString
viewPeerChatVRange (VersionRange minVer maxVer) = "peer chat protocol version range: (" <> sShow minVer <> ", " <> sShow maxVer <> ")"
viewConnectionStats :: ConnectionStats -> [StyledString] viewConnectionStats :: ConnectionStats -> [StyledString]
viewConnectionStats ConnectionStats {rcvQueuesInfo, sndQueuesInfo} = viewConnectionStats ConnectionStats {rcvQueuesInfo, sndQueuesInfo} =
["receiving messages via: " <> viewRcvQueuesInfo rcvQueuesInfo | not $ null rcvQueuesInfo] ["receiving messages via: " <> viewRcvQueuesInfo rcvQueuesInfo | not $ null rcvQueuesInfo]

View File

@ -49,7 +49,7 @@ extra-deps:
# - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561 # - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561
# - ../simplexmq # - ../simplexmq
- github: simplex-chat/simplexmq - github: simplex-chat/simplexmq
commit: 980e5c4d1ec15f44290542fd2a5d1c08456f00d1 commit: 351f42650c57f310fc1ea858ff9b7178823f1fd4
- github: kazu-yamamoto/http2 - github: kazu-yamamoto/http2
commit: b5a1b7200cf5bc7044af34ba325284271f6dff25 commit: b5a1b7200cf5bc7044af34ba325284271f6dff25
# - ../direct-sqlcipher # - ../direct-sqlcipher

View File

@ -13,13 +13,14 @@ import Control.Monad (forM_)
import Directory.Options import Directory.Options
import Directory.Service import Directory.Service
import Directory.Store import Directory.Store
import GHC.IO.Handle (hClose)
import Simplex.Chat.Bot.KnownContacts import Simplex.Chat.Bot.KnownContacts
import Simplex.Chat.Controller (ChatConfig (..))
import Simplex.Chat.Core import Simplex.Chat.Core
import Simplex.Chat.Options (ChatOpts (..), CoreChatOpts (..)) import Simplex.Chat.Options (ChatOpts (..), CoreChatOpts (..))
import Simplex.Chat.Types (GroupMemberRole (..), Profile (..)) import Simplex.Chat.Types (GroupMemberRole (..), Profile (..))
import System.FilePath ((</>)) import System.FilePath ((</>))
import Test.Hspec import Test.Hspec
import GHC.IO.Handle (hClose)
directoryServiceTests :: SpecWith FilePath directoryServiceTests :: SpecWith FilePath
directoryServiceTests = do directoryServiceTests = do
@ -232,10 +233,10 @@ testJoinGroup tmp =
dan <## "bob (Bob): contact is connected" dan <## "bob (Bob): contact is connected"
dan <## "#privacy: you joined the group" dan <## "#privacy: you joined the group"
dan <# ("#privacy bob> " <> welcomeMsg) dan <# ("#privacy bob> " <> welcomeMsg)
dan <### dan
[ "#privacy: member SimpleX-Directory is connected", <### [ "#privacy: member SimpleX-Directory is connected",
"#privacy: member cath (Catherine) is connected" "#privacy: member cath (Catherine) is connected"
], ],
do do
cath <## "#privacy: bob added dan (Daniel) to the group (connecting...)" cath <## "#privacy: bob added dan (Daniel) to the group (connecting...)"
cath <## "#privacy: new member dan is connected" cath <## "#privacy: new member dan is connected"
@ -243,9 +244,9 @@ testJoinGroup tmp =
testDelistedOwnerLeaves :: HasCallStack => FilePath -> IO () testDelistedOwnerLeaves :: HasCallStack => FilePath -> IO ()
testDelistedOwnerLeaves tmp = testDelistedOwnerLeaves tmp =
withDirectoryService tmp $ \superUser dsLink -> withDirectoryServiceCfg tmp testCfgCreateGroupDirect $ \superUser dsLink ->
withNewTestChat tmp "bob" bobProfile $ \bob -> withNewTestChatCfg tmp testCfgCreateGroupDirect "bob" bobProfile $ \bob ->
withNewTestChat tmp "cath" cathProfile $ \cath -> do withNewTestChatCfg tmp testCfgCreateGroupDirect "cath" cathProfile $ \cath -> do
bob `connectVia` dsLink bob `connectVia` dsLink
registerGroup superUser bob "privacy" "Privacy" registerGroup superUser bob "privacy" "Privacy"
addCathAsOwner bob cath addCathAsOwner bob cath
@ -259,9 +260,9 @@ testDelistedOwnerLeaves tmp =
testDelistedOwnerRemoved :: HasCallStack => FilePath -> IO () testDelistedOwnerRemoved :: HasCallStack => FilePath -> IO ()
testDelistedOwnerRemoved tmp = testDelistedOwnerRemoved tmp =
withDirectoryService tmp $ \superUser dsLink -> withDirectoryServiceCfg tmp testCfgCreateGroupDirect $ \superUser dsLink ->
withNewTestChat tmp "bob" bobProfile $ \bob -> withNewTestChatCfg tmp testCfgCreateGroupDirect "bob" bobProfile $ \bob ->
withNewTestChat tmp "cath" cathProfile $ \cath -> do withNewTestChatCfg tmp testCfgCreateGroupDirect "cath" cathProfile $ \cath -> do
bob `connectVia` dsLink bob `connectVia` dsLink
registerGroup superUser bob "privacy" "Privacy" registerGroup superUser bob "privacy" "Privacy"
addCathAsOwner bob cath addCathAsOwner bob cath
@ -274,9 +275,9 @@ testDelistedOwnerRemoved tmp =
testNotDelistedMemberLeaves :: HasCallStack => FilePath -> IO () testNotDelistedMemberLeaves :: HasCallStack => FilePath -> IO ()
testNotDelistedMemberLeaves tmp = testNotDelistedMemberLeaves tmp =
withDirectoryService tmp $ \superUser dsLink -> withDirectoryServiceCfg tmp testCfgCreateGroupDirect $ \superUser dsLink ->
withNewTestChat tmp "bob" bobProfile $ \bob -> withNewTestChatCfg tmp testCfgCreateGroupDirect "bob" bobProfile $ \bob ->
withNewTestChat tmp "cath" cathProfile $ \cath -> do withNewTestChatCfg tmp testCfgCreateGroupDirect "cath" cathProfile $ \cath -> do
bob `connectVia` dsLink bob `connectVia` dsLink
registerGroup superUser bob "privacy" "Privacy" registerGroup superUser bob "privacy" "Privacy"
addCathAsOwner bob cath addCathAsOwner bob cath
@ -286,10 +287,10 @@ testNotDelistedMemberLeaves tmp =
groupFound cath "privacy" groupFound cath "privacy"
testNotDelistedMemberRemoved :: HasCallStack => FilePath -> IO () testNotDelistedMemberRemoved :: HasCallStack => FilePath -> IO ()
testNotDelistedMemberRemoved tmp = testNotDelistedMemberRemoved tmp =
withDirectoryService tmp $ \superUser dsLink -> withDirectoryServiceCfg tmp testCfgCreateGroupDirect $ \superUser dsLink ->
withNewTestChat tmp "bob" bobProfile $ \bob -> withNewTestChatCfg tmp testCfgCreateGroupDirect "bob" bobProfile $ \bob ->
withNewTestChat tmp "cath" cathProfile $ \cath -> do withNewTestChatCfg tmp testCfgCreateGroupDirect "cath" cathProfile $ \cath -> do
bob `connectVia` dsLink bob `connectVia` dsLink
registerGroup superUser bob "privacy" "Privacy" registerGroup superUser bob "privacy" "Privacy"
addCathAsOwner bob cath addCathAsOwner bob cath
@ -299,9 +300,9 @@ testNotDelistedMemberRemoved tmp =
testDelistedServiceRemoved :: HasCallStack => FilePath -> IO () testDelistedServiceRemoved :: HasCallStack => FilePath -> IO ()
testDelistedServiceRemoved tmp = testDelistedServiceRemoved tmp =
withDirectoryService tmp $ \superUser dsLink -> withDirectoryServiceCfg tmp testCfgCreateGroupDirect $ \superUser dsLink ->
withNewTestChat tmp "bob" bobProfile $ \bob -> withNewTestChatCfg tmp testCfgCreateGroupDirect "bob" bobProfile $ \bob ->
withNewTestChat tmp "cath" cathProfile $ \cath -> do withNewTestChatCfg tmp testCfgCreateGroupDirect "cath" cathProfile $ \cath -> do
bob `connectVia` dsLink bob `connectVia` dsLink
registerGroup superUser bob "privacy" "Privacy" registerGroup superUser bob "privacy" "Privacy"
addCathAsOwner bob cath addCathAsOwner bob cath
@ -316,9 +317,9 @@ testDelistedServiceRemoved tmp =
testDelistedRoleChanges :: HasCallStack => FilePath -> IO () testDelistedRoleChanges :: HasCallStack => FilePath -> IO ()
testDelistedRoleChanges tmp = testDelistedRoleChanges tmp =
withDirectoryService tmp $ \superUser dsLink -> withDirectoryServiceCfg tmp testCfgCreateGroupDirect $ \superUser dsLink ->
withNewTestChat tmp "bob" bobProfile $ \bob -> withNewTestChatCfg tmp testCfgCreateGroupDirect "bob" bobProfile $ \bob ->
withNewTestChat tmp "cath" cathProfile $ \cath -> do withNewTestChatCfg tmp testCfgCreateGroupDirect "cath" cathProfile $ \cath -> do
bob `connectVia` dsLink bob `connectVia` dsLink
registerGroup superUser bob "privacy" "Privacy" registerGroup superUser bob "privacy" "Privacy"
addCathAsOwner bob cath addCathAsOwner bob cath
@ -362,9 +363,9 @@ testDelistedRoleChanges tmp =
testNotDelistedMemberRoleChanged :: HasCallStack => FilePath -> IO () testNotDelistedMemberRoleChanged :: HasCallStack => FilePath -> IO ()
testNotDelistedMemberRoleChanged tmp = testNotDelistedMemberRoleChanged tmp =
withDirectoryService tmp $ \superUser dsLink -> withDirectoryServiceCfg tmp testCfgCreateGroupDirect $ \superUser dsLink ->
withNewTestChat tmp "bob" bobProfile $ \bob -> withNewTestChatCfg tmp testCfgCreateGroupDirect "bob" bobProfile $ \bob ->
withNewTestChat tmp "cath" cathProfile $ \cath -> do withNewTestChatCfg tmp testCfgCreateGroupDirect "cath" cathProfile $ \cath -> do
bob `connectVia` dsLink bob `connectVia` dsLink
registerGroup superUser bob "privacy" "Privacy" registerGroup superUser bob "privacy" "Privacy"
addCathAsOwner bob cath addCathAsOwner bob cath
@ -426,9 +427,9 @@ testNotApprovedBadRoles tmp =
testRegOwnerChangedProfile :: HasCallStack => FilePath -> IO () testRegOwnerChangedProfile :: HasCallStack => FilePath -> IO ()
testRegOwnerChangedProfile tmp = testRegOwnerChangedProfile tmp =
withDirectoryService tmp $ \superUser dsLink -> withDirectoryServiceCfg tmp testCfgCreateGroupDirect $ \superUser dsLink ->
withNewTestChat tmp "bob" bobProfile $ \bob -> withNewTestChatCfg tmp testCfgCreateGroupDirect "bob" bobProfile $ \bob ->
withNewTestChat tmp "cath" cathProfile $ \cath -> do withNewTestChatCfg tmp testCfgCreateGroupDirect "cath" cathProfile $ \cath -> do
bob `connectVia` dsLink bob `connectVia` dsLink
registerGroup superUser bob "privacy" "Privacy" registerGroup superUser bob "privacy" "Privacy"
addCathAsOwner bob cath addCathAsOwner bob cath
@ -445,9 +446,9 @@ testRegOwnerChangedProfile tmp =
testAnotherOwnerChangedProfile :: HasCallStack => FilePath -> IO () testAnotherOwnerChangedProfile :: HasCallStack => FilePath -> IO ()
testAnotherOwnerChangedProfile tmp = testAnotherOwnerChangedProfile tmp =
withDirectoryService tmp $ \superUser dsLink -> withDirectoryServiceCfg tmp testCfgCreateGroupDirect $ \superUser dsLink ->
withNewTestChat tmp "bob" bobProfile $ \bob -> withNewTestChatCfg tmp testCfgCreateGroupDirect "bob" bobProfile $ \bob ->
withNewTestChat tmp "cath" cathProfile $ \cath -> do withNewTestChatCfg tmp testCfgCreateGroupDirect "cath" cathProfile $ \cath -> do
bob `connectVia` dsLink bob `connectVia` dsLink
registerGroup superUser bob "privacy" "Privacy" registerGroup superUser bob "privacy" "Privacy"
addCathAsOwner bob cath addCathAsOwner bob cath
@ -464,9 +465,9 @@ testAnotherOwnerChangedProfile tmp =
testRegOwnerRemovedLink :: HasCallStack => FilePath -> IO () testRegOwnerRemovedLink :: HasCallStack => FilePath -> IO ()
testRegOwnerRemovedLink tmp = testRegOwnerRemovedLink tmp =
withDirectoryService tmp $ \superUser dsLink -> withDirectoryServiceCfg tmp testCfgCreateGroupDirect $ \superUser dsLink ->
withNewTestChat tmp "bob" bobProfile $ \bob -> withNewTestChatCfg tmp testCfgCreateGroupDirect "bob" bobProfile $ \bob ->
withNewTestChat tmp "cath" cathProfile $ \cath -> do withNewTestChatCfg tmp testCfgCreateGroupDirect "cath" cathProfile $ \cath -> do
bob `connectVia` dsLink bob `connectVia` dsLink
registerGroup superUser bob "privacy" "Privacy" registerGroup superUser bob "privacy" "Privacy"
addCathAsOwner bob cath addCathAsOwner bob cath
@ -497,9 +498,9 @@ testRegOwnerRemovedLink tmp =
testAnotherOwnerRemovedLink :: HasCallStack => FilePath -> IO () testAnotherOwnerRemovedLink :: HasCallStack => FilePath -> IO ()
testAnotherOwnerRemovedLink tmp = testAnotherOwnerRemovedLink tmp =
withDirectoryService tmp $ \superUser dsLink -> withDirectoryServiceCfg tmp testCfgCreateGroupDirect $ \superUser dsLink ->
withNewTestChat tmp "bob" bobProfile $ \bob -> withNewTestChatCfg tmp testCfgCreateGroupDirect "bob" bobProfile $ \bob ->
withNewTestChat tmp "cath" cathProfile $ \cath -> do withNewTestChatCfg tmp testCfgCreateGroupDirect "cath" cathProfile $ \cath -> do
bob `connectVia` dsLink bob `connectVia` dsLink
registerGroup superUser bob "privacy" "Privacy" registerGroup superUser bob "privacy" "Privacy"
addCathAsOwner bob cath addCathAsOwner bob cath
@ -646,9 +647,9 @@ testDuplicateProhibitApproval tmp =
testListUserGroups :: HasCallStack => FilePath -> IO () testListUserGroups :: HasCallStack => FilePath -> IO ()
testListUserGroups tmp = testListUserGroups tmp =
withDirectoryService tmp $ \superUser dsLink -> withDirectoryServiceCfg tmp testCfgCreateGroupDirect $ \superUser dsLink ->
withNewTestChat tmp "bob" bobProfile $ \bob -> withNewTestChatCfg tmp testCfgCreateGroupDirect "bob" bobProfile $ \bob ->
withNewTestChat tmp "cath" cathProfile $ \cath -> do withNewTestChatCfg tmp testCfgCreateGroupDirect "cath" cathProfile $ \cath -> do
bob `connectVia` dsLink bob `connectVia` dsLink
cath `connectVia` dsLink cath `connectVia` dsLink
registerGroup superUser bob "privacy" "Privacy" registerGroup superUser bob "privacy" "Privacy"
@ -686,15 +687,15 @@ testRestoreDirectory tmp = do
withTestChat tmp "bob" $ \bob -> withTestChat tmp "bob" $ \bob ->
withTestChat tmp "cath" $ \cath -> do withTestChat tmp "cath" $ \cath -> do
bob <## "2 contacts connected (use /cs for the list)" bob <## "2 contacts connected (use /cs for the list)"
bob <### bob
[ "#privacy (Privacy): connected to server(s)", <### [ "#privacy (Privacy): connected to server(s)",
"#security (Security): connected to server(s)" "#security (Security): connected to server(s)"
] ]
cath <## "2 contacts connected (use /cs for the list)" cath <## "2 contacts connected (use /cs for the list)"
cath <### cath
[ "#privacy (Privacy): connected to server(s)", <### [ "#privacy (Privacy): connected to server(s)",
"#anonymity (Anonymity): connected to server(s)" "#anonymity (Anonymity): connected to server(s)"
] ]
listGroups superUser bob cath listGroups superUser bob cath
groupFoundN 3 bob "privacy" groupFoundN 3 bob "privacy"
groupFound bob "security" groupFound bob "security"
@ -784,14 +785,17 @@ addCathAsOwner bob cath = do
cath <## "#privacy: member SimpleX-Directory is connected" cath <## "#privacy: member SimpleX-Directory is connected"
withDirectoryService :: HasCallStack => FilePath -> (TestCC -> String -> IO ()) -> IO () 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 <- dsLink <-
withNewTestChat tmp serviceDbPrefix directoryProfile $ \ds -> withNewTestChatCfg tmp cfg serviceDbPrefix directoryProfile $ \ds ->
withNewTestChat tmp "super_user" aliceProfile $ \superUser -> do withNewTestChatCfg tmp cfg "super_user" aliceProfile $ \superUser -> do
connectUsers ds superUser connectUsers ds superUser
ds ##> "/ad" ds ##> "/ad"
getContactLink ds True getContactLink ds True
withDirectory tmp dsLink test withDirectory tmp cfg dsLink test
restoreDirectoryService :: HasCallStack => FilePath -> Int -> Int -> (TestCC -> String -> IO ()) -> IO () restoreDirectoryService :: HasCallStack => FilePath -> Int -> Int -> (TestCC -> String -> IO ()) -> IO ()
restoreDirectoryService tmp ctCount grCount test = do restoreDirectoryService tmp ctCount grCount test = do
@ -800,29 +804,29 @@ restoreDirectoryService tmp ctCount grCount test = do
ds <## (show ctCount <> " contacts connected (use /cs for the list)") ds <## (show ctCount <> " contacts connected (use /cs for the list)")
ds <## "Your address is active! To show: /sa" ds <## "Your address is active! To show: /sa"
ds <## (show grCount <> " group links active") ds <## (show grCount <> " group links active")
forM_ [1..grCount] $ \_ -> ds <##. "#" forM_ [1 .. grCount] $ \_ -> ds <##. "#"
ds ##> "/sa" ds ##> "/sa"
dsLink <- getContactLink ds False dsLink <- getContactLink ds False
ds <## "auto_accept on" ds <## "auto_accept on"
pure dsLink pure dsLink
withDirectory tmp dsLink test withDirectory tmp testCfg dsLink test
withDirectory :: HasCallStack => FilePath -> String -> (TestCC -> String -> IO ()) -> IO () withDirectory :: HasCallStack => FilePath -> ChatConfig -> String -> (TestCC -> String -> IO ()) -> IO ()
withDirectory tmp dsLink test = do withDirectory tmp cfg dsLink test = do
let opts = mkDirectoryOpts tmp [KnownContact 2 "alice"] let opts = mkDirectoryOpts tmp [KnownContact 2 "alice"]
runDirectory opts $ runDirectory cfg opts $
withTestChat tmp "super_user" $ \superUser -> do withTestChatCfg tmp cfg "super_user" $ \superUser -> do
superUser <## "1 contacts connected (use /cs for the list)" superUser <## "1 contacts connected (use /cs for the list)"
test superUser dsLink test superUser dsLink
runDirectory :: DirectoryOpts -> IO () -> IO () runDirectory :: ChatConfig -> DirectoryOpts -> IO () -> IO ()
runDirectory opts@DirectoryOpts {directoryLog} action = do runDirectory cfg opts@DirectoryOpts {directoryLog} action = do
st <- restoreDirectoryStore directoryLog st <- restoreDirectoryStore directoryLog
t <- forkIO $ bot st t <- forkIO $ bot st
threadDelay 500000 threadDelay 500000
action `finally` (mapM_ hClose (directoryLogFile st) >> killThread t) action `finally` (mapM_ hClose (directoryLogFile st) >> killThread t)
where 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 :: TestCC -> TestCC -> String -> String -> IO ()
registerGroup su u n fn = registerGroupId su u n fn 1 1 registerGroup su u n fn = registerGroupId su u n fn 1 1

View File

@ -133,6 +133,16 @@ testAgentCfgV1 =
testCfgV1 :: ChatConfig testCfgV1 :: ChatConfig
testCfgV1 = testCfg {agentConfig = testAgentCfgV1} testCfgV1 = testCfg {agentConfig = testAgentCfgV1}
testCfgCreateGroupDirect :: ChatConfig
testCfgCreateGroupDirect =
mkCfgCreateGroupDirect testCfg
mkCfgCreateGroupDirect :: ChatConfig -> ChatConfig
mkCfgCreateGroupDirect cfg = cfg {chatVRange = groupCreateDirectVRange}
groupCreateDirectVRange :: VersionRange
groupCreateDirectVRange = mkVersionRange 1 1
createTestChat :: FilePath -> ChatConfig -> ChatOpts -> String -> Profile -> IO TestCC createTestChat :: FilePath -> ChatConfig -> ChatOpts -> String -> Profile -> IO TestCC
createTestChat tmp cfg opts@ChatOpts {coreOptions = CoreChatOpts {dbKey}} dbPrefix profile = do createTestChat tmp cfg opts@ChatOpts {coreOptions = CoreChatOpts {dbKey}} dbPrefix profile = do
Right db@ChatDatabase {chatStore} <- createChatDatabase (tmp </> dbPrefix) dbKey MCError Right db@ChatDatabase {chatStore} <- createChatDatabase (tmp </> dbPrefix) dbKey MCError
@ -288,7 +298,10 @@ testChatCfgOpts3 cfg opts p1 p2 p3 test = testChatN cfg opts [p1, p2, p3] test_
test_ _ = error "expected 3 chat clients" test_ _ = error "expected 3 chat clients"
testChat4 :: HasCallStack => Profile -> Profile -> Profile -> Profile -> (HasCallStack => TestCC -> TestCC -> TestCC -> TestCC -> IO ()) -> FilePath -> IO () 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 where
test_ :: HasCallStack => [TestCC] -> IO () test_ :: HasCallStack => [TestCC] -> IO ()
test_ [tc1, tc2, tc3, tc4] = test tc1 tc2 tc3 tc4 test_ [tc1, tc2, tc3, tc4] = test tc1 tc2 tc3 tc4

View File

@ -17,9 +17,11 @@ import qualified Data.ByteString.Lazy.Char8 as LB
import Simplex.Chat.Call import Simplex.Chat.Call
import Simplex.Chat.Controller (ChatConfig (..)) import Simplex.Chat.Controller (ChatConfig (..))
import Simplex.Chat.Options (ChatOpts (..)) import Simplex.Chat.Options (ChatOpts (..))
import Simplex.Chat.Protocol (supportedChatVRange)
import Simplex.Chat.Store (agentStoreFile, chatStoreFile) import Simplex.Chat.Store (agentStoreFile, chatStoreFile)
import Simplex.Chat.Types (authErrDisableCount, sameVerificationCode, verificationCode) import Simplex.Chat.Types (authErrDisableCount, sameVerificationCode, verificationCode)
import qualified Simplex.Messaging.Crypto as C import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Version
import System.Directory (copyFile, doesDirectoryExist, doesFileExist) import System.Directory (copyFile, doesDirectoryExist, doesFileExist)
import System.FilePath ((</>)) import System.FilePath ((</>))
import Test.Hspec import Test.Hspec
@ -94,6 +96,21 @@ chatDirectTests = do
describe "delivery receipts" $ do describe "delivery receipts" $ do
it "should send delivery receipts" testSendDeliveryReceipts it "should send delivery receipts" testSendDeliveryReceipts
it "should send delivery receipts depending on configuration" testConfigureDeliveryReceipts it "should send delivery receipts depending on configuration" testConfigureDeliveryReceipts
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 "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 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
testAddContact :: HasCallStack => SpecWith FilePath testAddContact :: HasCallStack => SpecWith FilePath
testAddContact = versionTestMatrix2 runTestAddContact testAddContact = versionTestMatrix2 runTestAddContact
@ -1939,8 +1956,7 @@ testMarkContactVerified =
testChat2 aliceProfile bobProfile $ \alice bob -> do testChat2 aliceProfile bobProfile $ \alice bob -> do
connectUsers alice bob connectUsers alice bob
alice ##> "/i bob" alice ##> "/i bob"
bobInfo alice bobInfo alice False
alice <## "connection not verified, use /code command to see security code"
alice ##> "/code bob" alice ##> "/code bob"
bCode <- getTermLine alice bCode <- getTermLine alice
bob ##> "/code alice" bob ##> "/code alice"
@ -1951,28 +1967,31 @@ testMarkContactVerified =
alice ##> ("/verify bob " <> aCode) alice ##> ("/verify bob " <> aCode)
alice <## "connection verified" alice <## "connection verified"
alice ##> "/i bob" alice ##> "/i bob"
bobInfo alice bobInfo alice True
alice <## "connection verified"
alice ##> "/verify bob" alice ##> "/verify bob"
alice <##. "connection not verified, current code is " alice <##. "connection not verified, current code is "
alice ##> "/i bob" alice ##> "/i bob"
bobInfo alice bobInfo alice False
alice <## "connection not verified, use /code command to see security code"
where where
bobInfo :: HasCallStack => TestCC -> IO () bobInfo :: HasCallStack => TestCC -> Bool -> IO ()
bobInfo alice = do bobInfo alice verified = do
alice <## "contact ID: 2" alice <## "contact ID: 2"
alice <## "receiving messages via: localhost" alice <## "receiving messages via: localhost"
alice <## "sending messages via: localhost" alice <## "sending messages via: localhost"
alice <## "you've shared main profile with this contact" 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 :: HasCallStack => FilePath -> IO ()
testMarkGroupMemberVerified = testMarkGroupMemberVerified =
testChat2 aliceProfile bobProfile $ \alice bob -> do testChat2 aliceProfile bobProfile $ \alice bob -> do
createGroup2 "team" alice bob createGroup2 "team" alice bob
alice ##> "/i #team bob" alice ##> "/i #team bob"
bobInfo alice bobInfo alice False
alice <## "connection not verified, use /code command to see security code"
alice ##> "/code #team bob" alice ##> "/code #team bob"
bCode <- getTermLine alice bCode <- getTermLine alice
bob ##> "/code #team alice" bob ##> "/code #team alice"
@ -1983,20 +2002,24 @@ testMarkGroupMemberVerified =
alice ##> ("/verify #team bob " <> aCode) alice ##> ("/verify #team bob " <> aCode)
alice <## "connection verified" alice <## "connection verified"
alice ##> "/i #team bob" alice ##> "/i #team bob"
bobInfo alice bobInfo alice True
alice <## "connection verified"
alice ##> "/verify #team bob" alice ##> "/verify #team bob"
alice <##. "connection not verified, current code is " alice <##. "connection not verified, current code is "
alice ##> "/i #team bob" alice ##> "/i #team bob"
bobInfo alice bobInfo alice False
alice <## "connection not verified, use /code command to see security code"
where where
bobInfo :: HasCallStack => TestCC -> IO () bobInfo :: HasCallStack => TestCC -> Bool -> IO ()
bobInfo alice = do bobInfo alice verified = do
alice <## "group ID: 1" alice <## "group ID: 1"
alice <## "member ID: 2" alice <## "member ID: 2"
alice <## "receiving messages via: localhost" alice <## "receiving messages via: localhost"
alice <## "sending 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 :: HasCallStack => FilePath -> IO ()
testMsgDecryptError tmp = testMsgDecryptError tmp =
@ -2088,8 +2111,7 @@ testSyncRatchetCodeReset tmp =
alice <# "bob> hey" alice <# "bob> hey"
-- connection not verified -- connection not verified
bob ##> "/i alice" bob ##> "/i alice"
aliceInfo bob aliceInfo bob False
bob <## "connection not verified, use /code command to see security code"
-- verify connection -- verify connection
alice ##> "/code bob" alice ##> "/code bob"
bCode <- getTermLine alice bCode <- getTermLine alice
@ -2097,8 +2119,7 @@ testSyncRatchetCodeReset tmp =
bob <## "connection verified" bob <## "connection verified"
-- connection verified -- connection verified
bob ##> "/i alice" bob ##> "/i alice"
aliceInfo bob aliceInfo bob True
bob <## "connection verified"
setupDesynchronizedRatchet tmp alice setupDesynchronizedRatchet tmp alice
withTestChat tmp "bob_old" $ \bob -> do withTestChat tmp "bob_old" $ \bob -> do
bob <## "1 contacts connected (use /cs for the list)" bob <## "1 contacts connected (use /cs for the list)"
@ -2115,20 +2136,25 @@ testSyncRatchetCodeReset tmp =
-- connection not verified -- connection not verified
bob ##> "/i alice" bob ##> "/i alice"
aliceInfo bob aliceInfo bob False
bob <## "connection not verified, use /code command to see security code"
alice #> "@bob hello again" alice #> "@bob hello again"
bob <# "alice> hello again" bob <# "alice> hello again"
bob #> "@alice received!" bob #> "@alice received!"
alice <# "bob> received!" alice <# "bob> received!"
where where
aliceInfo :: HasCallStack => TestCC -> IO () aliceInfo :: HasCallStack => TestCC -> Bool -> IO ()
aliceInfo bob = do aliceInfo bob verified = do
bob <## "contact ID: 2" bob <## "contact ID: 2"
bob <## "receiving messages via: localhost" bob <## "receiving messages via: localhost"
bob <## "sending messages via: localhost" bob <## "sending messages via: localhost"
bob <## "you've shared main profile with this contact" 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 :: HasCallStack => FilePath -> IO ()
testSetMessageReactions = testSetMessageReactions =
@ -2271,3 +2297,85 @@ testConfigureDeliveryReceipts tmp =
cc1 #> ("@" <> name2 <> " " <> msg) cc1 #> ("@" <> name2 <> " " <> msg)
cc2 <# (name1 <> "> " <> msg) cc2 <# (name1 <> "> " <> msg)
cc1 <// 50000 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
testUpdatePeerChatVRange :: HasCallStack => FilePath -> IO ()
testUpdatePeerChatVRange 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 <## ("peer chat protocol version range: (" <> show minVer <> ", " <> show maxVer <> ")")

View File

@ -51,7 +51,7 @@ chatFileTests = do
it "files folder: sender deleted file during transfer" testFilesFoldersImageSndDelete it "files folder: sender deleted file during transfer" testFilesFoldersImageSndDelete
it "files folder: recipient deleted file during transfer" testFilesFoldersImageRcvDelete it "files folder: recipient deleted file during transfer" testFilesFoldersImageRcvDelete
it "send and receive image with text and quote" testSendImageWithTextAndQuote 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 it "send and receive image with text and quote to group" testGroupSendImageWithTextAndQuote
describe "async sending and receiving files" $ do describe "async sending and receiving files" $ do
-- fails on CI -- fails on CI
@ -730,11 +730,10 @@ testSendImageWithTextAndQuote =
(alice <## "completed sending file 3 (test.jpg) to bob") (alice <## "completed sending file 3 (test.jpg) to bob")
B.readFile "./tests/tmp/test_1.jpg" `shouldReturn` src B.readFile "./tests/tmp/test_1.jpg" `shouldReturn` src
testGroupSendImage :: SpecWith FilePath testGroupSendImage :: HasCallStack => FilePath -> IO ()
testGroupSendImage = versionTestMatrix3 runTestGroupSendImage testGroupSendImage =
where testChat3 aliceProfile bobProfile cathProfile $
runTestGroupSendImage :: HasCallStack => TestCC -> TestCC -> TestCC -> IO () \alice bob cath -> do
runTestGroupSendImage alice bob cath = do
createGroup3 "team" alice bob cath createGroup3 "team" alice bob cath
threadDelay 1000000 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=\"}}" alice ##> "/_send #1 json {\"filePath\": \"./tests/fixtures/test.jpg\", \"msgContent\": {\"text\":\"\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}"

View File

@ -10,8 +10,10 @@ import Control.Concurrent.Async (concurrently_)
import Control.Monad (when) import Control.Monad (when)
import qualified Data.Text as T import qualified Data.Text as T
import Simplex.Chat.Controller (ChatConfig (..)) import Simplex.Chat.Controller (ChatConfig (..))
import Simplex.Chat.Protocol (supportedChatVRange)
import Simplex.Chat.Store (agentStoreFile, chatStoreFile) import Simplex.Chat.Store (agentStoreFile, chatStoreFile)
import Simplex.Chat.Types (GroupMemberRole (..)) import Simplex.Chat.Types (GroupMemberRole (..))
import Simplex.Messaging.Version
import System.Directory (copyFile) import System.Directory (copyFile)
import System.FilePath ((</>)) import System.FilePath ((</>))
import Test.Hspec import Test.Hspec
@ -19,7 +21,7 @@ import Test.Hspec
chatGroupTests :: SpecWith FilePath chatGroupTests :: SpecWith FilePath
chatGroupTests = do chatGroupTests = do
describe "chat groups" $ 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 "add contacts, create group and send/receive messages, check messages" testGroupCheckMessages
it "create and join group with 4 members" testGroup2 it "create and join group with 4 members" testGroup2
it "create and delete group" testGroupDelete it "create and delete group" testGroupDelete
@ -64,15 +66,54 @@ chatGroupTests = do
describe "group delivery receipts" $ do describe "group delivery receipts" $ do
it "should send delivery receipts in group" testSendGroupDeliveryReceipts it "should send delivery receipts in group" testSendGroupDeliveryReceipts
it "should send delivery receipts in group depending on configuration" testConfigureGroupDeliveryReceipts 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
testGroup :: HasCallStack => SpecWith FilePath describe "3 members group" $ do
testGroup = versionTestMatrix3 runTestGroup 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 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 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 :: HasCallStack => FilePath -> IO ()
testGroupCheckMessages = testGroupCheckMessages =
testChat3 aliceProfile bobProfile cathProfile $ testChatCfg3 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile $
\alice bob cath -> testGroupShared alice bob cath True \alice bob cath -> testGroupShared alice bob cath True
testGroupShared :: HasCallStack => TestCC -> TestCC -> TestCC -> Bool -> IO () testGroupShared :: HasCallStack => TestCC -> TestCC -> TestCC -> Bool -> IO ()
@ -233,7 +274,7 @@ testGroupShared alice bob cath checkMessages = do
testGroup2 :: HasCallStack => FilePath -> IO () testGroup2 :: HasCallStack => FilePath -> IO ()
testGroup2 = testGroup2 =
testChat4 aliceProfile bobProfile cathProfile danProfile $ testChatCfg4 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile danProfile $
\alice bob cath dan -> do \alice bob cath dan -> do
connectUsers alice bob connectUsers alice bob
connectUsers alice cath connectUsers alice cath
@ -679,7 +720,7 @@ testDeleteGroupMemberProfileKept =
testGroupRemoveAdd :: HasCallStack => FilePath -> IO () testGroupRemoveAdd :: HasCallStack => FilePath -> IO ()
testGroupRemoveAdd = testGroupRemoveAdd =
testChat3 aliceProfile bobProfile cathProfile $ testChatCfg3 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile $
\alice bob cath -> do \alice bob cath -> do
createGroup3 "team" alice bob cath createGroup3 "team" alice bob cath
-- remove member -- remove member
@ -754,7 +795,7 @@ testGroupList =
testGroupMessageQuotedReply :: HasCallStack => FilePath -> IO () testGroupMessageQuotedReply :: HasCallStack => FilePath -> IO ()
testGroupMessageQuotedReply = testGroupMessageQuotedReply =
testChat3 aliceProfile bobProfile cathProfile $ testChatCfg3 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile $
\alice bob cath -> do \alice bob cath -> do
createGroup3 "team" alice bob cath createGroup3 "team" alice bob cath
threadDelay 1000000 threadDelay 1000000
@ -1232,7 +1273,7 @@ testGroupDeleteUnusedContacts =
cath <## "alice (Alice)" cath <## "alice (Alice)"
cath `hasContactProfiles` ["alice", "cath"] cath `hasContactProfiles` ["alice", "cath"]
where 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 :: HasCallStack => TestCC -> TestCC -> TestCC -> String -> IO ()
deleteGroup alice bob cath group = do deleteGroup alice bob cath group = do
alice ##> ("/d #" <> group) alice ##> ("/d #" <> group)
@ -1321,7 +1362,7 @@ testGroupDescription = testChat4 aliceProfile bobProfile cathProfile danProfile
testGroupModerate :: HasCallStack => FilePath -> IO () testGroupModerate :: HasCallStack => FilePath -> IO ()
testGroupModerate = testGroupModerate =
testChat3 aliceProfile bobProfile cathProfile $ testChatCfg3 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile $
\alice bob cath -> do \alice bob cath -> do
createGroup3 "team" alice bob cath createGroup3 "team" alice bob cath
alice ##> "/mr team cath member" alice ##> "/mr team cath member"
@ -1352,7 +1393,7 @@ testGroupModerate =
testGroupModerateFullDelete :: HasCallStack => FilePath -> IO () testGroupModerateFullDelete :: HasCallStack => FilePath -> IO ()
testGroupModerateFullDelete = testGroupModerateFullDelete =
testChat3 aliceProfile bobProfile cathProfile $ testChatCfg3 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile $
\alice bob cath -> do \alice bob cath -> do
createGroup3 "team" alice bob cath createGroup3 "team" alice bob cath
alice ##> "/mr team cath member" alice ##> "/mr team cath member"
@ -1390,10 +1431,10 @@ testGroupModerateFullDelete =
testGroupDelayedModeration :: HasCallStack => FilePath -> IO () testGroupDelayedModeration :: HasCallStack => FilePath -> IO ()
testGroupDelayedModeration tmp = do testGroupDelayedModeration tmp = do
withNewTestChat tmp "alice" aliceProfile $ \alice -> do withNewTestChatCfg tmp cfg "alice" aliceProfile $ \alice -> do
withNewTestChat tmp "bob" bobProfile $ \bob -> do withNewTestChatCfg tmp cfg "bob" bobProfile $ \bob -> do
createGroup2 "team" alice bob createGroup2 "team" alice bob
withNewTestChat tmp "cath" cathProfile $ \cath -> do withNewTestChatCfg tmp cfg "cath" cathProfile $ \cath -> do
connectUsers alice cath connectUsers alice cath
addMember "team" alice cath GRMember addMember "team" alice cath GRMember
cath ##> "/j team" cath ##> "/j team"
@ -1407,11 +1448,11 @@ testGroupDelayedModeration tmp = do
alice ##> "\\\\ #team @cath hi" alice ##> "\\\\ #team @cath hi"
alice <## "message marked deleted by you" alice <## "message marked deleted by you"
cath <# "#team cath> [marked deleted by alice] hi" 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 <## "1 contacts connected (use /cs for the list)"
bob <## "#team: connected to server(s)" bob <## "#team: connected to server(s)"
bob <## "#team: alice added cath (Catherine) to the group (connecting...)" 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 <## "2 contacts connected (use /cs for the list)"
cath <## "#team: connected to server(s)" cath <## "#team: connected to server(s)"
cath <## "#team: member bob (Bob) is connected" cath <## "#team: member bob (Bob) is connected"
@ -1424,13 +1465,15 @@ testGroupDelayedModeration tmp = do
bob ##> "/_get chat #1 count=2" bob ##> "/_get chat #1 count=2"
r <- chat <$> getTermLine bob r <- chat <$> getTermLine bob
r `shouldMatchList` [(0, "connected"), (0, "hi [marked deleted by alice]")] r `shouldMatchList` [(0, "connected"), (0, "hi [marked deleted by alice]")]
where
cfg = testCfgCreateGroupDirect
testGroupDelayedModerationFullDelete :: HasCallStack => FilePath -> IO () testGroupDelayedModerationFullDelete :: HasCallStack => FilePath -> IO ()
testGroupDelayedModerationFullDelete tmp = do testGroupDelayedModerationFullDelete tmp = do
withNewTestChat tmp "alice" aliceProfile $ \alice -> do withNewTestChatCfg tmp cfg "alice" aliceProfile $ \alice -> do
withNewTestChat tmp "bob" bobProfile $ \bob -> do withNewTestChatCfg tmp cfg "bob" bobProfile $ \bob -> do
createGroup2 "team" alice bob createGroup2 "team" alice bob
withNewTestChat tmp "cath" cathProfile $ \cath -> do withNewTestChatCfg tmp cfg "cath" cathProfile $ \cath -> do
connectUsers alice cath connectUsers alice cath
addMember "team" alice cath GRMember addMember "team" alice cath GRMember
cath ##> "/j team" cath ##> "/j team"
@ -1452,14 +1495,14 @@ testGroupDelayedModerationFullDelete tmp = do
cath <## "alice updated group #team:" cath <## "alice updated group #team:"
cath <## "updated group preferences:" cath <## "updated group preferences:"
cath <## "Full deletion: on" 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 <## "1 contacts connected (use /cs for the list)"
bob <## "#team: connected to server(s)" bob <## "#team: connected to server(s)"
bob <## "#team: alice added cath (Catherine) to the group (connecting...)" bob <## "#team: alice added cath (Catherine) to the group (connecting...)"
bob <## "alice updated group #team:" bob <## "alice updated group #team:"
bob <## "updated group preferences:" bob <## "updated group preferences:"
bob <## "Full deletion: on" 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 <## "2 contacts connected (use /cs for the list)"
cath <## "#team: connected to server(s)" cath <## "#team: connected to server(s)"
cath <## "#team: member bob (Bob) is connected" cath <## "#team: member bob (Bob) is connected"
@ -1472,6 +1515,8 @@ testGroupDelayedModerationFullDelete tmp = do
bob ##> "/_get chat #1 count=3" bob ##> "/_get chat #1 count=3"
r <- chat <$> getTermLine bob r <- chat <$> getTermLine bob
r `shouldMatchList` [(0, "Full deletion: on"), (0, "connected"), (0, "moderated [deleted by alice]")] r `shouldMatchList` [(0, "Full deletion: on"), (0, "connected"), (0, "moderated [deleted by alice]")]
where
cfg = testCfgCreateGroupDirect
testGroupAsync :: HasCallStack => FilePath -> IO () testGroupAsync :: HasCallStack => FilePath -> IO ()
testGroupAsync tmp = do testGroupAsync tmp = do
@ -2127,7 +2172,7 @@ testGroupLinkMemberRole =
testGroupLinkLeaveDelete :: HasCallStack => FilePath -> IO () testGroupLinkLeaveDelete :: HasCallStack => FilePath -> IO ()
testGroupLinkLeaveDelete = testGroupLinkLeaveDelete =
testChat3 aliceProfile bobProfile cathProfile $ testChatCfg3 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile $
\alice bob cath -> do \alice bob cath -> do
connectUsers alice bob connectUsers alice bob
connectUsers cath bob connectUsers cath bob
@ -2289,8 +2334,7 @@ testGroupSyncRatchetCodeReset tmp =
alice <# "#team bob> hey" alice <# "#team bob> hey"
-- connection not verified -- connection not verified
bob ##> "/i #team alice" bob ##> "/i #team alice"
aliceInfo bob aliceInfo bob False
bob <## "connection not verified, use /code command to see security code"
-- verify connection -- verify connection
alice ##> "/code #team bob" alice ##> "/code #team bob"
bCode <- getTermLine alice bCode <- getTermLine alice
@ -2298,8 +2342,7 @@ testGroupSyncRatchetCodeReset tmp =
bob <## "connection verified" bob <## "connection verified"
-- connection verified -- connection verified
bob ##> "/i #team alice" bob ##> "/i #team alice"
aliceInfo bob aliceInfo bob True
bob <## "connection verified"
setupDesynchronizedRatchet tmp alice setupDesynchronizedRatchet tmp alice
withTestChat tmp "bob_old" $ \bob -> do withTestChat tmp "bob_old" $ \bob -> do
bob <## "1 contacts connected (use /cs for the list)" bob <## "1 contacts connected (use /cs for the list)"
@ -2317,20 +2360,25 @@ testGroupSyncRatchetCodeReset tmp =
-- connection not verified -- connection not verified
bob ##> "/i #team alice" bob ##> "/i #team alice"
aliceInfo bob aliceInfo bob False
bob <## "connection not verified, use /code command to see security code"
alice #> "#team hello again" alice #> "#team hello again"
bob <# "#team alice> hello again" bob <# "#team alice> hello again"
bob #> "#team received!" bob #> "#team received!"
alice <# "#team bob> received!" alice <# "#team bob> received!"
where where
aliceInfo :: HasCallStack => TestCC -> IO () aliceInfo :: HasCallStack => TestCC -> Bool -> IO ()
aliceInfo bob = do aliceInfo bob verified = do
bob <## "group ID: 1" bob <## "group ID: 1"
bob <## "member ID: 1" bob <## "member ID: 1"
bob <## "receiving messages via: localhost" bob <## "receiving messages via: localhost"
bob <## "sending 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 :: HasCallStack => FilePath -> IO ()
testSetGroupMessageReactions = testSetGroupMessageReactions =
@ -2559,7 +2607,7 @@ testConfigureGroupDeliveryReceipts tmp =
receipt bob alice cath "team" "25" receipt bob alice cath "team" "25"
noReceipt bob alice cath "club" "26" noReceipt bob alice cath "club" "26"
where where
cfg = testCfg {showReceipts = True} cfg = mkCfgCreateGroupDirect $ testCfg {showReceipts = True}
receipt cc1 cc2 cc3 gName msg = do receipt cc1 cc2 cc3 gName msg = do
name1 <- userName cc1 name1 <- userName cc1
cc1 #> ("#" <> gName <> " " <> msg) cc1 #> ("#" <> gName <> " " <> msg)
@ -2579,3 +2627,62 @@ testConfigureGroupDeliveryReceipts tmp =
cc2 <# ("#" <> gName <> " " <> name1 <> "> " <> msg) cc2 <# ("#" <> gName <> " " <> name1 <> "> " <> msg)
cc3 <# ("#" <> gName <> " " <> name1 <> "> " <> msg) cc3 <# ("#" <> gName <> " " <> name1 <> "> " <> msg)
cc1 <// 50000 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)

View File

@ -18,7 +18,7 @@ chatProfileTests = do
it "update user profile and notify contacts" testUpdateProfile it "update user profile and notify contacts" testUpdateProfile
it "update user profile with image" testUpdateProfileImage it "update user profile with image" testUpdateProfileImage
describe "user contact link" $ do 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 "add contact link to profile" testProfileLink
it "auto accept contact requests" testUserContactLinkAutoAccept it "auto accept contact requests" testUserContactLinkAutoAccept
it "deduplicate contact requests" testDeduplicateContactRequests it "deduplicate contact requests" testDeduplicateContactRequests
@ -57,7 +57,7 @@ chatProfileTests = do
testUpdateProfile :: HasCallStack => FilePath -> IO () testUpdateProfile :: HasCallStack => FilePath -> IO ()
testUpdateProfile = testUpdateProfile =
testChat3 aliceProfile bobProfile cathProfile $ testChatCfg3 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile $
\alice bob cath -> do \alice bob cath -> do
createGroup3 "team" alice bob cath createGroup3 "team" alice bob cath
alice ##> "/p" alice ##> "/p"
@ -117,33 +117,35 @@ testUpdateProfileImage =
bob <## "use @alice2 <message> to send messages" bob <## "use @alice2 <message> to send messages"
(bob </) (bob </)
testUserContactLink :: SpecWith FilePath testUserContactLink :: HasCallStack => FilePath -> IO ()
testUserContactLink = versionTestMatrix3 $ \alice bob cath -> do testUserContactLink =
alice ##> "/ad" testChat3 aliceProfile bobProfile cathProfile $
cLink <- getContactLink alice True \alice bob cath -> do
bob ##> ("/c " <> cLink) alice ##> "/ad"
alice <#? bob cLink <- getContactLink alice True
alice @@@ [("<@bob", "")] bob ##> ("/c " <> cLink)
alice ##> "/ac bob" alice <#? bob
alice <## "bob (Bob): accepting contact request..." alice @@@ [("<@bob", "")]
concurrently_ alice ##> "/ac bob"
(bob <## "alice (Alice): contact is connected") alice <## "bob (Bob): accepting contact request..."
(alice <## "bob (Bob): contact is connected") concurrently_
threadDelay 100000 (bob <## "alice (Alice): contact is connected")
alice @@@ [("@bob", lastChatFeature)] (alice <## "bob (Bob): contact is connected")
alice <##> bob threadDelay 100000
alice @@@ [("@bob", lastChatFeature)]
alice <##> bob
cath ##> ("/c " <> cLink) cath ##> ("/c " <> cLink)
alice <#? cath alice <#? cath
alice @@@ [("<@cath", ""), ("@bob", "hey")] alice @@@ [("<@cath", ""), ("@bob", "hey")]
alice ##> "/ac cath" alice ##> "/ac cath"
alice <## "cath (Catherine): accepting contact request..." alice <## "cath (Catherine): accepting contact request..."
concurrently_ concurrently_
(cath <## "alice (Alice): contact is connected") (cath <## "alice (Alice): contact is connected")
(alice <## "cath (Catherine): contact is connected") (alice <## "cath (Catherine): contact is connected")
threadDelay 100000 threadDelay 100000
alice @@@ [("@cath", lastChatFeature), ("@bob", "hey")] alice @@@ [("@cath", lastChatFeature), ("@bob", "hey")]
alice <##> cath alice <##> cath
testProfileLink :: HasCallStack => FilePath -> IO () testProfileLink :: HasCallStack => FilePath -> IO ()
testProfileLink = testProfileLink =
@ -214,6 +216,7 @@ testProfileLink =
cc <## ("contact address: " <> cLink) cc <## ("contact address: " <> cLink)
cc <## "you've shared main profile with this contact" cc <## "you've shared main profile with this contact"
cc <## "connection not verified, use /code command to see security code" cc <## "connection not verified, use /code command to see security code"
cc <## currentChatVRangeInfo
checkAliceNoProfileLink cc = do checkAliceNoProfileLink cc = do
cc ##> "/info alice" cc ##> "/info alice"
cc <## "contact ID: 2" cc <## "contact ID: 2"
@ -221,6 +224,7 @@ testProfileLink =
cc <##. "sending messages via" cc <##. "sending messages via"
cc <## "you've shared main profile with this contact" cc <## "you've shared main profile with this contact"
cc <## "connection not verified, use /code command to see security code" cc <## "connection not verified, use /code command to see security code"
cc <## currentChatVRangeInfo
testUserContactLinkAutoAccept :: HasCallStack => FilePath -> IO () testUserContactLinkAutoAccept :: HasCallStack => FilePath -> IO ()
testUserContactLinkAutoAccept = testUserContactLinkAutoAccept =
@ -760,192 +764,193 @@ testSetResetSetConnectionIncognito = testChat2 aliceProfile bobProfile $
bob `hasContactProfiles` ["bob", T.pack aliceIncognito] bob `hasContactProfiles` ["bob", T.pack aliceIncognito]
testJoinGroupIncognito :: HasCallStack => FilePath -> IO () testJoinGroupIncognito :: HasCallStack => FilePath -> IO ()
testJoinGroupIncognito = testChat4 aliceProfile bobProfile cathProfile danProfile $ testJoinGroupIncognito =
\alice bob cath dan -> do testChatCfg4 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile danProfile $
-- non incognito connections \alice bob cath dan -> do
connectUsers alice bob -- non incognito connections
connectUsers alice dan connectUsers alice bob
connectUsers bob cath connectUsers alice dan
connectUsers bob dan connectUsers bob cath
connectUsers cath dan connectUsers bob dan
-- cath connected incognito to alice connectUsers cath dan
alice ##> "/c" -- cath connected incognito to alice
inv <- getInvitation alice alice ##> "/c"
cath ##> ("/c i " <> inv) inv <- getInvitation alice
cath <## "confirmation sent!" cath ##> ("/c i " <> inv)
cathIncognito <- getTermLine cath cath <## "confirmation sent!"
concurrentlyN_ cathIncognito <- getTermLine cath
[ do concurrentlyN_
cath <## ("alice (Alice): contact is connected, your incognito profile for this contact is " <> cathIncognito) [ do
cath <## "use /i alice to print out this incognito profile again", cath <## ("alice (Alice): contact is connected, your incognito profile for this contact is " <> cathIncognito)
alice <## (cathIncognito <> ": contact is connected") cath <## "use /i alice to print out this incognito profile again",
] alice <## (cathIncognito <> ": contact is connected")
-- alice creates group ]
alice ##> "/g secret_club" -- alice creates group
alice <## "group #secret_club is created" alice ##> "/g secret_club"
alice <## "to add members use /a secret_club <name> or /create link #secret_club" alice <## "group #secret_club is created"
-- alice invites bob alice <## "to add members use /a secret_club <name> or /create link #secret_club"
alice ##> "/a secret_club bob admin" -- alice invites bob
concurrentlyN_ alice ##> "/a secret_club bob admin"
[ alice <## "invitation to join the group #secret_club sent to bob", concurrentlyN_
do [ alice <## "invitation to join the group #secret_club sent to bob",
bob <## "#secret_club: alice invites you to join the group as admin" do
bob <## "use /j secret_club to accept" bob <## "#secret_club: alice invites you to join the group as admin"
] bob <## "use /j secret_club to accept"
bob ##> "/j secret_club" ]
concurrently_ bob ##> "/j secret_club"
(alice <## "#secret_club: bob joined the group") concurrently_
(bob <## "#secret_club: you joined the group") (alice <## "#secret_club: bob joined the group")
-- alice invites cath (bob <## "#secret_club: you joined the group")
alice ##> ("/a secret_club " <> cathIncognito <> " admin") -- alice invites cath
concurrentlyN_ alice ##> ("/a secret_club " <> cathIncognito <> " admin")
[ alice <## ("invitation to join the group #secret_club sent to " <> cathIncognito), concurrentlyN_
do [ alice <## ("invitation to join the group #secret_club sent to " <> cathIncognito),
cath <## "#secret_club: alice invites you to join the group as admin" do
cath <## ("use /j secret_club to join incognito as " <> cathIncognito) 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" -- cath uses the same incognito profile when joining group, cath and bob don't merge contacts
concurrentlyN_ cath ##> "/j secret_club"
[ alice <## ("#secret_club: " <> cathIncognito <> " joined the group"), concurrentlyN_
do [ alice <## ("#secret_club: " <> cathIncognito <> " joined the group"),
cath <## ("#secret_club: you joined the group incognito as " <> cathIncognito) do
cath <## "#secret_club: member bob_1 (Bob) is connected", cath <## ("#secret_club: you joined the group incognito as " <> cathIncognito)
do cath <## "#secret_club: member bob_1 (Bob) is connected",
bob <## ("#secret_club: alice added " <> cathIncognito <> " to the group (connecting...)") do
bob <## ("#secret_club: new member " <> cathIncognito <> " is connected") 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 cannot invite to the group because her membership is incognito
cath <## "you've connected to this group using an incognito profile - prohibited to invite contacts" cath ##> "/a secret_club dan"
-- alice invites dan cath <## "you've connected to this group using an incognito profile - prohibited to invite contacts"
alice ##> "/a secret_club dan admin" -- alice invites dan
concurrentlyN_ alice ##> "/a secret_club dan admin"
[ alice <## "invitation to join the group #secret_club sent to dan", concurrentlyN_
do [ alice <## "invitation to join the group #secret_club sent to dan",
dan <## "#secret_club: alice invites you to join the group as admin" do
dan <## "use /j secret_club to accept" 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 dan ##> "/j secret_club"
concurrentlyN_ -- cath and dan don't merge contacts
[ alice <## "#secret_club: dan joined the group", concurrentlyN_
do [ alice <## "#secret_club: dan joined the group",
dan <## "#secret_club: you joined the group" do
dan dan <## "#secret_club: you joined the group"
<### [ ConsoleString $ "#secret_club: member " <> cathIncognito <> " is connected", dan
"#secret_club: member bob_1 (Bob) is connected", <### [ ConsoleString $ "#secret_club: member " <> cathIncognito <> " is connected",
"contact bob_1 is merged into bob", "#secret_club: member bob_1 (Bob) is connected",
"use @bob <message> to send messages" "contact bob_1 is merged into bob",
], "use @bob <message> to send messages"
do ],
bob <## "#secret_club: alice added dan_1 (Daniel) to the group (connecting...)" do
bob <## "#secret_club: new member dan_1 is connected" bob <## "#secret_club: alice added dan_1 (Daniel) to the group (connecting...)"
bob <## "contact dan_1 is merged into dan" bob <## "#secret_club: new member dan_1 is connected"
bob <## "use @dan <message> to send messages", bob <## "contact dan_1 is merged into dan"
do bob <## "use @dan <message> to send messages",
cath <## "#secret_club: alice added dan_1 (Daniel) to the group (connecting...)" do
cath <## "#secret_club: new member dan_1 is connected" 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" -- send messages - group is incognito for cath
concurrentlyN_ alice #> "#secret_club hello"
[ bob <# "#secret_club alice> hello", concurrentlyN_
cath ?<# "#secret_club alice> hello", [ bob <# "#secret_club alice> hello",
dan <# "#secret_club alice> hello" cath ?<# "#secret_club alice> hello",
] dan <# "#secret_club alice> hello"
bob #> "#secret_club hi there" ]
concurrentlyN_ bob #> "#secret_club hi there"
[ alice <# "#secret_club bob> hi there", concurrentlyN_
cath ?<# "#secret_club bob_1> hi there", [ alice <# "#secret_club bob> hi there",
dan <# "#secret_club bob> hi there" cath ?<# "#secret_club bob_1> hi there",
] dan <# "#secret_club bob> hi there"
cath ?#> "#secret_club hey" ]
concurrentlyN_ cath ?#> "#secret_club hey"
[ alice <# ("#secret_club " <> cathIncognito <> "> hey"), concurrentlyN_
bob <# ("#secret_club " <> cathIncognito <> "> hey"), [ alice <# ("#secret_club " <> cathIncognito <> "> hey"),
dan <# ("#secret_club " <> cathIncognito <> "> hey") bob <# ("#secret_club " <> cathIncognito <> "> hey"),
] dan <# ("#secret_club " <> cathIncognito <> "> hey")
dan #> "#secret_club how is it going?" ]
concurrentlyN_ dan #> "#secret_club how is it going?"
[ alice <# "#secret_club dan> how is it going?", concurrentlyN_
bob <# "#secret_club dan> how is it going?", [ alice <# "#secret_club dan> how is it going?",
cath ?<# "#secret_club dan_1> 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 and bob can send messages via new direct connection, cath is incognito
cath ?<# "bob_1> hi, I'm bob" bob #> ("@" <> cathIncognito <> " hi, I'm bob")
cath ?#> "@bob_1 hey, I'm incognito" cath ?<# "bob_1> hi, I'm bob"
bob <# (cathIncognito <> "> hey, I'm incognito") cath ?#> "@bob_1 hey, I'm incognito"
-- cath and dan can send messages via new direct connection, cath is incognito bob <# (cathIncognito <> "> hey, I'm incognito")
dan #> ("@" <> cathIncognito <> " hi, I'm dan") -- cath and dan can send messages via new direct connection, cath is incognito
cath ?<# "dan_1> hi, I'm dan" dan #> ("@" <> cathIncognito <> " hi, I'm dan")
cath ?#> "@dan_1 hey, I'm incognito" cath ?<# "dan_1> hi, I'm dan"
dan <# (cathIncognito <> "> hey, I'm incognito") cath ?#> "@dan_1 hey, I'm incognito"
-- non incognito connections are separate dan <# (cathIncognito <> "> hey, I'm incognito")
bob <##> cath -- non incognito connections are separate
dan <##> cath bob <##> cath
-- list groups dan <##> cath
cath ##> "/gs" -- list groups
cath <## "i #secret_club (4 members)" cath ##> "/gs"
-- list group members cath <## "i #secret_club (4 members)"
alice ##> "/ms secret_club" -- list group members
alice alice ##> "/ms secret_club"
<### [ "alice (Alice): owner, you, created group", alice
"bob (Bob): admin, invited, connected", <### [ "alice (Alice): owner, you, created group",
ConsoleString $ cathIncognito <> ": admin, invited, connected", "bob (Bob): admin, invited, connected",
"dan (Daniel): admin, invited, connected" ConsoleString $ cathIncognito <> ": admin, invited, connected",
] "dan (Daniel): admin, invited, connected"
bob ##> "/ms secret_club" ]
bob bob ##> "/ms secret_club"
<### [ "alice (Alice): owner, host, connected", bob
"bob (Bob): admin, you, connected", <### [ "alice (Alice): owner, host, connected",
ConsoleString $ cathIncognito <> ": admin, connected", "bob (Bob): admin, you, connected",
"dan (Daniel): admin, connected" ConsoleString $ cathIncognito <> ": admin, connected",
] "dan (Daniel): admin, connected"
cath ##> "/ms secret_club" ]
cath cath ##> "/ms secret_club"
<### [ "alice (Alice): owner, host, connected", cath
"bob_1 (Bob): admin, connected", <### [ "alice (Alice): owner, host, connected",
ConsoleString $ "i " <> cathIncognito <> ": admin, you, connected", "bob_1 (Bob): admin, connected",
"dan_1 (Daniel): admin, connected" ConsoleString $ "i " <> cathIncognito <> ": admin, you, connected",
] "dan_1 (Daniel): admin, connected"
dan ##> "/ms secret_club" ]
dan dan ##> "/ms secret_club"
<### [ "alice (Alice): owner, host, connected", dan
"bob (Bob): admin, connected", <### [ "alice (Alice): owner, host, connected",
ConsoleString $ cathIncognito <> ": admin, connected", "bob (Bob): admin, connected",
"dan (Daniel): admin, you, connected" ConsoleString $ cathIncognito <> ": admin, connected",
] "dan (Daniel): admin, you, connected"
-- remove member ]
bob ##> ("/rm secret_club " <> cathIncognito) -- remove member
concurrentlyN_ bob ##> ("/rm secret_club " <> cathIncognito)
[ bob <## ("#secret_club: you removed " <> cathIncognito <> " from the group"), concurrentlyN_
alice <## ("#secret_club: bob removed " <> cathIncognito <> " from the group"), [ bob <## ("#secret_club: you removed " <> cathIncognito <> " from the group"),
dan <## ("#secret_club: bob removed " <> cathIncognito <> " from the group"), alice <## ("#secret_club: bob removed " <> cathIncognito <> " from the group"),
do dan <## ("#secret_club: bob removed " <> cathIncognito <> " from the group"),
cath <## "#secret_club: bob_1 removed you from the group" do
cath <## "use /d #secret_club to delete the group" cath <## "#secret_club: bob_1 removed you from the group"
] cath <## "use /d #secret_club to delete the group"
bob #> "#secret_club hi" ]
concurrentlyN_ bob #> "#secret_club hi"
[ alice <# "#secret_club bob> hi", concurrentlyN_
dan <# "#secret_club bob> hi", [ alice <# "#secret_club bob> hi",
(cath </) dan <# "#secret_club bob> hi",
] (cath </)
alice #> "#secret_club hello" ]
concurrentlyN_ alice #> "#secret_club hello"
[ bob <# "#secret_club alice> hello", concurrentlyN_
dan <# "#secret_club alice> hello", [ bob <# "#secret_club alice> hello",
(cath </) dan <# "#secret_club alice> hello",
] (cath </)
cath ##> "#secret_club hello" ]
cath <## "you are no longer a member of the group" cath ##> "#secret_club hello"
-- cath can still message members directly cath <## "you are no longer a member of the group"
bob #> ("@" <> cathIncognito <> " I removed you from group") -- cath can still message members directly
cath ?<# "bob_1> I removed you from group" bob #> ("@" <> cathIncognito <> " I removed you from group")
cath ?#> "@bob_1 ok" cath ?<# "bob_1> I removed you from group"
bob <# (cathIncognito <> "> ok") cath ?#> "@bob_1 ok"
bob <# (cathIncognito <> "> ok")
testCantInviteContactIncognito :: HasCallStack => FilePath -> IO () testCantInviteContactIncognito :: HasCallStack => FilePath -> IO ()
testCantInviteContactIncognito = testChat2 aliceProfile bobProfile $ testCantInviteContactIncognito = testChat2 aliceProfile bobProfile $
@ -1354,54 +1359,55 @@ testAllowFullDeletionGroup =
testProhibitDirectMessages :: HasCallStack => FilePath -> IO () testProhibitDirectMessages :: HasCallStack => FilePath -> IO ()
testProhibitDirectMessages = testProhibitDirectMessages =
testChat4 aliceProfile bobProfile cathProfile danProfile $ \alice bob cath dan -> do testChatCfg4 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile danProfile $
createGroup3 "team" alice bob cath \alice bob cath dan -> do
threadDelay 1000000 createGroup3 "team" alice bob cath
alice ##> "/set direct #team off" threadDelay 1000000
alice <## "updated group preferences:" alice ##> "/set direct #team off"
alice <## "Direct messages: off" alice <## "updated group preferences:"
directProhibited bob alice <## "Direct messages: off"
directProhibited cath directProhibited bob
threadDelay 1000000 directProhibited cath
-- still can send direct messages to direct contacts threadDelay 1000000
alice #> "@bob hello again" -- still can send direct messages to direct contacts
bob <# "alice> hello again" alice #> "@bob hello again"
alice #> "@cath hello again" bob <# "alice> hello again"
cath <# "alice> hello again" alice #> "@cath hello again"
bob ##> "@cath hello again" cath <# "alice> hello again"
bob <## "direct messages to indirect contact cath are prohibited" bob ##> "@cath hello again"
(cath </) bob <## "direct messages to indirect contact cath are prohibited"
connectUsers cath dan (cath </)
addMember "team" cath dan GRMember connectUsers cath dan
dan ##> "/j #team" addMember "team" cath dan GRMember
concurrentlyN_ dan ##> "/j #team"
[ cath <## "#team: dan joined the group", concurrentlyN_
do [ cath <## "#team: dan joined the group",
dan <## "#team: you joined the group" do
dan dan <## "#team: you joined the group"
<### [ "#team: member alice (Alice) is connected", dan
"#team: member bob (Bob) is connected" <### [ "#team: member alice (Alice) is connected",
], "#team: member bob (Bob) is connected"
do ],
alice <## "#team: cath added dan (Daniel) to the group (connecting...)" do
alice <## "#team: new member dan is connected", alice <## "#team: cath added dan (Daniel) to the group (connecting...)"
do alice <## "#team: new member dan is connected",
bob <## "#team: cath added dan (Daniel) to the group (connecting...)" do
bob <## "#team: new member dan is connected" 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" alice ##> "@dan hi"
bob ##> "@dan hi" alice <## "direct messages to indirect contact dan are prohibited"
bob <## "direct messages to indirect contact dan are prohibited" bob ##> "@dan hi"
(dan </) bob <## "direct messages to indirect contact dan are prohibited"
dan ##> "@alice hi" (dan </)
dan <## "direct messages to indirect contact alice are prohibited" dan ##> "@alice hi"
dan ##> "@bob hi" dan <## "direct messages to indirect contact alice are prohibited"
dan <## "direct messages to indirect contact bob are prohibited" dan ##> "@bob hi"
dan #> "@cath hi" dan <## "direct messages to indirect contact bob are prohibited"
cath <# "dan> hi" dan #> "@cath hi"
cath #> "@dan hi" cath <# "dan> hi"
dan <# "cath> hi" cath #> "@dan hi"
dan <# "cath> hi"
where where
directProhibited :: HasCallStack => TestCC -> IO () directProhibited :: HasCallStack => TestCC -> IO ()
directProhibited cc = do directProhibited cc = do

View File

@ -18,11 +18,13 @@ import Data.Maybe (fromMaybe)
import Data.String import Data.String
import qualified Data.Text as T import qualified Data.Text as T
import Simplex.Chat.Controller (ChatConfig (..), ChatController (..), InlineFilesConfig (..), defaultInlineFilesConfig) import Simplex.Chat.Controller (ChatConfig (..), ChatController (..), InlineFilesConfig (..), defaultInlineFilesConfig)
import Simplex.Chat.Protocol
import Simplex.Chat.Store.Profiles (getUserContactProfiles) import Simplex.Chat.Store.Profiles (getUserContactProfiles)
import Simplex.Chat.Types import Simplex.Chat.Types
import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.Preferences
import Simplex.Messaging.Agent.Store.SQLite (withTransaction) import Simplex.Messaging.Agent.Store.SQLite (withTransaction)
import Simplex.Messaging.Encoding.String import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Version
import System.Directory (doesFileExist) import System.Directory (doesFileExist)
import System.Environment (lookupEnv) import System.Environment (lookupEnv)
import System.FilePath ((</>)) import System.FilePath ((</>))
@ -65,9 +67,9 @@ versionTestMatrix2 runTest = do
it "v1 to v2" $ runTestCfg2 testCfg testCfgV1 runTest it "v1 to v2" $ runTestCfg2 testCfg testCfgV1 runTest
it "v2 to v1" $ runTestCfg2 testCfgV1 testCfg runTest it "v2 to v1" $ runTestCfg2 testCfgV1 testCfg runTest
versionTestMatrix3 :: (HasCallStack => TestCC -> TestCC -> TestCC -> IO ()) -> SpecWith FilePath -- versionTestMatrix3 :: (HasCallStack => TestCC -> TestCC -> TestCC -> IO ()) -> SpecWith FilePath
versionTestMatrix3 runTest = do -- versionTestMatrix3 runTest = do
it "v2" $ testChat3 aliceProfile bobProfile cathProfile runTest -- it "v2" $ testChat3 aliceProfile bobProfile cathProfile runTest
-- it "v1" $ testChatCfg3 testCfgV1 aliceProfile bobProfile cathProfile runTest -- it "v1" $ testChatCfg3 testCfgV1 aliceProfile bobProfile cathProfile runTest
-- it "v1 to v2" $ runTestCfg3 testCfg testCfgV1 testCfgV1 runTest -- it "v1 to v2" $ runTestCfg3 testCfg testCfgV1 testCfgV1 runTest
@ -356,7 +358,7 @@ dropTime_ msg = case splitAt 6 msg of
_ -> Nothing _ -> Nothing
dropStrPrefix :: HasCallStack => String -> String -> String dropStrPrefix :: HasCallStack => String -> String -> String
dropStrPrefix pfx s = dropStrPrefix pfx s =
let (p, rest) = splitAt (length pfx) s let (p, rest) = splitAt (length pfx) s
in if p == pfx then rest else error $ "no prefix " <> pfx <> " in string : " <> 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_ concurrently_
(cc2 <## ("started receiving file 1 (" <> fileName <> ") from " <> name1)) (cc2 <## ("started receiving file 1 (" <> fileName <> ") from " <> name1))
(cc1 <## ("started sending file 1 (" <> fileName <> ") to " <> name2)) (cc1 <## ("started sending file 1 (" <> fileName <> ") to " <> name2))
currentChatVRangeInfo :: String
currentChatVRangeInfo =
"peer chat protocol version range: " <> vRangeStr supportedChatVRange
vRangeStr :: VersionRange -> String
vRangeStr (VersionRange minVer maxVer) = "(" <> show minVer <> ", " <> show maxVer <> ")"

View File

@ -76,10 +76,10 @@ s ##==## msg = do
s ==## msg s ==## msg
(==#) :: MsgEncodingI e => ByteString -> ChatMsgEvent e -> Expectation (==#) :: 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 (#==) :: 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 (#==#) :: MsgEncodingI e => ByteString -> ChatMsgEvent e -> Expectation
s #==# msg = do s #==# msg = do
@ -101,59 +101,66 @@ testGroupProfile = GroupProfile {displayName = "team", fullName = "Team", descri
decodeChatMessageTest :: Spec decodeChatMessageTest :: Spec
decodeChatMessageTest = describe "Chat message encoding/decoding" $ do decodeChatMessageTest = describe "Chat message encoding/decoding" $ do
it "x.msg.new simple text" $ 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)) #==# XMsgNew (MCSimple (extMsgContent (MCText "hello") Nothing))
it "x.msg.new simple text - timed message TTL" $ 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)) #==# XMsgNew (MCSimple (ExtMsgContent (MCText "hello") Nothing (Just 3600) Nothing))
it "x.msg.new simple text - live message" $ 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))) #==# XMsgNew (MCSimple (ExtMsgContent (MCText "hello") Nothing Nothing (Just True)))
it "x.msg.new simple link" $ 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)) #==# 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" $ 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)) #==# XMsgNew (MCSimple (extMsgContent (MCImage "" $ ImageData "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=") Nothing))
it "x.msg.new simple image with text" $ 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)) #==# 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 " $ it "x.msg.new chat message" $
"{\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}" "{\"v\":\"1\",\"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))) ##==## 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" $ 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 ##==## ChatMessage
chatInitialVRange
(Just $ SharedMsgId "\1\2\3\4") (Just $ SharedMsgId "\1\2\3\4")
(XMsgNew (MCQuote quotedMsg (extMsgContent (MCText "hello to you too") Nothing))) (XMsgNew (MCQuote quotedMsg (extMsgContent (MCText "hello to you too") Nothing)))
it "x.msg.new quote - timed message TTL" $ 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 ##==## ChatMessage
chatInitialVRange
(Just $ SharedMsgId "\1\2\3\4") (Just $ SharedMsgId "\1\2\3\4")
(XMsgNew (MCQuote quotedMsg (ExtMsgContent (MCText "hello to you too") Nothing (Just 3600) Nothing))) (XMsgNew (MCQuote quotedMsg (ExtMsgContent (MCText "hello to you too") Nothing (Just 3600) Nothing)))
it "x.msg.new quote - live message" $ 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 ##==## ChatMessage
chatInitialVRange
(Just $ SharedMsgId "\1\2\3\4") (Just $ SharedMsgId "\1\2\3\4")
(XMsgNew (MCQuote quotedMsg (ExtMsgContent (MCText "hello to you too") Nothing Nothing (Just True)))) (XMsgNew (MCQuote quotedMsg (ExtMsgContent (MCText "hello to you too") Nothing Nothing (Just True))))
it "x.msg.new forward" $ it "x.msg.new forward" $
"{\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"forward\":true}}" "{\"v\":\"1\",\"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)) ##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew $ MCForward (extMsgContent (MCText "hello") Nothing))
it "x.msg.new forward - timed message TTL" $ it "x.msg.new forward - timed message TTL" $
"{\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"forward\":true,\"ttl\":3600}}" "{\"v\":\"1\",\"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)) ##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew $ MCForward (ExtMsgContent (MCText "hello") Nothing (Just 3600) Nothing))
it "x.msg.new forward - live message" $ it "x.msg.new forward - live message" $
"{\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"forward\":true,\"live\":true}}" "{\"v\":\"1\",\"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))) ##==## 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" $ 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}))) #==# 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" $ 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}))) #==# 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" $ 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 ##==## ChatMessage
chatInitialVRange
(Just $ SharedMsgId "\1\2\3\4") (Just $ SharedMsgId "\1\2\3\4")
( XMsgNew ( XMsgNew
( MCQuote ( MCQuote
@ -165,101 +172,113 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do
) )
) )
it "x.msg.new forward with file" $ 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\"}}}" "{\"v\":\"1\",\"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}))) ##==## 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" $ 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 #==# XMsgUpdate (SharedMsgId "\1\2\3\4") (MCText "hello") Nothing Nothing
it "x.msg.del" $ 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 #==# XMsgDel (SharedMsgId "\1\2\3\4") Nothing
it "x.msg.deleted" $ it "x.msg.deleted" $
"{\"event\":\"x.msg.deleted\",\"params\":{}}" "{\"v\":\"1\",\"event\":\"x.msg.deleted\",\"params\":{}}"
#==# XMsgDeleted #==# XMsgDeleted
it "x.file" $ 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} #==# XFile FileInvitation {fileName = "photo.jpg", fileSize = 12345, fileDigest = Nothing, fileConnReq = Just testConnReq, fileInline = Nothing, fileDescr = Nothing}
it "x.file without file invitation" $ 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} #==# XFile FileInvitation {fileName = "photo.jpg", fileSize = 12345, fileDigest = Nothing, fileConnReq = Nothing, fileInline = Nothing, fileDescr = Nothing}
it "x.file.acpt" $ 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" #==# XFileAcpt "photo.jpg"
it "x.file.acpt.inv" $ 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" #==# XFileAcptInv (SharedMsgId "\1\2\3\4") (Just testConnReq) "photo.jpg"
it "x.file.acpt.inv" $ 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" #==# XFileAcptInv (SharedMsgId "\1\2\3\4") Nothing "photo.jpg"
it "x.file.cancel" $ 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") #==# XFileCancel (SharedMsgId "\1\2\3\4")
it "x.info" $ 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 #==# XInfo testProfile
it "x.info with empty full name" $ 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} #==# XInfo Profile {displayName = "alice", fullName = "", image = Nothing, contactLink = Nothing, preferences = testChatPreferences}
it "x.contact with xContactId" $ 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") #==# XContact testProfile (Just $ XContactId "\1\2\3\4")
it "x.contact without XContactId" $ 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 #==# XContact testProfile Nothing
it "x.contact with content null" $ 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 ==# XContact testProfile Nothing
it "x.contact with content (ignored)" $ 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 ==# XContact testProfile Nothing
it "x.grp.inv" $ 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} #==# 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" $ 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"} #==# 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" $ 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") #==# XGrpAcpt (MemberId "\1\2\3\4")
it "x.grp.mem.new" $ 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} #==# 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" $ 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} #==# 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" $ 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} #==# 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" $ 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} #==# 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" $ 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 #==# XGrpMemInfo (MemberId "\1\2\3\4") testProfile
it "x.grp.mem.con" $ 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") #==# XGrpMemCon (MemberId "\1\2\3\4")
it "x.grp.mem.con.all" $ 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") #==# XGrpMemConAll (MemberId "\1\2\3\4")
it "x.grp.mem.del" $ 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") #==# XGrpMemDel (MemberId "\1\2\3\4")
it "x.grp.leave" $ it "x.grp.leave" $
"{\"event\":\"x.grp.leave\",\"params\":{}}" "{\"v\":\"1\",\"event\":\"x.grp.leave\",\"params\":{}}"
==# XGrpLeave ==# XGrpLeave
it "x.grp.del" $ it "x.grp.del" $
"{\"event\":\"x.grp.del\",\"params\":{}}" "{\"v\":\"1\",\"event\":\"x.grp.del\",\"params\":{}}"
==# XGrpDel ==# XGrpDel
it "x.info.probe" $ 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") #==# XInfoProbe (Probe "\1\2\3\4")
it "x.info.probe.check" $ 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") #==# XInfoProbeCheck (ProbeHash "\1\2\3\4")
it "x.info.probe.ok" $ 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") #==# XInfoProbeOk (Probe "\1\2\3\4")
it "x.ok" $ it "x.ok" $
"{\"event\":\"x.ok\",\"params\":{}}" "{\"v\":\"1\",\"event\":\"x.ok\",\"params\":{}}"
==# XOk ==# XOk