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:
commit
5e8e4c295c
@ -9,7 +9,7 @@ constraints: zip +disable-bzip2 +disable-zstd
|
||||
source-repository-package
|
||||
type: git
|
||||
location: https://github.com/simplex-chat/simplexmq.git
|
||||
tag: 980e5c4d1ec15f44290542fd2a5d1c08456f00d1
|
||||
tag: 351f42650c57f310fc1ea858ff9b7178823f1fd4
|
||||
|
||||
source-repository-package
|
||||
type: git
|
||||
|
@ -3,24 +3,31 @@ sequenceDiagram
|
||||
participant A as Alice
|
||||
participant B as Bob
|
||||
participant C as Existing<br>contact
|
||||
|
||||
|
||||
note over A, B: 1. send and accept group invitation
|
||||
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: establish group member connection
|
||||
B ->> A: x.grp.acpt<br>accept invitation<br>(via member connection)<br>establish group member connection
|
||||
|
||||
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 ->> B: x.grp.mem.intro * N<br>"introduce" members<br>(via member connection)
|
||||
B ->> A: x.grp.mem.inv * N<br>"invitations" to connect<br>for all members<br>(via member connection)
|
||||
A ->> M: x.grp.mem.fwd<br>forward "invitations"<br>to all members<br>(via member connections)
|
||||
loop batched
|
||||
A ->> B: x.grp.mem.intro * N<br>"introduce" members and<br>their chat protocol versions<br>(via member connection)
|
||||
note over B: prepare group member connections
|
||||
opt chat protocol compatible version < 2
|
||||
note over B: prepare direct connections
|
||||
end
|
||||
B ->> A: x.grp.mem.inv * N<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
|
||||
M ->> B: establish group member connection
|
||||
M ->> B: establish direct connection
|
||||
|
||||
note over M, C: 4. deduplicate new contact
|
||||
B ->> M: x.info.probe<br>"probe" is sent to all new members
|
||||
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
|
||||
opt chat protocol compatible version < 2
|
||||
M ->> B: establish direct connection
|
||||
note over M, C: 4. deduplicate new contact
|
||||
B ->> M: x.info.probe<br>"probe" is sent to all new members
|
||||
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 |
@ -1,5 +1,5 @@
|
||||
{
|
||||
"https://github.com/simplex-chat/simplexmq.git"."980e5c4d1ec15f44290542fd2a5d1c08456f00d1" = "1lqciyy215dvmbhykyp80bwipqmxybv39p6jff6vjgd5r34958nh";
|
||||
"https://github.com/simplex-chat/simplexmq.git"."351f42650c57f310fc1ea858ff9b7178823f1fd4" = "12r13yc0qk9dkii58808862wraqrk66rzmkrgyp6lg1xrazrd0d2";
|
||||
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
|
||||
"https://github.com/kazu-yamamoto/http2.git"."b5a1b7200cf5bc7044af34ba325284271f6dff25" = "0dqb50j57an64nf4qcf5vcz4xkd1vzvghvf8bk529c1k30r9nfzb";
|
||||
"https://github.com/simplex-chat/direct-sqlcipher.git"."34309410eb2069b029b8fc1872deb1e0db123294" = "0kwkmhyfsn2lixdlgl15smgr1h5gjk7fky6abzh8rng2h5ymnffd";
|
||||
|
@ -109,6 +109,7 @@ library
|
||||
Simplex.Chat.Migrations.M20230721_group_snd_item_statuses
|
||||
Simplex.Chat.Migrations.M20230814_indexes
|
||||
Simplex.Chat.Migrations.M20230827_file_encryption
|
||||
Simplex.Chat.Migrations.M20230829_connections_chat_vrange
|
||||
Simplex.Chat.Mobile
|
||||
Simplex.Chat.Mobile.File
|
||||
Simplex.Chat.Mobile.Shared
|
||||
|
@ -94,6 +94,7 @@ import qualified Simplex.Messaging.Protocol as SMP
|
||||
import qualified Simplex.Messaging.TMap as TM
|
||||
import Simplex.Messaging.Transport.Client (defaultSocksProxy)
|
||||
import Simplex.Messaging.Util
|
||||
import Simplex.Messaging.Version
|
||||
import System.Exit (exitFailure, exitSuccess)
|
||||
import System.FilePath (combine, splitExtensions, takeFileName, (</>))
|
||||
import System.IO (Handle, IOMode (..), SeekMode (..), hFlush, stdout)
|
||||
@ -113,6 +114,7 @@ defaultChatConfig =
|
||||
{ tcpPort = undefined, -- agent does not listen to TCP
|
||||
tbqSize = 1024
|
||||
},
|
||||
chatVRange = supportedChatVRange,
|
||||
confirmMigrations = MCConsole,
|
||||
defaultServers =
|
||||
DefaultAgentServers
|
||||
@ -1296,7 +1298,8 @@ processChatCommand = \case
|
||||
-- [incognito] generate profile to send
|
||||
incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing
|
||||
let profileToSend = userProfileToSend user incognitoProfile Nothing
|
||||
connId <- withAgent $ \a -> joinConnection a (aUserId user) True cReq . directMessage $ XInfo profileToSend
|
||||
dm <- directMessage $ XInfo profileToSend
|
||||
connId <- withAgent $ \a -> joinConnection a (aUserId user) True cReq dm
|
||||
conn <- withStore' $ \db -> createDirectConnection db user connId cReq ConnJoined $ incognitoProfile $> profileToSend
|
||||
toView $ CRNewContactConnection user conn
|
||||
pure $ CRSentConfirmation user
|
||||
@ -1434,11 +1437,16 @@ processChatCommand = \case
|
||||
Nothing -> throwChatError $ CEGroupCantResendInvitation gInfo cName
|
||||
| otherwise -> throwChatError $ CEGroupDuplicateMember cName
|
||||
APIJoinGroup groupId -> withUser $ \user@User {userId} -> do
|
||||
ReceivedGroupInvitation {fromMember, connRequest, groupInfo = g@GroupInfo {membership}} <- withStore $ \db -> getGroupInvitation db user groupId
|
||||
(invitation, ct) <- withStore $ \db -> do
|
||||
inv@ReceivedGroupInvitation {fromMember} <- getGroupInvitation db user groupId
|
||||
(inv,) <$> getContactViaMember db user fromMember
|
||||
let ReceivedGroupInvitation {fromMember, connRequest, groupInfo = g@GroupInfo {membership}} = invitation
|
||||
Contact {activeConn = Connection {peerChatVRange}} = ct
|
||||
withChatLock "joinGroup" . procCmd $ do
|
||||
agentConnId <- withAgent $ \a -> joinConnection a (aUserId user) True connRequest . directMessage $ XGrpAcpt (memberId (membership :: GroupMember))
|
||||
dm <- directMessage $ XGrpAcpt (memberId (membership :: GroupMember))
|
||||
agentConnId <- withAgent $ \a -> joinConnection a (aUserId user) True connRequest dm
|
||||
withStore' $ \db -> do
|
||||
createMemberConnection db userId fromMember agentConnId
|
||||
createMemberConnection db userId fromMember agentConnId peerChatVRange
|
||||
updateGroupMemberStatus db userId fromMember GSMemAccepted
|
||||
updateGroupMemberStatus db userId membership GSMemAccepted
|
||||
updateCIGroupInvitationStatus user
|
||||
@ -1840,7 +1848,8 @@ processChatCommand = \case
|
||||
-- [incognito] generate profile to send
|
||||
incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing
|
||||
let profileToSend = userProfileToSend user incognitoProfile Nothing
|
||||
connId <- withAgent $ \a -> joinConnection a (aUserId user) True cReq $ directMessage (XContact profileToSend $ Just xContactId)
|
||||
dm <- directMessage (XContact profileToSend $ Just xContactId)
|
||||
connId <- withAgent $ \a -> joinConnection a (aUserId user) True cReq dm
|
||||
let groupLinkId = crClientData >>= decodeJSON >>= \(CRDataGroup gli) -> Just gli
|
||||
conn <- withStore' $ \db -> createConnReqConnection db userId connId cReqHash xContactId incognitoProfile groupLinkId
|
||||
toView $ CRNewContactConnection user conn
|
||||
@ -1857,7 +1866,7 @@ processChatCommand = \case
|
||||
xftpCfg <- readTVarIO =<< asks userXFTPFileConfig
|
||||
fileSize <- liftIO $ CF.getFileContentsSize $ CryptoFile fsFilePath cfArgs
|
||||
when (fromInteger fileSize > maxFileSize) $ throwChatError $ CEFileSize f
|
||||
let chunks = -((-fileSize) `div` fileChunkSize)
|
||||
let chunks = - ((- fileSize) `div` fileChunkSize)
|
||||
fileInline = inlineFileMode mc inlineFiles chunks n
|
||||
fileMode = case xftpCfg of
|
||||
Just cfg
|
||||
@ -2231,7 +2240,7 @@ acceptFileReceive user@User {userId} RcvFileTransfer {fileId, xftpRcvFile, fileI
|
||||
case (xftpRcvFile, fileConnReq) of
|
||||
-- direct file protocol
|
||||
(Nothing, Just connReq) -> do
|
||||
connIds <- joinAgentConnectionAsync user True connReq . directMessage $ XFileAcpt fName
|
||||
connIds <- joinAgentConnectionAsync user True connReq =<< directMessage (XFileAcpt fName)
|
||||
filePath <- getRcvFilePath fileId filePath_ fName True
|
||||
withStoreCtx (Just "acceptFileReceive, acceptRcvFileTransfer") $ \db -> acceptRcvFileTransfer db user fileId connIds ConnJoined filePath
|
||||
-- XFTP
|
||||
@ -2346,17 +2355,18 @@ getRcvFilePath fileId fPath_ fn keepHandle = case fPath_ of
|
||||
in ifM (doesFileExist f) (tryCombine $ n + 1) (pure f)
|
||||
|
||||
acceptContactRequest :: ChatMonad m => User -> UserContactRequest -> Maybe IncognitoProfile -> m Contact
|
||||
acceptContactRequest user UserContactRequest {agentInvitationId = AgentInvId invId, localDisplayName = cName, profileId, profile = cp, userContactLinkId, xContactId} incognitoProfile = do
|
||||
acceptContactRequest user UserContactRequest {agentInvitationId = AgentInvId invId, cReqChatVRange, localDisplayName = cName, profileId, profile = cp, userContactLinkId, xContactId} incognitoProfile = do
|
||||
let profileToSend = profileToSendOnAccept user incognitoProfile
|
||||
acId <- withAgent $ \a -> acceptContact a True invId . directMessage $ XInfo profileToSend
|
||||
withStore' $ \db -> createAcceptedContact db user acId cName profileId cp userContactLinkId xContactId incognitoProfile
|
||||
dm <- directMessage $ XInfo profileToSend
|
||||
acId <- withAgent $ \a -> acceptContact a True invId dm
|
||||
withStore' $ \db -> createAcceptedContact db user acId cReqChatVRange cName profileId cp userContactLinkId xContactId incognitoProfile
|
||||
|
||||
acceptContactRequestAsync :: ChatMonad m => User -> UserContactRequest -> Maybe IncognitoProfile -> m Contact
|
||||
acceptContactRequestAsync user UserContactRequest {agentInvitationId = AgentInvId invId, localDisplayName = cName, profileId, profile = p, userContactLinkId, xContactId} incognitoProfile = do
|
||||
acceptContactRequestAsync user UserContactRequest {agentInvitationId = AgentInvId invId, cReqChatVRange, localDisplayName = cName, profileId, profile = p, userContactLinkId, xContactId} incognitoProfile = do
|
||||
let profileToSend = profileToSendOnAccept user incognitoProfile
|
||||
(cmdId, acId) <- agentAcceptContactAsync user True invId $ XInfo profileToSend
|
||||
withStore' $ \db -> do
|
||||
ct@Contact {activeConn = Connection {connId}} <- createAcceptedContact db user acId cName profileId p userContactLinkId xContactId incognitoProfile
|
||||
ct@Contact {activeConn = Connection {connId}} <- createAcceptedContact db user acId cReqChatVRange cName profileId p userContactLinkId xContactId incognitoProfile
|
||||
setCommandConnId db user cmdId connId
|
||||
pure ct
|
||||
|
||||
@ -2556,7 +2566,7 @@ cleanupManager = do
|
||||
`catchChatError` (toView . CRChatError (Just user))
|
||||
cleanupMessages = do
|
||||
ts <- liftIO getCurrentTime
|
||||
let cutoffTs = addUTCTime (-(30 * nominalDay)) ts
|
||||
let cutoffTs = addUTCTime (- (30 * nominalDay)) ts
|
||||
withStoreCtx' (Just "cleanupManager, deleteOldMessages") (`deleteOldMessages` cutoffTs)
|
||||
|
||||
startProximateTimedItemThread :: ChatMonad m => User -> (ChatRef, ChatItemId) -> UTCTime -> m ()
|
||||
@ -2840,21 +2850,23 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
|
||||
_ -> Nothing
|
||||
|
||||
processDirectMessage :: ACommand 'Agent e -> ConnectionEntity -> Connection -> Maybe Contact -> m ()
|
||||
processDirectMessage agentMsg connEntity conn@Connection {connId, viaUserContactLink, groupLinkId, customUserProfileId, connectionCode} = \case
|
||||
processDirectMessage agentMsg connEntity conn@Connection {connId, peerChatVRange, viaUserContactLink, groupLinkId, customUserProfileId, connectionCode} = \case
|
||||
Nothing -> case agentMsg of
|
||||
CONF confId _ connInfo -> do
|
||||
-- [incognito] send saved profile
|
||||
incognitoProfile <- forM customUserProfileId $ \profileId -> withStore (\db -> getProfileById db userId profileId)
|
||||
let profileToSend = userProfileToSend user (fromLocalProfile <$> incognitoProfile) Nothing
|
||||
saveConnInfo conn connInfo
|
||||
conn' <- saveConnInfo conn connInfo
|
||||
-- [async agent commands] no continuation needed, but command should be asynchronous for stability
|
||||
allowAgentConnectionAsync user conn confId $ XInfo profileToSend
|
||||
INFO connInfo ->
|
||||
saveConnInfo conn connInfo
|
||||
allowAgentConnectionAsync user conn' confId $ XInfo profileToSend
|
||||
INFO connInfo -> do
|
||||
_conn' <- saveConnInfo conn connInfo
|
||||
pure ()
|
||||
MSG meta _msgFlags msgBody -> do
|
||||
cmdId <- createAckCmd conn
|
||||
withAckMessage agentConnId cmdId meta $
|
||||
saveRcvMSG conn (ConnectionId connId) meta msgBody cmdId $> False
|
||||
withAckMessage agentConnId cmdId meta $ do
|
||||
(_conn', _) <- saveRcvMSG conn (ConnectionId connId) meta msgBody cmdId
|
||||
pure False
|
||||
SENT msgId ->
|
||||
sentMsgDeliveryEvent conn msgId
|
||||
OK ->
|
||||
@ -2879,54 +2891,57 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
|
||||
setConnConnReqInv db user connId cReq
|
||||
getXGrpMemIntroContDirect db user ct
|
||||
forM_ contData $ \(hostConnId, xGrpMemIntroCont) ->
|
||||
sendXGrpMemInv hostConnId directConnReq xGrpMemIntroCont
|
||||
sendXGrpMemInv hostConnId (Just directConnReq) xGrpMemIntroCont
|
||||
CRContactUri _ -> throwChatError $ CECommandError "unexpected ConnectionRequestUri type"
|
||||
MSG msgMeta _msgFlags msgBody -> do
|
||||
cmdId <- createAckCmd conn
|
||||
withAckMessage agentConnId cmdId msgMeta $ do
|
||||
msg@RcvMessage {chatMsgEvent = ACME _ event} <- saveRcvMSG conn (ConnectionId connId) msgMeta msgBody cmdId
|
||||
assertDirectAllowed user MDRcv ct $ toCMEventTag event
|
||||
(conn', msg@RcvMessage {chatMsgEvent = ACME _ event}) <- saveRcvMSG conn (ConnectionId connId) msgMeta msgBody cmdId
|
||||
let ct' = ct {activeConn = conn'} :: Contact
|
||||
assertDirectAllowed user MDRcv ct' $ toCMEventTag event
|
||||
updateChatLock "directMessage" event
|
||||
case event of
|
||||
XMsgNew mc -> newContentMessage ct mc msg msgMeta
|
||||
XMsgFileDescr sharedMsgId fileDescr -> messageFileDescription ct sharedMsgId fileDescr msgMeta
|
||||
XMsgFileCancel sharedMsgId -> cancelMessageFile ct sharedMsgId msgMeta
|
||||
XMsgUpdate sharedMsgId mContent ttl live -> messageUpdate ct sharedMsgId mContent msg msgMeta ttl live
|
||||
XMsgDel sharedMsgId _ -> messageDelete ct sharedMsgId msg msgMeta
|
||||
XMsgReact sharedMsgId _ reaction add -> directMsgReaction ct sharedMsgId reaction add msg msgMeta
|
||||
XMsgNew mc -> newContentMessage ct' mc msg msgMeta
|
||||
XMsgFileDescr sharedMsgId fileDescr -> messageFileDescription ct' sharedMsgId fileDescr msgMeta
|
||||
XMsgFileCancel sharedMsgId -> cancelMessageFile ct' sharedMsgId msgMeta
|
||||
XMsgUpdate sharedMsgId mContent ttl live -> messageUpdate ct' sharedMsgId mContent msg msgMeta ttl live
|
||||
XMsgDel sharedMsgId _ -> messageDelete ct' sharedMsgId msg msgMeta
|
||||
XMsgReact sharedMsgId _ reaction add -> directMsgReaction ct' sharedMsgId reaction add msg msgMeta
|
||||
-- TODO discontinue XFile
|
||||
XFile fInv -> processFileInvitation' ct fInv msg msgMeta
|
||||
XFileCancel sharedMsgId -> xFileCancel ct sharedMsgId msgMeta
|
||||
XFileAcptInv sharedMsgId fileConnReq_ fName -> xFileAcptInv ct sharedMsgId fileConnReq_ fName msgMeta
|
||||
XInfo p -> xInfo ct p
|
||||
XGrpInv gInv -> processGroupInvitation ct gInv msg msgMeta
|
||||
XInfoProbe probe -> xInfoProbe ct probe
|
||||
XInfoProbeCheck probeHash -> xInfoProbeCheck ct probeHash
|
||||
XInfoProbeOk probe -> xInfoProbeOk ct probe
|
||||
XCallInv callId invitation -> xCallInv ct callId invitation msg msgMeta
|
||||
XCallOffer callId offer -> xCallOffer ct callId offer msg msgMeta
|
||||
XCallAnswer callId answer -> xCallAnswer ct callId answer msg msgMeta
|
||||
XCallExtra callId extraInfo -> xCallExtra ct callId extraInfo msg msgMeta
|
||||
XCallEnd callId -> xCallEnd ct callId msg msgMeta
|
||||
BFileChunk sharedMsgId chunk -> bFileChunk ct sharedMsgId chunk msgMeta
|
||||
XFile fInv -> processFileInvitation' ct' fInv msg msgMeta
|
||||
XFileCancel sharedMsgId -> xFileCancel ct' sharedMsgId msgMeta
|
||||
XFileAcptInv sharedMsgId fileConnReq_ fName -> xFileAcptInv ct' sharedMsgId fileConnReq_ fName msgMeta
|
||||
XInfo p -> xInfo ct' p
|
||||
XGrpInv gInv -> processGroupInvitation ct' gInv msg msgMeta
|
||||
XInfoProbe probe -> xInfoProbe ct' probe
|
||||
XInfoProbeCheck probeHash -> xInfoProbeCheck ct' probeHash
|
||||
XInfoProbeOk probe -> xInfoProbeOk ct' probe
|
||||
XCallInv callId invitation -> xCallInv ct' callId invitation msg msgMeta
|
||||
XCallOffer callId offer -> xCallOffer ct' callId offer msg msgMeta
|
||||
XCallAnswer callId answer -> xCallAnswer ct' callId answer msg msgMeta
|
||||
XCallExtra callId extraInfo -> xCallExtra ct' callId extraInfo msg msgMeta
|
||||
XCallEnd callId -> xCallEnd ct' callId msg msgMeta
|
||||
BFileChunk sharedMsgId chunk -> bFileChunk ct' sharedMsgId chunk msgMeta
|
||||
_ -> messageError $ "unsupported message: " <> T.pack (show event)
|
||||
let Contact {chatSettings = ChatSettings {sendRcpts}} = ct
|
||||
let Contact {chatSettings = ChatSettings {sendRcpts}} = ct'
|
||||
pure $ fromMaybe (sendRcptsContacts user) sendRcpts && hasDeliveryReceipt (toCMEventTag event)
|
||||
RCVD msgMeta msgRcpt ->
|
||||
withAckMessage' agentConnId conn msgMeta $
|
||||
directMsgReceived ct conn msgMeta msgRcpt
|
||||
CONF confId _ connInfo -> do
|
||||
-- confirming direct connection with a member
|
||||
ChatMessage {chatMsgEvent} <- parseChatMessage conn connInfo
|
||||
ChatMessage {chatVRange, chatMsgEvent} <- parseChatMessage conn connInfo
|
||||
conn' <- updatePeerChatVRange conn chatVRange
|
||||
case chatMsgEvent of
|
||||
XGrpMemInfo _memId _memProfile -> do
|
||||
-- TODO check member ID
|
||||
-- TODO update member profile
|
||||
-- [async agent commands] no continuation needed, but command should be asynchronous for stability
|
||||
allowAgentConnectionAsync user conn confId XOk
|
||||
allowAgentConnectionAsync user conn' confId XOk
|
||||
_ -> messageError "CONF from member must have x.grp.mem.info"
|
||||
INFO connInfo -> do
|
||||
ChatMessage {chatMsgEvent} <- parseChatMessage conn connInfo
|
||||
ChatMessage {chatVRange, chatMsgEvent} <- parseChatMessage conn connInfo
|
||||
_conn' <- updatePeerChatVRange conn chatVRange
|
||||
case chatMsgEvent of
|
||||
XGrpMemInfo _memId _memProfile -> do
|
||||
-- TODO check member ID
|
||||
@ -2958,7 +2973,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
|
||||
forM_ groupId_ $ \groupId -> do
|
||||
gVar <- asks idsDrg
|
||||
groupConnIds <- createAgentConnectionAsync user CFCreateConnGrpInv True SCMInvitation
|
||||
withStore $ \db -> createNewContactMemberAsync db gVar user groupId ct gLinkMemRole groupConnIds
|
||||
withStore $ \db -> createNewContactMemberAsync db gVar user groupId ct gLinkMemRole groupConnIds peerChatVRange
|
||||
_ -> pure ()
|
||||
Just (gInfo@GroupInfo {membership}, m@GroupMember {activeConn}) ->
|
||||
when (maybe False ((== ConnReady) . connStatus) activeConn) $ do
|
||||
@ -3025,22 +3040,30 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
|
||||
case cReq of
|
||||
groupConnReq@(CRInvitationUri _ _) -> case cmdFunction of
|
||||
-- [async agent commands] XGrpMemIntro continuation on receiving INV
|
||||
CFCreateConnGrpMemInv -> do
|
||||
contData <- withStore' $ \db -> do
|
||||
setConnConnReqInv db user connId cReq
|
||||
getXGrpMemIntroContGroup db user m
|
||||
forM_ contData $ \(hostConnId, directConnReq) -> do
|
||||
let GroupMember {groupMemberId, memberId} = m
|
||||
sendXGrpMemInv hostConnId directConnReq XGrpMemIntroCont {groupId, groupMemberId, memberId, groupConnReq}
|
||||
CFCreateConnGrpMemInv
|
||||
| isCompatibleRange (peerChatVRange conn) groupNoDirectVRange -> sendWithDirectCReq -- sendWithoutDirectCReq
|
||||
| otherwise -> sendWithDirectCReq
|
||||
where
|
||||
sendWithoutDirectCReq = do
|
||||
let GroupMember {groupMemberId, memberId} = m
|
||||
hostConnId <- withStore $ \db -> do
|
||||
liftIO $ setConnConnReqInv db user connId cReq
|
||||
getHostConnId db user groupId
|
||||
sendXGrpMemInv hostConnId Nothing XGrpMemIntroCont {groupId, groupMemberId, memberId, groupConnReq}
|
||||
sendWithDirectCReq = do
|
||||
let GroupMember {groupMemberId, memberId} = m
|
||||
contData <- withStore' $ \db -> do
|
||||
setConnConnReqInv db user connId cReq
|
||||
getXGrpMemIntroContGroup db user m
|
||||
forM_ contData $ \(hostConnId, directConnReq) ->
|
||||
sendXGrpMemInv hostConnId (Just directConnReq) XGrpMemIntroCont {groupId, groupMemberId, memberId, groupConnReq}
|
||||
-- [async agent commands] group link auto-accept continuation on receiving INV
|
||||
CFCreateConnGrpInv ->
|
||||
withStore' (\db -> getContactViaMember db user m) >>= \case
|
||||
Nothing -> messageError "implementation error: invitee does not have contact"
|
||||
Just ct -> do
|
||||
withStore' $ \db -> setNewContactMemberConnRequest db user m cReq
|
||||
groupLinkId <- withStore' $ \db -> getGroupLinkId db user gInfo
|
||||
sendGrpInvitation ct m groupLinkId
|
||||
toView $ CRSentGroupInvitation user gInfo ct m
|
||||
CFCreateConnGrpInv -> do
|
||||
ct <- withStore $ \db -> getContactViaMember db user m
|
||||
withStore' $ \db -> setNewContactMemberConnRequest db user m cReq
|
||||
groupLinkId <- withStore' $ \db -> getGroupLinkId db user gInfo
|
||||
sendGrpInvitation ct m groupLinkId
|
||||
toView $ CRSentGroupInvitation user gInfo ct m
|
||||
where
|
||||
sendGrpInvitation :: Contact -> GroupMember -> Maybe GroupLinkId -> m ()
|
||||
sendGrpInvitation ct GroupMember {memberId, memberRole = memRole} groupLinkId = do
|
||||
@ -3052,7 +3075,8 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
|
||||
_ -> throwChatError $ CECommandError "unexpected cmdFunction"
|
||||
CRContactUri _ -> throwChatError $ CECommandError "unexpected ConnectionRequestUri type"
|
||||
CONF confId _ connInfo -> do
|
||||
ChatMessage {chatMsgEvent} <- parseChatMessage conn connInfo
|
||||
ChatMessage {chatVRange, chatMsgEvent} <- parseChatMessage conn connInfo
|
||||
conn' <- updatePeerChatVRange conn chatVRange
|
||||
case memberCategory m of
|
||||
GCInviteeMember ->
|
||||
case chatMsgEvent of
|
||||
@ -3060,7 +3084,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
|
||||
| sameMemberId memId m -> do
|
||||
withStore $ \db -> liftIO $ updateGroupMemberStatus db userId m GSMemAccepted
|
||||
-- [async agent commands] no continuation needed, but command should be asynchronous for stability
|
||||
allowAgentConnectionAsync user conn confId XOk
|
||||
allowAgentConnectionAsync user conn' confId XOk
|
||||
| otherwise -> messageError "x.grp.acpt: memberId is different from expected"
|
||||
_ -> messageError "CONF from invited member must have x.grp.acpt"
|
||||
_ ->
|
||||
@ -3069,11 +3093,12 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
|
||||
| sameMemberId memId m -> do
|
||||
-- TODO update member profile
|
||||
-- [async agent commands] no continuation needed, but command should be asynchronous for stability
|
||||
allowAgentConnectionAsync user conn confId $ XGrpMemInfo (memberId (membership :: GroupMember)) (fromLocalProfile $ memberProfile membership)
|
||||
allowAgentConnectionAsync user conn' confId $ XGrpMemInfo (memberId (membership :: GroupMember)) (fromLocalProfile $ memberProfile membership)
|
||||
| otherwise -> messageError "x.grp.mem.info: memberId is different from expected"
|
||||
_ -> messageError "CONF from member must have x.grp.mem.info"
|
||||
INFO connInfo -> do
|
||||
ChatMessage {chatMsgEvent} <- parseChatMessage conn connInfo
|
||||
ChatMessage {chatVRange, chatMsgEvent} <- parseChatMessage conn connInfo
|
||||
_conn' <- updatePeerChatVRange conn chatVRange
|
||||
case chatMsgEvent of
|
||||
XGrpMemInfo memId _memProfile
|
||||
| sameMemberId memId m -> do
|
||||
@ -3114,7 +3139,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
|
||||
processIntro intro `catchChatError` (toView . CRChatError (Just user))
|
||||
where
|
||||
processIntro intro@GroupMemberIntro {introId} = do
|
||||
void $ sendDirectMessage conn (XGrpMemIntro . memberInfo $ reMember intro) (GroupId groupId)
|
||||
void $ sendDirectMessage conn (XGrpMemIntro $ memberInfo (reMember intro)) (GroupId groupId)
|
||||
withStore' $ \db -> updateIntroStatus db introId GMIntroSent
|
||||
_ -> do
|
||||
-- TODO send probe and decide whether to use existing contact connection or the new contact connection
|
||||
@ -3131,28 +3156,29 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
|
||||
MSG msgMeta _msgFlags msgBody -> do
|
||||
cmdId <- createAckCmd conn
|
||||
withAckMessage agentConnId cmdId msgMeta $ do
|
||||
msg@RcvMessage {chatMsgEvent = ACME _ event} <- saveRcvMSG conn (GroupId groupId) msgMeta msgBody cmdId
|
||||
(conn', msg@RcvMessage {chatMsgEvent = ACME _ event}) <- saveRcvMSG conn (GroupId groupId) msgMeta msgBody cmdId
|
||||
let m' = m {activeConn = Just conn'} :: GroupMember
|
||||
updateChatLock "groupMessage" event
|
||||
case event of
|
||||
XMsgNew mc -> canSend $ newGroupContentMessage gInfo m mc msg msgMeta
|
||||
XMsgFileDescr sharedMsgId fileDescr -> canSend $ groupMessageFileDescription gInfo m sharedMsgId fileDescr msgMeta
|
||||
XMsgFileCancel sharedMsgId -> cancelGroupMessageFile gInfo m sharedMsgId msgMeta
|
||||
XMsgUpdate sharedMsgId mContent ttl live -> canSend $ groupMessageUpdate gInfo m sharedMsgId mContent msg msgMeta ttl live
|
||||
XMsgDel sharedMsgId memberId -> groupMessageDelete gInfo m sharedMsgId memberId msg msgMeta
|
||||
XMsgReact sharedMsgId (Just memberId) reaction add -> groupMsgReaction gInfo m sharedMsgId memberId reaction add msg msgMeta
|
||||
XMsgNew mc -> canSend m' $ newGroupContentMessage gInfo m' mc msg msgMeta
|
||||
XMsgFileDescr sharedMsgId fileDescr -> canSend m' $ groupMessageFileDescription gInfo m' sharedMsgId fileDescr msgMeta
|
||||
XMsgFileCancel sharedMsgId -> cancelGroupMessageFile gInfo m' sharedMsgId msgMeta
|
||||
XMsgUpdate sharedMsgId mContent ttl live -> canSend m' $ groupMessageUpdate gInfo m' sharedMsgId mContent msg msgMeta ttl live
|
||||
XMsgDel sharedMsgId memberId -> groupMessageDelete gInfo m' sharedMsgId memberId msg msgMeta
|
||||
XMsgReact sharedMsgId (Just memberId) reaction add -> groupMsgReaction gInfo m' sharedMsgId memberId reaction add msg msgMeta
|
||||
-- TODO discontinue XFile
|
||||
XFile fInv -> processGroupFileInvitation' gInfo m fInv msg msgMeta
|
||||
XFileCancel sharedMsgId -> xFileCancelGroup gInfo m sharedMsgId msgMeta
|
||||
XFileAcptInv sharedMsgId fileConnReq_ fName -> xFileAcptInvGroup gInfo m sharedMsgId fileConnReq_ fName msgMeta
|
||||
XGrpMemNew memInfo -> xGrpMemNew gInfo m memInfo msg msgMeta
|
||||
XGrpMemIntro memInfo -> xGrpMemIntro gInfo m memInfo
|
||||
XGrpMemInv memId introInv -> xGrpMemInv gInfo m memId introInv
|
||||
XGrpMemFwd memInfo introInv -> xGrpMemFwd gInfo m memInfo introInv
|
||||
XGrpMemRole memId memRole -> xGrpMemRole gInfo m memId memRole msg msgMeta
|
||||
XGrpMemDel memId -> xGrpMemDel gInfo m memId msg msgMeta
|
||||
XGrpLeave -> xGrpLeave gInfo m msg msgMeta
|
||||
XGrpDel -> xGrpDel gInfo m msg msgMeta
|
||||
XGrpInfo p' -> xGrpInfo gInfo m p' msg msgMeta
|
||||
XFile fInv -> processGroupFileInvitation' gInfo m' fInv msg msgMeta
|
||||
XFileCancel sharedMsgId -> xFileCancelGroup gInfo m' sharedMsgId msgMeta
|
||||
XFileAcptInv sharedMsgId fileConnReq_ fName -> xFileAcptInvGroup gInfo m' sharedMsgId fileConnReq_ fName msgMeta
|
||||
XGrpMemNew memInfo -> xGrpMemNew gInfo m' memInfo msg msgMeta
|
||||
XGrpMemIntro memInfo -> xGrpMemIntro gInfo m' memInfo
|
||||
XGrpMemInv memId introInv -> xGrpMemInv gInfo m' memId introInv
|
||||
XGrpMemFwd memInfo introInv -> xGrpMemFwd gInfo m' memInfo introInv
|
||||
XGrpMemRole memId memRole -> xGrpMemRole gInfo m' memId memRole msg msgMeta
|
||||
XGrpMemDel memId -> xGrpMemDel gInfo m' memId msg msgMeta
|
||||
XGrpLeave -> xGrpLeave gInfo m' msg msgMeta
|
||||
XGrpDel -> xGrpDel gInfo m' msg msgMeta
|
||||
XGrpInfo p' -> xGrpInfo gInfo m' p' msg msgMeta
|
||||
BFileChunk sharedMsgId chunk -> bFileChunkGroup gInfo sharedMsgId chunk msgMeta
|
||||
_ -> messageError $ "unsupported message: " <> T.pack (show event)
|
||||
currentMemCount <- withStore' $ \db -> getGroupCurrentMembersCount db user gInfo
|
||||
@ -3162,8 +3188,8 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
|
||||
&& hasDeliveryReceipt (toCMEventTag event)
|
||||
&& currentMemCount <= smallGroupsRcptsMemLimit
|
||||
where
|
||||
canSend a
|
||||
| memberRole (m :: GroupMember) <= GRObserver = messageError "member is not allowed to send messages"
|
||||
canSend mem a
|
||||
| memberRole (mem :: GroupMember) <= GRObserver = messageError "member is not allowed to send messages"
|
||||
| otherwise = a
|
||||
RCVD msgMeta msgRcpt ->
|
||||
withAckMessage' agentConnId conn msgMeta $
|
||||
@ -3248,14 +3274,15 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
|
||||
-- SMP CONF for SndFileConnection happens for direct file protocol
|
||||
-- when recipient of the file "joins" connection created by the sender
|
||||
CONF confId _ connInfo -> do
|
||||
ChatMessage {chatMsgEvent} <- parseChatMessage conn connInfo
|
||||
ChatMessage {chatVRange, chatMsgEvent} <- parseChatMessage conn connInfo
|
||||
conn' <- updatePeerChatVRange conn chatVRange
|
||||
case chatMsgEvent of
|
||||
-- TODO save XFileAcpt message
|
||||
XFileAcpt name
|
||||
| name == fileName -> do
|
||||
withStore' $ \db -> updateSndFileStatus db ft FSAccepted
|
||||
-- [async agent commands] no continuation needed, but command should be asynchronous for stability
|
||||
allowAgentConnectionAsync user conn confId XOk
|
||||
allowAgentConnectionAsync user conn' confId XOk
|
||||
| otherwise -> messageError "x.file.acpt: fileName is different from expected"
|
||||
_ -> messageError "CONF from file connection must have x.file.acpt"
|
||||
CON -> do
|
||||
@ -3316,9 +3343,10 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
|
||||
-- when sender of the file "joins" connection created by the recipient
|
||||
-- (sender doesn't create connections for all group members)
|
||||
CONF confId _ connInfo -> do
|
||||
ChatMessage {chatMsgEvent} <- parseChatMessage conn connInfo
|
||||
ChatMessage {chatVRange, chatMsgEvent} <- parseChatMessage conn connInfo
|
||||
conn' <- updatePeerChatVRange conn chatVRange
|
||||
case chatMsgEvent of
|
||||
XOk -> allowAgentConnectionAsync user conn confId XOk -- [async agent commands] no continuation needed, but command should be asynchronous for stability
|
||||
XOk -> allowAgentConnectionAsync user conn' confId XOk -- [async agent commands] no continuation needed, but command should be asynchronous for stability
|
||||
_ -> pure ()
|
||||
CON -> startReceivingFile user fileId
|
||||
MSG meta _ msgBody -> do
|
||||
@ -3377,10 +3405,10 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
|
||||
processUserContactRequest :: ACommand 'Agent e -> ConnectionEntity -> Connection -> UserContact -> m ()
|
||||
processUserContactRequest agentMsg connEntity conn UserContact {userContactLinkId} = case agentMsg of
|
||||
REQ invId _ connInfo -> do
|
||||
ChatMessage {chatMsgEvent} <- parseChatMessage conn connInfo
|
||||
ChatMessage {chatVRange, chatMsgEvent} <- parseChatMessage conn connInfo
|
||||
case chatMsgEvent of
|
||||
XContact p xContactId_ -> profileContactRequest invId p xContactId_
|
||||
XInfo p -> profileContactRequest invId p Nothing
|
||||
XContact p xContactId_ -> profileContactRequest invId chatVRange p xContactId_
|
||||
XInfo p -> profileContactRequest invId chatVRange p Nothing
|
||||
-- TODO show/log error, other events in contact request
|
||||
_ -> pure ()
|
||||
MERR _ err -> do
|
||||
@ -3392,9 +3420,9 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
|
||||
-- TODO add debugging output
|
||||
_ -> pure ()
|
||||
where
|
||||
profileContactRequest :: InvitationId -> Profile -> Maybe XContactId -> m ()
|
||||
profileContactRequest invId p xContactId_ = do
|
||||
withStore (\db -> createOrUpdateContactRequest db user userContactLinkId invId p xContactId_) >>= \case
|
||||
profileContactRequest :: InvitationId -> VersionRange -> Profile -> Maybe XContactId -> m ()
|
||||
profileContactRequest invId chatVRange p xContactId_ = do
|
||||
withStore (\db -> createOrUpdateContactRequest db user userContactLinkId invId chatVRange p xContactId_) >>= \case
|
||||
CORContact contact -> toView $ CRContactRequestAlreadyAccepted user contact
|
||||
CORRequest cReq@UserContactRequest {localDisplayName} -> do
|
||||
withStore' (\db -> getUserContactLinkById db userId userContactLinkId) >>= \case
|
||||
@ -3892,7 +3920,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
|
||||
then unless cancelled $ case fileConnReq_ of
|
||||
-- receiving via a separate connection
|
||||
Just fileConnReq -> do
|
||||
connIds <- joinAgentConnectionAsync user True fileConnReq $ directMessage XOk
|
||||
connIds <- joinAgentConnectionAsync user True fileConnReq =<< directMessage XOk
|
||||
withStore' $ \db -> createSndDirectFTConnection db user fileId connIds
|
||||
-- receiving inline
|
||||
_ -> do
|
||||
@ -3989,7 +4017,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
|
||||
(Just fileConnReq, _) -> do
|
||||
-- receiving via a separate connection
|
||||
-- [async agent commands] no continuation needed, but command should be asynchronous for stability
|
||||
connIds <- joinAgentConnectionAsync user True fileConnReq $ directMessage XOk
|
||||
connIds <- joinAgentConnectionAsync user True fileConnReq =<< directMessage XOk
|
||||
withStore' $ \db -> createSndGroupFileTransferConnection db user fileId connIds m
|
||||
(_, Just conn) -> do
|
||||
-- receiving inline
|
||||
@ -4012,7 +4040,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
|
||||
|
||||
processGroupInvitation :: Contact -> GroupInvitation -> RcvMessage -> MsgMeta -> m ()
|
||||
processGroupInvitation ct inv msg msgMeta = do
|
||||
let Contact {localDisplayName = c, activeConn = Connection {customUserProfileId, groupLinkId = groupLinkId'}} = ct
|
||||
let Contact {localDisplayName = c, activeConn = Connection {peerChatVRange, customUserProfileId, groupLinkId = groupLinkId'}} = ct
|
||||
GroupInvitation {fromMember = (MemberIdRole fromMemId fromRole), invitedMember = (MemberIdRole memId memRole), connRequest, groupLinkId} = inv
|
||||
checkIntegrityCreateItem (CDDirectRcv ct) msgMeta
|
||||
when (fromRole < GRAdmin || fromRole < memRole) $ throwChatError (CEGroupContactRole c)
|
||||
@ -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
|
||||
if sameGroupLinkId groupLinkId groupLinkId'
|
||||
then do
|
||||
connIds <- joinAgentConnectionAsync user True connRequest . directMessage $ XGrpAcpt memberId
|
||||
connIds <- joinAgentConnectionAsync user True connRequest =<< directMessage (XGrpAcpt memberId)
|
||||
withStore' $ \db -> do
|
||||
createMemberConnectionAsync db user hostId connIds
|
||||
createMemberConnectionAsync db user hostId connIds peerChatVRange
|
||||
updateGroupMemberStatusById db userId hostId GSMemAccepted
|
||||
updateGroupMemberStatus db userId membership GSMemAccepted
|
||||
toView $ CRUserAcceptedGroupSent user gInfo {membership = membership {memberStatus = GSMemAccepted}} (Just ct)
|
||||
@ -4223,18 +4251,20 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
|
||||
withStore' $ \db -> mergeContactRecords db userId c1 c2
|
||||
toView $ CRContactsMerged user c1 c2
|
||||
|
||||
saveConnInfo :: Connection -> ConnInfo -> m ()
|
||||
saveConnInfo :: Connection -> ConnInfo -> m Connection
|
||||
saveConnInfo activeConn connInfo = do
|
||||
ChatMessage {chatMsgEvent} <- parseChatMessage activeConn connInfo
|
||||
ChatMessage {chatVRange, chatMsgEvent} <- parseChatMessage activeConn connInfo
|
||||
conn' <- updatePeerChatVRange activeConn chatVRange
|
||||
case chatMsgEvent of
|
||||
XInfo p -> do
|
||||
ct <- withStore $ \db -> createDirectContact db user activeConn p
|
||||
ct <- withStore $ \db -> createDirectContact db user conn' p
|
||||
toView $ CRContactConnecting user ct
|
||||
pure conn'
|
||||
-- TODO show/log error, other events in SMP confirmation
|
||||
_ -> pure ()
|
||||
_ -> pure conn'
|
||||
|
||||
xGrpMemNew :: GroupInfo -> GroupMember -> MemberInfo -> RcvMessage -> MsgMeta -> m ()
|
||||
xGrpMemNew gInfo m memInfo@(MemberInfo memId memRole memberProfile) msg msgMeta = do
|
||||
xGrpMemNew gInfo m memInfo@(MemberInfo memId memRole _ memberProfile) msg msgMeta = do
|
||||
checkHostRole m memRole
|
||||
members <- withStore' $ \db -> getGroupMembers db user gInfo
|
||||
unless (sameMemberId memId $ membership gInfo) $
|
||||
@ -4247,7 +4277,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
|
||||
toView $ CRJoinedGroupMemberConnecting user gInfo m newMember
|
||||
|
||||
xGrpMemIntro :: GroupInfo -> GroupMember -> MemberInfo -> m ()
|
||||
xGrpMemIntro gInfo@GroupInfo {membership, chatSettings = ChatSettings {enableNtfs}} m@GroupMember {memberRole, localDisplayName = c} memInfo@(MemberInfo memId _ _) = do
|
||||
xGrpMemIntro gInfo@GroupInfo {membership, chatSettings = ChatSettings {enableNtfs}} m@GroupMember {memberRole, localDisplayName = c} memInfo@(MemberInfo memId _ memberChatVRange _) = do
|
||||
case memberCategory m of
|
||||
GCHostMember -> do
|
||||
members <- withStore' $ \db -> getGroupMembers db user gInfo
|
||||
@ -4256,14 +4286,19 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
|
||||
else do
|
||||
when (memberRole < GRAdmin) $ throwChatError (CEGroupContactRole c)
|
||||
-- [async agent commands] commands should be asynchronous, continuation is to send XGrpMemInv - have to remember one has completed and process on second
|
||||
groupConnIds <- createAgentConnectionAsync user CFCreateConnGrpMemInv enableNtfs SCMInvitation
|
||||
directConnIds <- createAgentConnectionAsync user CFCreateConnGrpMemInv enableNtfs SCMInvitation
|
||||
-- [incognito] direct connection with member has to be established using the same incognito profile [that was known to host and used for group membership]
|
||||
groupConnIds <- createConn
|
||||
directConnIds <- case memberChatVRange of
|
||||
Nothing -> Just <$> createConn
|
||||
Just mcvr
|
||||
| isCompatibleRange (fromChatVRange mcvr) groupNoDirectVRange -> Just <$> createConn -- pure Nothing
|
||||
| otherwise -> Just <$> createConn
|
||||
let customUserProfileId = if memberIncognito membership then Just (localProfileId $ memberProfile membership) else Nothing
|
||||
void $ withStore $ \db -> createIntroReMember db user gInfo m memInfo groupConnIds directConnIds customUserProfileId
|
||||
_ -> messageError "x.grp.mem.intro can be only sent by host member"
|
||||
where
|
||||
createConn = createAgentConnectionAsync user CFCreateConnGrpMemInv enableNtfs SCMInvitation
|
||||
|
||||
sendXGrpMemInv :: Int64 -> ConnReqInvitation -> XGrpMemIntroCont -> m ()
|
||||
sendXGrpMemInv :: Int64 -> Maybe ConnReqInvitation -> XGrpMemIntroCont -> m ()
|
||||
sendXGrpMemInv hostConnId directConnReq XGrpMemIntroCont {groupId, groupMemberId, memberId, groupConnReq} = do
|
||||
hostConn <- withStore $ \db -> getConnectionById db user hostConnId
|
||||
let msg = XGrpMemInv memberId IntroInvitation {groupConnReq, directConnReq}
|
||||
@ -4284,7 +4319,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
|
||||
_ -> messageError "x.grp.mem.inv can be only sent by invitee member"
|
||||
|
||||
xGrpMemFwd :: GroupInfo -> GroupMember -> MemberInfo -> IntroInvitation -> m ()
|
||||
xGrpMemFwd gInfo@GroupInfo {membership, chatSettings = ChatSettings {enableNtfs}} m memInfo@(MemberInfo memId memRole _) introInv@IntroInvitation {groupConnReq, directConnReq} = do
|
||||
xGrpMemFwd gInfo@GroupInfo {membership, chatSettings = ChatSettings {enableNtfs}} m memInfo@(MemberInfo memId memRole memberChatVRange _) introInv@IntroInvitation {groupConnReq, directConnReq} = do
|
||||
checkHostRole m memRole
|
||||
members <- withStore' $ \db -> getGroupMembers db user gInfo
|
||||
toMember <- case find (sameMemberId memId) members of
|
||||
@ -4296,12 +4331,13 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
|
||||
Just m' -> pure m'
|
||||
withStore' $ \db -> saveMemberInvitation db toMember introInv
|
||||
-- [incognito] send membership incognito profile, create direct connection as incognito
|
||||
let msg = XGrpMemInfo (memberId (membership :: GroupMember)) (fromLocalProfile $ memberProfile membership)
|
||||
dm <- directMessage $ XGrpMemInfo (memberId (membership :: GroupMember)) (fromLocalProfile $ memberProfile membership)
|
||||
-- [async agent commands] no continuation needed, but commands should be asynchronous for stability
|
||||
groupConnIds <- joinAgentConnectionAsync user enableNtfs groupConnReq $ directMessage msg
|
||||
directConnIds <- joinAgentConnectionAsync user enableNtfs directConnReq $ directMessage msg
|
||||
groupConnIds <- joinAgentConnectionAsync user enableNtfs groupConnReq dm
|
||||
directConnIds <- forM directConnReq $ \dcr -> joinAgentConnectionAsync user enableNtfs dcr dm
|
||||
let customUserProfileId = if memberIncognito membership then Just (localProfileId $ memberProfile membership) else Nothing
|
||||
withStore' $ \db -> createIntroToMemberContact db user m toMember groupConnIds directConnIds customUserProfileId
|
||||
mcvr = maybe chatInitialVRange fromChatVRange memberChatVRange
|
||||
withStore' $ \db -> createIntroToMemberContact db user m toMember mcvr groupConnIds directConnIds customUserProfileId
|
||||
|
||||
xGrpMemRole :: GroupInfo -> GroupMember -> MemberId -> GroupMemberRole -> RcvMessage -> MsgMeta -> m ()
|
||||
xGrpMemRole gInfo@GroupInfo {membership} m@GroupMember {memberRole = senderRole} memId memRole msg msgMeta
|
||||
@ -4441,6 +4477,13 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
|
||||
toView $ CRChatItemStatusUpdated user (AChatItem SCTGroup SMDSnd (GroupChat gInfo) chatItem)
|
||||
_ -> 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 =
|
||||
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 chatMsgEvent connOrGroupId = do
|
||||
gVar <- asks idsDrg
|
||||
ChatConfig {chatVRange} <- asks config
|
||||
withStore $ \db -> createNewSndMessage db gVar connOrGroupId $ \sharedMsgId ->
|
||||
let msgBody = strEncode ChatMessage {msgId = Just sharedMsgId, chatMsgEvent}
|
||||
let msgBody = strEncode ChatMessage {chatVRange, msgId = Just sharedMsgId, chatMsgEvent}
|
||||
in NewMessage {chatMsgEvent, msgBody}
|
||||
|
||||
directMessage :: MsgEncodingI e => ChatMsgEvent e -> ByteString
|
||||
directMessage chatMsgEvent = strEncode ChatMessage {msgId = Nothing, chatMsgEvent}
|
||||
directMessage :: (MsgEncodingI e, ChatMonad m) => ChatMsgEvent e -> m ByteString
|
||||
directMessage chatMsgEvent = do
|
||||
ChatConfig {chatVRange} <- asks config
|
||||
pure $ strEncode ChatMessage {chatVRange, msgId = Nothing, chatMsgEvent}
|
||||
|
||||
deliverMessage :: ChatMonad m => Connection -> CMEventTag e -> MsgBody -> MessageId -> m Int64
|
||||
deliverMessage conn@Connection {connId} cmEventTag msgBody msgId = do
|
||||
@ -4699,15 +4745,17 @@ sendPendingGroupMessages user GroupMember {groupMemberId, localDisplayName} conn
|
||||
_ -> throwChatError $ CEGroupMemberIntroNotFound localDisplayName
|
||||
_ -> pure ()
|
||||
|
||||
saveRcvMSG :: ChatMonad m => Connection -> ConnOrGroupId -> MsgMeta -> MsgBody -> CommandId -> m RcvMessage
|
||||
saveRcvMSG :: ChatMonad m => Connection -> ConnOrGroupId -> MsgMeta -> MsgBody -> CommandId -> m (Connection, RcvMessage)
|
||||
saveRcvMSG conn@Connection {connId} connOrGroupId agentMsgMeta msgBody agentAckCmdId = do
|
||||
ACMsg _ ChatMessage {msgId = sharedMsgId_, chatMsgEvent} <- parseAChatMessage conn agentMsgMeta msgBody
|
||||
ACMsg _ ChatMessage {chatVRange, msgId = sharedMsgId_, chatMsgEvent} <- parseAChatMessage conn agentMsgMeta msgBody
|
||||
conn' <- updatePeerChatVRange conn chatVRange
|
||||
let agentMsgId = fst $ recipient agentMsgMeta
|
||||
newMsg = NewMessage {chatMsgEvent, msgBody}
|
||||
rcvMsgDelivery = RcvMsgDelivery {connId, agentMsgId, agentMsgMeta, agentAckCmdId}
|
||||
withStoreCtx'
|
||||
msg <- withStoreCtx'
|
||||
(Just $ "createNewMessageAndRcvMsgDelivery, rcvMsgDelivery: " <> show rcvMsgDelivery <> ", sharedMsgId_: " <> show sharedMsgId_ <> ", msgDeliveryStatus: MDSRcvAgent")
|
||||
$ \db -> createNewMessageAndRcvMsgDelivery db connOrGroupId newMsg sharedMsgId_ rcvMsgDelivery
|
||||
pure (conn', msg)
|
||||
|
||||
saveSndChatItem :: ChatMonad m => User -> ChatDirection c 'MDSnd -> SndMessage -> CIContent 'MDSnd -> m (ChatItem c 'MDSnd)
|
||||
saveSndChatItem user cd msg content = saveSndChatItem' user cd msg content Nothing Nothing Nothing False
|
||||
@ -4805,13 +4853,15 @@ joinAgentConnectionAsync user enableNtfs cReqUri cInfo = do
|
||||
allowAgentConnectionAsync :: (MsgEncodingI e, ChatMonad m) => User -> Connection -> ConfirmationId -> ChatMsgEvent e -> m ()
|
||||
allowAgentConnectionAsync user conn@Connection {connId} confId msg = do
|
||||
cmdId <- withStore' $ \db -> createCommand db user (Just connId) CFAllowConn
|
||||
withAgent $ \a -> allowConnectionAsync a (aCorrId cmdId) (aConnId conn) confId $ directMessage msg
|
||||
dm <- directMessage msg
|
||||
withAgent $ \a -> allowConnectionAsync a (aCorrId cmdId) (aConnId conn) confId dm
|
||||
withStore' $ \db -> updateConnectionStatus db conn ConnAccepted
|
||||
|
||||
agentAcceptContactAsync :: (MsgEncodingI e, ChatMonad m) => User -> Bool -> InvitationId -> ChatMsgEvent e -> m (CommandId, ConnId)
|
||||
agentAcceptContactAsync user enableNtfs invId msg = do
|
||||
cmdId <- withStore' $ \db -> createCommand db user Nothing CFAcceptContact
|
||||
connId <- withAgent $ \a -> acceptContactAsync a (aCorrId cmdId) enableNtfs invId $ directMessage msg
|
||||
dm <- directMessage msg
|
||||
connId <- withAgent $ \a -> acceptContactAsync a (aCorrId cmdId) enableNtfs invId dm
|
||||
pure (cmdId, connId)
|
||||
|
||||
deleteAgentConnectionAsync :: ChatMonad m => User -> ConnId -> m ()
|
||||
|
@ -67,6 +67,7 @@ import Simplex.Messaging.TMap (TMap)
|
||||
import Simplex.Messaging.Transport (simplexMQVersion)
|
||||
import Simplex.Messaging.Transport.Client (TransportHost)
|
||||
import Simplex.Messaging.Util (allFinally, catchAllErrors, tryAllErrors, (<$$>))
|
||||
import Simplex.Messaging.Version
|
||||
import System.IO (Handle)
|
||||
import System.Mem.Weak (Weak)
|
||||
import UnliftIO.STM
|
||||
@ -75,7 +76,7 @@ versionNumber :: String
|
||||
versionNumber = showVersion SC.version
|
||||
|
||||
versionString :: String -> String
|
||||
versionString version = "SimpleX Chat v" <> version
|
||||
versionString ver = "SimpleX Chat v" <> ver
|
||||
|
||||
updateStr :: String
|
||||
updateStr = "To update run: curl -o- https://raw.githubusercontent.com/simplex-chat/simplex-chat/master/install.sh | bash"
|
||||
@ -104,6 +105,7 @@ coreVersionInfo simplexmqCommit =
|
||||
|
||||
data ChatConfig = ChatConfig
|
||||
{ agentConfig :: AgentConfig,
|
||||
chatVRange :: VersionRange,
|
||||
confirmMigrations :: MigrationConfirmation,
|
||||
defaultServers :: DefaultAgentServers,
|
||||
tbqSize :: Natural,
|
||||
|
@ -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;
|
||||
|]
|
@ -285,6 +285,8 @@ CREATE TABLE connections(
|
||||
security_code TEXT NULL,
|
||||
security_code_verified_at TEXT NULL,
|
||||
auth_err_counter INTEGER DEFAULT 0 CHECK(auth_err_counter NOT NULL),
|
||||
peer_chat_min_version INTEGER NOT NULL DEFAULT 1,
|
||||
peer_chat_max_version INTEGER NOT NULL DEFAULT 1,
|
||||
FOREIGN KEY(snd_file_id, connection_id)
|
||||
REFERENCES snd_files(file_id, connection_id)
|
||||
ON DELETE CASCADE
|
||||
@ -318,6 +320,8 @@ CREATE TABLE contact_requests(
|
||||
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE,
|
||||
updated_at TEXT CHECK(updated_at NOT NULL),
|
||||
xcontact_id BLOB,
|
||||
peer_chat_min_version INTEGER NOT NULL DEFAULT 1,
|
||||
peer_chat_max_version INTEGER NOT NULL DEFAULT 1,
|
||||
FOREIGN KEY(user_id, local_display_name)
|
||||
REFERENCES display_names(user_id, local_display_name)
|
||||
ON UPDATE CASCADE
|
||||
|
@ -46,6 +46,17 @@ import Simplex.Messaging.Encoding
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Parsers (dropPrefix, fromTextField_, fstToLower, parseAll, sumTypeJSON, taggedObjectJSON)
|
||||
import Simplex.Messaging.Util (eitherToMaybe, safeDecodeUtf8, (<$?>))
|
||||
import Simplex.Messaging.Version hiding (version)
|
||||
|
||||
currentChatVersion :: Version
|
||||
currentChatVersion = 2
|
||||
|
||||
supportedChatVRange :: VersionRange
|
||||
supportedChatVRange = mkVersionRange 1 currentChatVersion
|
||||
|
||||
-- version range that supports skipping establishing direct connections in a group
|
||||
groupNoDirectVRange :: VersionRange
|
||||
groupNoDirectVRange = mkVersionRange 2 currentChatVersion
|
||||
|
||||
data ConnectionEntity
|
||||
= RcvDirectMsgConnection {entityConnection :: Connection, contact :: Maybe Contact}
|
||||
@ -102,7 +113,8 @@ data AppMessage (e :: MsgEncoding) where
|
||||
|
||||
-- chat message is sent as JSON with these properties
|
||||
data AppMessageJson = AppMessageJson
|
||||
{ msgId :: Maybe SharedMsgId,
|
||||
{ v :: Maybe ChatVersionRange,
|
||||
msgId :: Maybe SharedMsgId,
|
||||
event :: Text,
|
||||
params :: J.Object
|
||||
}
|
||||
@ -161,7 +173,11 @@ instance ToJSON MsgRef where
|
||||
toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True}
|
||||
toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True}
|
||||
|
||||
data ChatMessage e = ChatMessage {msgId :: Maybe SharedMsgId, chatMsgEvent :: ChatMsgEvent e}
|
||||
data ChatMessage e = ChatMessage
|
||||
{ chatVRange :: VersionRange,
|
||||
msgId :: Maybe SharedMsgId,
|
||||
chatMsgEvent :: ChatMsgEvent e
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
data AChatMessage = forall e. MsgEncodingI e => ACMsg (SMsgEncoding e) (ChatMessage e)
|
||||
@ -724,17 +740,17 @@ appBinaryToCM :: AppMessageBinary -> Either String (ChatMessage 'Binary)
|
||||
appBinaryToCM AppMessageBinary {msgId, tag, body} = do
|
||||
eventTag <- strDecode $ B.singleton tag
|
||||
chatMsgEvent <- parseAll (msg eventTag) body
|
||||
pure ChatMessage {msgId, chatMsgEvent}
|
||||
pure ChatMessage {chatVRange = chatInitialVRange, msgId, chatMsgEvent}
|
||||
where
|
||||
msg :: CMEventTag 'Binary -> A.Parser (ChatMsgEvent 'Binary)
|
||||
msg = \case
|
||||
BFileChunk_ -> BFileChunk <$> (SharedMsgId <$> smpP) <*> (unIFC <$> smpP)
|
||||
|
||||
appJsonToCM :: AppMessageJson -> Either String (ChatMessage 'Json)
|
||||
appJsonToCM AppMessageJson {msgId, event, params} = do
|
||||
appJsonToCM AppMessageJson {v, msgId, event, params} = do
|
||||
eventTag <- strDecode $ encodeUtf8 event
|
||||
chatMsgEvent <- msg eventTag
|
||||
pure ChatMessage {msgId, chatMsgEvent}
|
||||
pure ChatMessage {chatVRange = maybe chatInitialVRange fromChatVRange v, msgId, chatMsgEvent}
|
||||
where
|
||||
p :: FromJSON a => J.Key -> Either String a
|
||||
p key = JT.parseEither (.: key) params
|
||||
@ -784,11 +800,11 @@ appJsonToCM AppMessageJson {msgId, event, params} = do
|
||||
key .=? value = maybe id ((:) . (key .=)) value
|
||||
|
||||
chatToAppMessage :: forall e. MsgEncodingI e => ChatMessage e -> AppMessage e
|
||||
chatToAppMessage ChatMessage {msgId, chatMsgEvent} = case encoding @e of
|
||||
chatToAppMessage ChatMessage {chatVRange, msgId, chatMsgEvent} = case encoding @e of
|
||||
SBinary ->
|
||||
let (binaryMsgId, body) = toBody chatMsgEvent
|
||||
in AMBinary AppMessageBinary {msgId = binaryMsgId, tag = B.head $ strEncode tag, body}
|
||||
SJson -> AMJson AppMessageJson {msgId, event = textEncode tag, params = params chatMsgEvent}
|
||||
SJson -> AMJson AppMessageJson {v = Just $ ChatVersionRange chatVRange, msgId, event = textEncode tag, params = params chatMsgEvent}
|
||||
where
|
||||
tag = toCMEventTag chatMsgEvent
|
||||
o :: [(J.Key, J.Value)] -> J.Object
|
||||
@ -804,7 +820,7 @@ chatToAppMessage ChatMessage {msgId, chatMsgEvent} = case encoding @e of
|
||||
XMsgUpdate msgId' content ttl live -> o $ ("ttl" .=? ttl) $ ("live" .=? live) ["msgId" .= msgId', "content" .= content]
|
||||
XMsgDel msgId' memberId -> o $ ("memberId" .=? memberId) ["msgId" .= msgId']
|
||||
XMsgDeleted -> JM.empty
|
||||
XMsgReact msgId' memberId reaction add -> o $ ("memberId" .=? memberId) ["msgId" .= msgId', "reaction" .= reaction, "add" .= add]
|
||||
XMsgReact msgId' memberId reaction add -> o $ ("memberId" .=? memberId) ["msgId" .= msgId', "reaction" .= reaction, "add" .= add]
|
||||
XFile fileInv -> o ["file" .= fileInv]
|
||||
XFileAcpt fileName -> o ["fileName" .= fileName]
|
||||
XFileAcptInv sharedMsgId fileConnReq fileName -> o $ ("fileConnReq" .=? fileConnReq) ["msgId" .= sharedMsgId, "fileName" .= fileName]
|
||||
|
@ -49,7 +49,8 @@ getConnectionEntity db user@User {userId, userContactId} agentConnId = do
|
||||
db
|
||||
[sql|
|
||||
SELECT connection_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, via_group_link, group_link_id, custom_user_profile_id,
|
||||
conn_status, conn_type, local_alias, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at, security_code, security_code_verified_at, auth_err_counter
|
||||
conn_status, conn_type, local_alias, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at, security_code, security_code_verified_at, auth_err_counter,
|
||||
peer_chat_min_version, peer_chat_max_version
|
||||
FROM connections
|
||||
WHERE user_id = ? AND agent_conn_id = ?
|
||||
|]
|
||||
|
@ -75,6 +75,7 @@ import Simplex.Chat.Types.Preferences
|
||||
import Simplex.Messaging.Agent.Protocol (ConnId, InvitationId, UserId)
|
||||
import Simplex.Messaging.Agent.Store.SQLite (firstRow, maybeFirstRow)
|
||||
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
|
||||
import Simplex.Messaging.Version
|
||||
|
||||
getPendingContactConnection :: DB.Connection -> UserId -> Int64 -> ExceptT StoreError IO PendingContactConnection
|
||||
getPendingContactConnection db userId connId = do
|
||||
@ -143,7 +144,8 @@ getConnReqContactXContactId db user@User {userId} cReqHash = do
|
||||
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts,
|
||||
-- Connection
|
||||
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.local_alias,
|
||||
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter
|
||||
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter,
|
||||
c.peer_chat_min_version, c.peer_chat_max_version
|
||||
FROM contacts ct
|
||||
JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id
|
||||
JOIN connections c ON c.contact_id = ct.contact_id
|
||||
@ -411,8 +413,8 @@ getUserContacts db user@User {userId} = do
|
||||
contactIds <- map fromOnly <$> DB.query db "SELECT contact_id FROM contacts WHERE user_id = ? AND deleted = 0" (Only userId)
|
||||
rights <$> mapM (runExceptT . getContact db user) contactIds
|
||||
|
||||
createOrUpdateContactRequest :: DB.Connection -> User -> Int64 -> InvitationId -> Profile -> Maybe XContactId -> ExceptT StoreError IO ContactOrRequest
|
||||
createOrUpdateContactRequest db user@User {userId} userContactLinkId invId Profile {displayName, fullName, image, contactLink, preferences} xContactId_ =
|
||||
createOrUpdateContactRequest :: DB.Connection -> User -> Int64 -> InvitationId -> VersionRange -> Profile -> Maybe XContactId -> ExceptT StoreError IO ContactOrRequest
|
||||
createOrUpdateContactRequest db user@User {userId} userContactLinkId invId (VersionRange minV maxV) Profile {displayName, fullName, image, contactLink, preferences} xContactId_ =
|
||||
liftIO (maybeM getContact' xContactId_) >>= \case
|
||||
Just contact -> pure $ CORContact contact
|
||||
Nothing -> CORRequest <$> createOrUpdate_
|
||||
@ -441,10 +443,10 @@ createOrUpdateContactRequest db user@User {userId} userContactLinkId invId Profi
|
||||
db
|
||||
[sql|
|
||||
INSERT INTO contact_requests
|
||||
(user_contact_link_id, agent_invitation_id, contact_profile_id, local_display_name, user_id, created_at, updated_at, xcontact_id)
|
||||
VALUES (?,?,?,?,?,?,?,?)
|
||||
(user_contact_link_id, agent_invitation_id, peer_chat_min_version, peer_chat_max_version, contact_profile_id, local_display_name, user_id, created_at, updated_at, xcontact_id)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?)
|
||||
|]
|
||||
(userContactLinkId, invId, profileId, ldn, userId, currentTs, currentTs, xContactId_)
|
||||
(userContactLinkId, invId, minV, maxV, profileId, ldn, userId, currentTs, currentTs, xContactId_)
|
||||
insertedRowId db
|
||||
getContact' :: XContactId -> IO (Maybe Contact)
|
||||
getContact' xContactId =
|
||||
@ -458,7 +460,8 @@ createOrUpdateContactRequest db user@User {userId} userContactLinkId invId Profi
|
||||
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts,
|
||||
-- Connection
|
||||
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.local_alias,
|
||||
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter
|
||||
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter,
|
||||
c.peer_chat_min_version, c.peer_chat_max_version
|
||||
FROM contacts ct
|
||||
JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id
|
||||
LEFT JOIN connections c ON c.contact_id = ct.contact_id
|
||||
@ -475,7 +478,8 @@ createOrUpdateContactRequest db user@User {userId} userContactLinkId invId Profi
|
||||
[sql|
|
||||
SELECT
|
||||
cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.user_contact_link_id,
|
||||
c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, p.preferences, cr.created_at, cr.updated_at
|
||||
c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, p.preferences, cr.created_at, cr.updated_at,
|
||||
cr.peer_chat_min_version, cr.peer_chat_max_version
|
||||
FROM contact_requests cr
|
||||
JOIN connections c USING (user_contact_link_id)
|
||||
JOIN contact_profiles p USING (contact_profile_id)
|
||||
@ -489,10 +493,26 @@ createOrUpdateContactRequest db user@User {userId} userContactLinkId invId Profi
|
||||
currentTs <- liftIO getCurrentTime
|
||||
updateProfile currentTs
|
||||
if displayName == oldDisplayName
|
||||
then Right <$> DB.execute db "UPDATE contact_requests SET agent_invitation_id = ?, updated_at = ? WHERE user_id = ? AND contact_request_id = ?" (invId, currentTs, userId, cReqId)
|
||||
then
|
||||
Right
|
||||
<$> DB.execute
|
||||
db
|
||||
[sql|
|
||||
UPDATE contact_requests
|
||||
SET agent_invitation_id = ?, 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 ->
|
||||
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)
|
||||
where
|
||||
updateProfile currentTs =
|
||||
@ -527,7 +547,8 @@ getContactRequest db User {userId} contactRequestId =
|
||||
[sql|
|
||||
SELECT
|
||||
cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.user_contact_link_id,
|
||||
c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, p.preferences, cr.created_at, cr.updated_at
|
||||
c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, p.preferences, cr.created_at, cr.updated_at,
|
||||
cr.peer_chat_min_version, cr.peer_chat_max_version
|
||||
FROM contact_requests cr
|
||||
JOIN connections c USING (user_contact_link_id)
|
||||
JOIN contact_profiles p USING (contact_profile_id)
|
||||
@ -566,8 +587,8 @@ deleteContactRequest db User {userId} contactRequestId = do
|
||||
(userId, userId, contactRequestId)
|
||||
DB.execute db "DELETE FROM contact_requests WHERE user_id = ? AND contact_request_id = ?" (userId, contactRequestId)
|
||||
|
||||
createAcceptedContact :: DB.Connection -> User -> ConnId -> ContactName -> ProfileId -> Profile -> Int64 -> Maybe XContactId -> Maybe IncognitoProfile -> IO Contact
|
||||
createAcceptedContact db user@User {userId, profile = LocalProfile {preferences}} agentConnId localDisplayName profileId profile userContactLinkId xContactId incognitoProfile = do
|
||||
createAcceptedContact :: DB.Connection -> User -> ConnId -> VersionRange -> ContactName -> ProfileId -> Profile -> Int64 -> Maybe XContactId -> Maybe IncognitoProfile -> IO Contact
|
||||
createAcceptedContact db user@User {userId, profile = LocalProfile {preferences}} agentConnId cReqChatVRange localDisplayName profileId profile userContactLinkId xContactId incognitoProfile = do
|
||||
DB.execute db "DELETE FROM contact_requests WHERE user_id = ? AND local_display_name = ?" (userId, localDisplayName)
|
||||
createdAt <- getCurrentTime
|
||||
customUserProfileId <- forM incognitoProfile $ \case
|
||||
@ -579,7 +600,7 @@ createAcceptedContact db user@User {userId, profile = LocalProfile {preferences}
|
||||
"INSERT INTO contacts (user_id, local_display_name, contact_profile_id, enable_ntfs, user_preferences, created_at, updated_at, chat_ts, xcontact_id) VALUES (?,?,?,?,?,?,?,?,?)"
|
||||
(userId, localDisplayName, profileId, True, userPreferences, createdAt, createdAt, createdAt, xContactId)
|
||||
contactId <- insertedRowId db
|
||||
activeConn <- createConnection_ db userId ConnContact (Just contactId) agentConnId Nothing (Just userContactLinkId) customUserProfileId 0 createdAt
|
||||
activeConn <- createConnection_ db userId ConnContact (Just contactId) agentConnId cReqChatVRange Nothing (Just userContactLinkId) customUserProfileId 0 createdAt
|
||||
let mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito activeConn
|
||||
pure $ Contact {contactId, localDisplayName, profile = toLocalProfile profileId profile "", activeConn, viaGroup = Nothing, contactUsed = False, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = createdAt, updatedAt = createdAt, chatTs = Just createdAt}
|
||||
|
||||
@ -603,7 +624,8 @@ getContact_ db user@User {userId} contactId deleted =
|
||||
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts,
|
||||
-- Connection
|
||||
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.local_alias,
|
||||
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter
|
||||
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter,
|
||||
c.peer_chat_min_version, c.peer_chat_max_version
|
||||
FROM contacts ct
|
||||
JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id
|
||||
LEFT JOIN connections c ON c.contact_id = ct.contact_id
|
||||
@ -651,7 +673,8 @@ getContactConnections db userId Contact {contactId} =
|
||||
db
|
||||
[sql|
|
||||
SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
|
||||
c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter
|
||||
c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter,
|
||||
c.peer_chat_min_version, c.peer_chat_max_version
|
||||
FROM connections c
|
||||
JOIN contacts ct ON ct.contact_id = c.contact_id
|
||||
WHERE c.user_id = ? AND ct.user_id = ? AND ct.contact_id = ?
|
||||
@ -667,7 +690,8 @@ getConnectionById db User {userId} connId = ExceptT $ do
|
||||
db
|
||||
[sql|
|
||||
SELECT connection_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, via_group_link, group_link_id, custom_user_profile_id,
|
||||
conn_status, conn_type, local_alias, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at, security_code, security_code_verified_at, auth_err_counter
|
||||
conn_status, conn_type, local_alias, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at, security_code, security_code_verified_at, auth_err_counter,
|
||||
peer_chat_min_version, peer_chat_max_version
|
||||
FROM connections
|
||||
WHERE user_id = ? AND connection_id = ?
|
||||
|]
|
||||
|
@ -425,7 +425,7 @@ getChatRefByFileId db User {userId} fileId =
|
||||
createSndFileConnection_ :: DB.Connection -> UserId -> Int64 -> ConnId -> IO Connection
|
||||
createSndFileConnection_ db userId fileId agentConnId = do
|
||||
currentTs <- getCurrentTime
|
||||
createConnection_ db userId ConnSndFile (Just fileId) agentConnId Nothing Nothing Nothing 0 currentTs
|
||||
createConnection_ db userId ConnSndFile (Just fileId) agentConnId chatInitialVRange Nothing Nothing Nothing 0 currentTs
|
||||
|
||||
updateSndFileStatus :: DB.Connection -> SndFileTransfer -> FileStatus -> IO ()
|
||||
updateSndFileStatus db SndFileTransfer {fileId, connId} status = do
|
||||
|
@ -83,6 +83,7 @@ module Simplex.Chat.Store.Groups
|
||||
updateGroupSettings,
|
||||
getXGrpMemIntroContDirect,
|
||||
getXGrpMemIntroContGroup,
|
||||
getHostConnId,
|
||||
)
|
||||
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.Crypto as C
|
||||
import Simplex.Messaging.Util (eitherToMaybe)
|
||||
import Simplex.Messaging.Version
|
||||
import UnliftIO.STM
|
||||
|
||||
type GroupInfoRow = (Int64, GroupName, GroupName, Text, Maybe Text, Maybe ImageData, Maybe ProfileId, Maybe Bool, Maybe Bool, Bool, Maybe GroupPreferences) :. (UTCTime, UTCTime, Maybe UTCTime) :. GroupMemberRow
|
||||
@ -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 (?,?,?,?,?,?,?,?,?)"
|
||||
(userId, groupId, groupLinkId, "group_link_" <> localDisplayName, cReq, memberRole, True, currentTs, currentTs)
|
||||
userContactLinkId <- insertedRowId db
|
||||
void $ createConnection_ db userId ConnUserContact (Just userContactLinkId) agentConnId Nothing Nothing Nothing 0 currentTs
|
||||
void $ createConnection_ db userId ConnUserContact (Just userContactLinkId) agentConnId chatInitialVRange Nothing Nothing Nothing 0 currentTs
|
||||
|
||||
getGroupLinkConnection :: DB.Connection -> User -> GroupInfo -> ExceptT StoreError IO Connection
|
||||
getGroupLinkConnection db User {userId} groupInfo@GroupInfo {groupId} =
|
||||
@ -151,7 +153,8 @@ getGroupLinkConnection db User {userId} groupInfo@GroupInfo {groupId} =
|
||||
db
|
||||
[sql|
|
||||
SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
|
||||
c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter
|
||||
c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter,
|
||||
c.peer_chat_min_version, c.peer_chat_max_version
|
||||
FROM connections c
|
||||
JOIN user_contact_links uc ON c.user_contact_link_id = uc.user_contact_link_id
|
||||
WHERE c.user_id = ? AND uc.user_id = ? AND uc.group_id = ?
|
||||
@ -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.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences,
|
||||
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
|
||||
c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter
|
||||
c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter,
|
||||
c.peer_chat_min_version, c.peer_chat_max_version
|
||||
FROM group_members m
|
||||
JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id)
|
||||
JOIN groups g ON g.group_id = m.group_id
|
||||
@ -478,20 +482,21 @@ getUserGroupsWithSummary db user _contactId_ search_ =
|
||||
-- the statuses on non-current members should match memberCurrent' function
|
||||
getGroupSummary :: DB.Connection -> User -> GroupId -> IO GroupSummary
|
||||
getGroupSummary db User {userId} groupId = do
|
||||
currentMembers_ <- maybeFirstRow fromOnly $
|
||||
DB.query
|
||||
db
|
||||
[sql|
|
||||
SELECT count (m.group_member_id)
|
||||
FROM groups g
|
||||
JOIN group_members m USING (group_id)
|
||||
WHERE g.user_id = ?
|
||||
AND g.group_id = ?
|
||||
AND m.member_status != ?
|
||||
AND m.member_status != ?
|
||||
AND m.member_status != ?
|
||||
|]
|
||||
(userId, groupId, GSMemRemoved, GSMemLeft, GSMemInvited)
|
||||
currentMembers_ <-
|
||||
maybeFirstRow fromOnly $
|
||||
DB.query
|
||||
db
|
||||
[sql|
|
||||
SELECT count (m.group_member_id)
|
||||
FROM groups g
|
||||
JOIN group_members m USING (group_id)
|
||||
WHERE g.user_id = ?
|
||||
AND g.group_id = ?
|
||||
AND m.member_status != ?
|
||||
AND m.member_status != ?
|
||||
AND m.member_status != ?
|
||||
|]
|
||||
(userId, groupId, GSMemRemoved, GSMemLeft, GSMemInvited)
|
||||
pure GroupSummary {currentMembers = fromMaybe 0 currentMembers_}
|
||||
|
||||
getContactGroupPreferences :: DB.Connection -> User -> Contact -> IO [FullGroupPreferences]
|
||||
@ -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.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences,
|
||||
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
|
||||
c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter
|
||||
c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter,
|
||||
c.peer_chat_min_version, c.peer_chat_max_version
|
||||
FROM group_members m
|
||||
JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id)
|
||||
LEFT JOIN connections c ON c.connection_id = (
|
||||
@ -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)
|
||||
|
||||
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
|
||||
createdAt <- liftIO getCurrentTime
|
||||
member@GroupMember {groupMemberId} <- createMember_ (MemberId memId) createdAt
|
||||
void $ createMemberConnection_ db userId groupMemberId agentConnId Nothing 0 createdAt
|
||||
void $ createMemberConnection_ db userId groupMemberId agentConnId peerChatVRange Nothing 0 createdAt
|
||||
pure member
|
||||
where
|
||||
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)
|
||||
)
|
||||
|
||||
createNewContactMemberAsync :: DB.Connection -> TVar ChaChaDRG -> User -> GroupId -> Contact -> GroupMemberRole -> (CommandId, ConnId) -> ExceptT StoreError IO ()
|
||||
createNewContactMemberAsync db gVar user@User {userId, userContactId} groupId Contact {contactId, localDisplayName, profile} memberRole (cmdId, agentConnId) =
|
||||
createNewContactMemberAsync :: DB.Connection -> TVar ChaChaDRG -> User -> GroupId -> Contact -> GroupMemberRole -> (CommandId, ConnId) -> VersionRange -> ExceptT StoreError IO ()
|
||||
createNewContactMemberAsync db gVar user@User {userId, userContactId} groupId Contact {contactId, localDisplayName, profile} memberRole (cmdId, agentConnId) peerChatVRange =
|
||||
createWithRandomId gVar $ \memId -> do
|
||||
createdAt <- liftIO getCurrentTime
|
||||
insertMember_ (MemberId memId) createdAt
|
||||
groupMemberId <- liftIO $ insertedRowId db
|
||||
Connection {connId} <- createMemberConnection_ db userId groupMemberId agentConnId Nothing 0 createdAt
|
||||
Connection {connId} <- createMemberConnection_ db userId groupMemberId agentConnId peerChatVRange Nothing 0 createdAt
|
||||
setCommandConnId db user cmdId connId
|
||||
where
|
||||
insertMember_ memberId createdAt =
|
||||
@ -670,30 +676,32 @@ createNewContactMemberAsync db gVar user@User {userId, userContactId} groupId Co
|
||||
:. (userId, localDisplayName, contactId, localProfileId profile, createdAt, createdAt)
|
||||
)
|
||||
|
||||
getContactViaMember :: DB.Connection -> User -> GroupMember -> IO (Maybe Contact)
|
||||
getContactViaMember :: DB.Connection -> User -> GroupMember -> ExceptT StoreError IO Contact
|
||||
getContactViaMember db user@User {userId} GroupMember {groupMemberId} =
|
||||
maybeFirstRow (toContact user) $
|
||||
DB.query
|
||||
db
|
||||
[sql|
|
||||
SELECT
|
||||
-- Contact
|
||||
ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.enable_ntfs, ct.send_rcpts, ct.favorite,
|
||||
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts,
|
||||
-- Connection
|
||||
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.local_alias,
|
||||
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter
|
||||
FROM contacts ct
|
||||
JOIN contact_profiles cp ON cp.contact_profile_id = ct.contact_profile_id
|
||||
JOIN connections c ON c.connection_id = (
|
||||
SELECT max(cc.connection_id)
|
||||
FROM connections cc
|
||||
where cc.contact_id = ct.contact_id
|
||||
)
|
||||
JOIN group_members m ON m.contact_id = ct.contact_id
|
||||
WHERE ct.user_id = ? AND m.group_member_id = ? AND ct.deleted = 0
|
||||
|]
|
||||
(userId, groupMemberId)
|
||||
ExceptT $
|
||||
firstRow (toContact user) (SEContactNotFoundByMemberId groupMemberId) $
|
||||
DB.query
|
||||
db
|
||||
[sql|
|
||||
SELECT
|
||||
-- Contact
|
||||
ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.enable_ntfs, ct.send_rcpts, ct.favorite,
|
||||
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts,
|
||||
-- Connection
|
||||
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.local_alias,
|
||||
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter,
|
||||
c.peer_chat_min_version, c.peer_chat_max_version
|
||||
FROM contacts ct
|
||||
JOIN contact_profiles cp ON cp.contact_profile_id = ct.contact_profile_id
|
||||
JOIN connections c ON c.connection_id = (
|
||||
SELECT max(cc.connection_id)
|
||||
FROM connections cc
|
||||
where cc.contact_id = ct.contact_id
|
||||
)
|
||||
JOIN group_members m ON m.contact_id = ct.contact_id
|
||||
WHERE ct.user_id = ? AND m.group_member_id = ? AND ct.deleted = 0
|
||||
|]
|
||||
(userId, groupMemberId)
|
||||
|
||||
setNewContactMemberConnRequest :: DB.Connection -> User -> GroupMember -> ConnReqInvitation -> IO ()
|
||||
setNewContactMemberConnRequest db User {userId} GroupMember {groupMemberId} connRequest = do
|
||||
@ -705,15 +713,15 @@ getMemberInvitation db User {userId} groupMemberId =
|
||||
fmap join . maybeFirstRow fromOnly $
|
||||
DB.query db "SELECT sent_inv_queue_info FROM group_members WHERE group_member_id = ? AND user_id = ?" (groupMemberId, userId)
|
||||
|
||||
createMemberConnection :: DB.Connection -> UserId -> GroupMember -> ConnId -> IO ()
|
||||
createMemberConnection db userId GroupMember {groupMemberId} agentConnId = do
|
||||
createMemberConnection :: DB.Connection -> UserId -> GroupMember -> ConnId -> VersionRange -> IO ()
|
||||
createMemberConnection db userId GroupMember {groupMemberId} agentConnId peerChatVRange = do
|
||||
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 user@User {userId} groupMemberId (cmdId, agentConnId) = do
|
||||
createMemberConnectionAsync :: DB.Connection -> User -> GroupMemberId -> (CommandId, ConnId) -> VersionRange -> IO ()
|
||||
createMemberConnectionAsync db user@User {userId} groupMemberId (cmdId, agentConnId) peerChatVRange = do
|
||||
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
|
||||
|
||||
updateGroupMemberStatus :: DB.Connection -> UserId -> GroupMember -> GroupMemberStatus -> IO ()
|
||||
@ -733,25 +741,30 @@ updateGroupMemberStatusById db userId groupMemberId memStatus = do
|
||||
|
||||
-- | add new member with profile
|
||||
createNewGroupMember :: DB.Connection -> User -> GroupInfo -> MemberInfo -> GroupMemberCategory -> GroupMemberStatus -> ExceptT StoreError IO GroupMember
|
||||
createNewGroupMember db user@User {userId} gInfo memInfo@(MemberInfo _ _ Profile {displayName, fullName, image, contactLink, preferences}) memCategory memStatus =
|
||||
ExceptT . withLocalDisplayName db userId displayName $ \localDisplayName -> do
|
||||
currentTs <- getCurrentTime
|
||||
createNewGroupMember db user gInfo memInfo memCategory memStatus = do
|
||||
currentTs <- liftIO getCurrentTime
|
||||
(localDisplayName, memProfileId) <- createNewMemberProfile_ db user memInfo currentTs
|
||||
let newMember =
|
||||
NewGroupMember
|
||||
{ memInfo,
|
||||
memCategory,
|
||||
memStatus,
|
||||
memInvitedBy = IBUnknown,
|
||||
localDisplayName,
|
||||
memContactId = Nothing,
|
||||
memProfileId
|
||||
}
|
||||
liftIO $ createNewMember_ db user gInfo newMember currentTs
|
||||
|
||||
createNewMemberProfile_ :: DB.Connection -> User -> MemberInfo -> UTCTime -> ExceptT StoreError IO (Text, ProfileId)
|
||||
createNewMemberProfile_ db User {userId} (MemberInfo _ _ _ Profile {displayName, fullName, image, contactLink, preferences}) createdAt =
|
||||
ExceptT . withLocalDisplayName db userId displayName $ \ldn -> do
|
||||
DB.execute
|
||||
db
|
||||
"INSERT INTO contact_profiles (display_name, full_name, image, contact_link, user_id, preferences, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)"
|
||||
(displayName, fullName, image, contactLink, userId, preferences, currentTs, currentTs)
|
||||
memProfileId <- insertedRowId db
|
||||
let newMember =
|
||||
NewGroupMember
|
||||
{ memInfo,
|
||||
memCategory,
|
||||
memStatus,
|
||||
memInvitedBy = IBUnknown,
|
||||
localDisplayName,
|
||||
memContactId = Nothing,
|
||||
memProfileId
|
||||
}
|
||||
Right <$> createNewMember_ db user gInfo newMember currentTs
|
||||
(displayName, fullName, image, contactLink, userId, preferences, createdAt, createdAt)
|
||||
profileId <- insertedRowId db
|
||||
pure $ Right (ldn, profileId)
|
||||
|
||||
createNewMember_ :: DB.Connection -> User -> GroupInfo -> NewGroupMember -> UTCTime -> IO GroupMember
|
||||
createNewMember_
|
||||
@ -759,7 +772,7 @@ createNewMember_
|
||||
User {userId, userContactId}
|
||||
GroupInfo {groupId}
|
||||
NewGroupMember
|
||||
{ memInfo = MemberInfo memberId memberRole memberProfile,
|
||||
{ memInfo = MemberInfo memberId memberRole _ memberProfile,
|
||||
memCategory = memberCategory,
|
||||
memStatus = memberStatus,
|
||||
memInvitedBy = invitedBy,
|
||||
@ -903,43 +916,41 @@ getIntroduction_ db reMember toMember = ExceptT $ do
|
||||
where
|
||||
toIntro :: [(Int64, Maybe ConnReqInvitation, Maybe ConnReqInvitation, GroupMemberIntroStatus)] -> Either StoreError GroupMemberIntro
|
||||
toIntro [(introId, groupConnReq, directConnReq, introStatus)] =
|
||||
let introInvitation = IntroInvitation <$> groupConnReq <*> directConnReq
|
||||
let introInvitation = IntroInvitation <$> groupConnReq <*> pure directConnReq
|
||||
in Right GroupMemberIntro {introId, reMember, toMember, introStatus, introInvitation}
|
||||
toIntro _ = Left SEIntroNotFound
|
||||
|
||||
createIntroReMember :: DB.Connection -> User -> GroupInfo -> GroupMember -> MemberInfo -> (CommandId, ConnId) -> (CommandId, ConnId) -> Maybe ProfileId -> ExceptT StoreError IO GroupMember
|
||||
createIntroReMember db user@User {userId} gInfo@GroupInfo {groupId} _host@GroupMember {memberContactId, activeConn} memInfo@(MemberInfo _ _ memberProfile) (groupCmdId, groupAgentConnId) (directCmdId, directAgentConnId) customUserProfileId = do
|
||||
let cLevel = 1 + maybe 0 (connLevel :: Connection -> Int) activeConn
|
||||
createIntroReMember :: DB.Connection -> User -> GroupInfo -> GroupMember -> MemberInfo -> (CommandId, ConnId) -> Maybe (CommandId, ConnId) -> Maybe ProfileId -> ExceptT StoreError IO GroupMember
|
||||
createIntroReMember db user@User {userId} gInfo@GroupInfo {groupId} _host@GroupMember {memberContactId, activeConn} memInfo@(MemberInfo _ _ memberChatVRange memberProfile) (groupCmdId, groupAgentConnId) directConnIds customUserProfileId = do
|
||||
let mcvr = maybe chatInitialVRange fromChatVRange memberChatVRange
|
||||
cLevel = 1 + maybe 0 (connLevel :: Connection -> Int) activeConn
|
||||
currentTs <- liftIO getCurrentTime
|
||||
Connection {connId = directConnId} <- liftIO $ createConnection_ db userId ConnContact Nothing directAgentConnId memberContactId Nothing customUserProfileId cLevel currentTs
|
||||
liftIO $ setCommandConnId db user directCmdId directConnId
|
||||
(localDisplayName, contactId, memProfileId) <- createContact_ db userId directConnId memberProfile "" (Just groupId) currentTs Nothing
|
||||
newMember <- case directConnIds of
|
||||
Just (directCmdId, directAgentConnId) -> do
|
||||
Connection {connId = directConnId} <- liftIO $ createConnection_ db userId ConnContact Nothing directAgentConnId mcvr memberContactId Nothing customUserProfileId cLevel currentTs
|
||||
liftIO $ setCommandConnId db user directCmdId directConnId
|
||||
(localDisplayName, contactId, memProfileId) <- createContact_ db userId directConnId memberProfile "" (Just groupId) currentTs Nothing
|
||||
pure $ NewGroupMember {memInfo, memCategory = GCPreMember, memStatus = GSMemIntroduced, memInvitedBy = IBUnknown, localDisplayName, memContactId = Just contactId, memProfileId}
|
||||
Nothing -> do
|
||||
(localDisplayName, memProfileId) <- createNewMemberProfile_ db user memInfo currentTs
|
||||
pure $ NewGroupMember {memInfo, memCategory = GCPreMember, memStatus = GSMemIntroduced, memInvitedBy = IBUnknown, localDisplayName, memContactId = Nothing, memProfileId}
|
||||
liftIO $ do
|
||||
let newMember =
|
||||
NewGroupMember
|
||||
{ memInfo,
|
||||
memCategory = GCPreMember,
|
||||
memStatus = GSMemIntroduced,
|
||||
memInvitedBy = IBUnknown,
|
||||
localDisplayName,
|
||||
memContactId = Just contactId,
|
||||
memProfileId
|
||||
}
|
||||
member <- createNewMember_ db user gInfo newMember currentTs
|
||||
conn@Connection {connId = groupConnId} <- createMemberConnection_ db userId (groupMemberId' member) groupAgentConnId memberContactId cLevel currentTs
|
||||
conn@Connection {connId = groupConnId} <- createMemberConnection_ db userId (groupMemberId' member) groupAgentConnId mcvr memberContactId cLevel currentTs
|
||||
liftIO $ setCommandConnId db user groupCmdId groupConnId
|
||||
pure (member :: GroupMember) {activeConn = Just conn}
|
||||
|
||||
createIntroToMemberContact :: DB.Connection -> User -> GroupMember -> GroupMember -> (CommandId, ConnId) -> (CommandId, ConnId) -> Maybe ProfileId -> IO ()
|
||||
createIntroToMemberContact db user@User {userId} GroupMember {memberContactId = viaContactId, activeConn} _to@GroupMember {groupMemberId, localDisplayName} (groupCmdId, groupAgentConnId) (directCmdId, directAgentConnId) customUserProfileId = do
|
||||
createIntroToMemberContact :: DB.Connection -> User -> GroupMember -> GroupMember -> VersionRange -> (CommandId, ConnId) -> Maybe (CommandId, ConnId) -> Maybe ProfileId -> IO ()
|
||||
createIntroToMemberContact db user@User {userId} GroupMember {memberContactId = viaContactId, activeConn} _to@GroupMember {groupMemberId, localDisplayName} mcvr (groupCmdId, groupAgentConnId) directConnIds customUserProfileId = do
|
||||
let cLevel = 1 + maybe 0 (connLevel :: Connection -> Int) activeConn
|
||||
currentTs <- getCurrentTime
|
||||
Connection {connId = groupConnId} <- createMemberConnection_ db userId groupMemberId groupAgentConnId viaContactId cLevel currentTs
|
||||
Connection {connId = groupConnId} <- createMemberConnection_ db userId groupMemberId groupAgentConnId mcvr viaContactId cLevel currentTs
|
||||
setCommandConnId db user groupCmdId groupConnId
|
||||
Connection {connId = directConnId} <- createConnection_ db userId ConnContact Nothing directAgentConnId viaContactId Nothing customUserProfileId cLevel currentTs
|
||||
setCommandConnId db user directCmdId directConnId
|
||||
contactId <- createMemberContact_ directConnId currentTs
|
||||
updateMember_ contactId currentTs
|
||||
forM_ directConnIds $ \(directCmdId, directAgentConnId) -> do
|
||||
Connection {connId = directConnId} <- createConnection_ db userId ConnContact Nothing directAgentConnId mcvr viaContactId Nothing customUserProfileId cLevel currentTs
|
||||
setCommandConnId db user directCmdId directConnId
|
||||
contactId <- createMemberContact_ directConnId currentTs
|
||||
updateMember_ contactId currentTs
|
||||
where
|
||||
createMemberContact_ :: Int64 -> UTCTime -> IO Int64
|
||||
createMemberContact_ connId ts = do
|
||||
@ -966,8 +977,8 @@ createIntroToMemberContact db user@User {userId} GroupMember {memberContactId =
|
||||
|]
|
||||
[":contact_id" := contactId, ":updated_at" := ts, ":group_member_id" := groupMemberId]
|
||||
|
||||
createMemberConnection_ :: DB.Connection -> UserId -> Int64 -> ConnId -> Maybe Int64 -> Int -> UTCTime -> IO Connection
|
||||
createMemberConnection_ db userId groupMemberId agentConnId viaContact = createConnection_ db userId ConnMember (Just groupMemberId) agentConnId viaContact Nothing Nothing
|
||||
createMemberConnection_ :: DB.Connection -> UserId -> Int64 -> ConnId -> VersionRange -> Maybe Int64 -> Int -> UTCTime -> IO Connection
|
||||
createMemberConnection_ db userId groupMemberId agentConnId peerChatVRange viaContact = createConnection_ db userId ConnMember (Just groupMemberId) agentConnId peerChatVRange viaContact Nothing Nothing
|
||||
|
||||
getViaGroupMember :: DB.Connection -> User -> Contact -> IO (Maybe (GroupInfo, GroupMember))
|
||||
getViaGroupMember db User {userId, userContactId} Contact {contactId} =
|
||||
@ -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.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences,
|
||||
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
|
||||
c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter
|
||||
c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter,
|
||||
c.peer_chat_min_version, c.peer_chat_max_version
|
||||
FROM group_members m
|
||||
JOIN contacts ct ON ct.contact_id = m.contact_id
|
||||
JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id)
|
||||
@ -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,
|
||||
p.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts,
|
||||
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
|
||||
c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter
|
||||
c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter,
|
||||
c.peer_chat_min_version, c.peer_chat_max_version
|
||||
FROM contacts ct
|
||||
JOIN contact_profiles p ON ct.contact_profile_id = p.contact_profile_id
|
||||
JOIN connections c ON c.connection_id = (
|
||||
@ -1336,3 +1349,9 @@ getXGrpMemIntroContGroup db User {userId} GroupMember {groupMemberId} = do
|
||||
toCont (hostConnId, connReq_) = case connReq_ of
|
||||
Just connReq -> Just (hostConnId, connReq)
|
||||
_ -> Nothing
|
||||
|
||||
getHostConnId :: DB.Connection -> User -> GroupId -> ExceptT StoreError IO GroupMemberId
|
||||
getHostConnId db user@User {userId} groupId = do
|
||||
hostMemberId <- getHostMemberId_ db user groupId
|
||||
ExceptT . firstRow fromOnly (SEConnectionNotFoundByMemberId hostMemberId) $
|
||||
DB.query db "SELECT connection_id FROM connections WHERE user_id = ? AND group_member_id = ?" (userId, hostMemberId)
|
||||
|
@ -479,6 +479,7 @@ getDirectChatPreviews_ db user@User {userId} = do
|
||||
-- Connection
|
||||
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.local_alias,
|
||||
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter,
|
||||
c.peer_chat_min_version, c.peer_chat_max_version,
|
||||
-- ChatStats
|
||||
COALESCE(ChatStats.UnreadCount, 0), COALESCE(ChatStats.MinUnread, 0), ct.unread_chat,
|
||||
-- ChatItem
|
||||
@ -609,7 +610,8 @@ getContactRequestChatPreviews_ db User {userId} =
|
||||
[sql|
|
||||
SELECT
|
||||
cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.user_contact_link_id,
|
||||
c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, p.preferences, cr.created_at, cr.updated_at
|
||||
c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, p.preferences, cr.created_at, cr.updated_at,
|
||||
cr.peer_chat_min_version, cr.peer_chat_max_version
|
||||
FROM contact_requests cr
|
||||
JOIN connections c ON c.user_contact_link_id = cr.user_contact_link_id
|
||||
JOIN contact_profiles p ON p.contact_profile_id = cr.contact_profile_id
|
||||
|
@ -77,6 +77,7 @@ import Simplex.Chat.Migrations.M20230705_delivery_receipts
|
||||
import Simplex.Chat.Migrations.M20230721_group_snd_item_statuses
|
||||
import Simplex.Chat.Migrations.M20230814_indexes
|
||||
import Simplex.Chat.Migrations.M20230827_file_encryption
|
||||
import Simplex.Chat.Migrations.M20230829_connections_chat_vrange
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..))
|
||||
|
||||
schemaMigrations :: [(String, Query, Maybe Query)]
|
||||
@ -153,7 +154,8 @@ schemaMigrations =
|
||||
("20230705_delivery_receipts", m20230705_delivery_receipts, Just down_m20230705_delivery_receipts),
|
||||
("20230721_group_snd_item_statuses", m20230721_group_snd_item_statuses, Just down_m20230721_group_snd_item_statuses),
|
||||
("20230814_indexes", m20230814_indexes, Just down_m20230814_indexes),
|
||||
("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
|
||||
|
@ -302,7 +302,7 @@ createUserContactLink db User {userId} agentConnId cReq =
|
||||
"INSERT INTO user_contact_links (user_id, conn_req_contact, created_at, updated_at) VALUES (?,?,?,?)"
|
||||
(userId, cReq, currentTs, currentTs)
|
||||
userContactLinkId <- insertedRowId db
|
||||
void $ createConnection_ db userId ConnUserContact (Just userContactLinkId) agentConnId Nothing Nothing Nothing 0 currentTs
|
||||
void $ createConnection_ db userId ConnUserContact (Just userContactLinkId) agentConnId chatInitialVRange Nothing Nothing Nothing 0 currentTs
|
||||
|
||||
getUserAddressConnections :: DB.Connection -> User -> ExceptT StoreError IO [Connection]
|
||||
getUserAddressConnections db User {userId} = do
|
||||
@ -316,7 +316,8 @@ getUserAddressConnections db User {userId} = do
|
||||
db
|
||||
[sql|
|
||||
SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
|
||||
c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter
|
||||
c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter,
|
||||
c.peer_chat_min_version, c.peer_chat_max_version
|
||||
FROM connections c
|
||||
JOIN user_contact_links uc ON c.user_contact_link_id = uc.user_contact_link_id
|
||||
WHERE c.user_id = ? AND uc.user_id = ? AND uc.local_display_name = '' AND uc.group_id IS NULL
|
||||
@ -331,6 +332,7 @@ getUserContactLinks db User {userId} =
|
||||
[sql|
|
||||
SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
|
||||
c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter,
|
||||
c.peer_chat_min_version, c.peer_chat_max_version,
|
||||
uc.user_contact_link_id, uc.conn_req_contact, uc.group_id
|
||||
FROM connections c
|
||||
JOIN user_contact_links uc ON c.user_contact_link_id = uc.user_contact_link_id
|
||||
|
@ -17,8 +17,8 @@ import Control.Monad.Except
|
||||
import Crypto.Random (ChaChaDRG, randomBytesGenerate)
|
||||
import Data.Aeson (ToJSON)
|
||||
import qualified Data.Aeson as J
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Base64 as B64
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import Data.Int (Int64)
|
||||
import Data.Maybe (fromMaybe, isJust, listToMaybe)
|
||||
import Data.Text (Text)
|
||||
@ -37,6 +37,7 @@ import Simplex.Messaging.Agent.Store.SQLite (firstRow, maybeFirstRow)
|
||||
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
|
||||
import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON)
|
||||
import Simplex.Messaging.Util (allFinally)
|
||||
import Simplex.Messaging.Version
|
||||
import UnliftIO.STM
|
||||
|
||||
-- These error type constructors must be added to mobile apps
|
||||
@ -50,6 +51,7 @@ data StoreError
|
||||
| SEUserNotFoundByContactRequestId {contactRequestId :: Int64}
|
||||
| SEContactNotFound {contactId :: ContactId}
|
||||
| SEContactNotFoundByName {contactName :: ContactName}
|
||||
| SEContactNotFoundByMemberId {groupMemberId :: GroupMemberId}
|
||||
| SEContactNotReady {contactName :: ContactName}
|
||||
| SEDuplicateContactLink
|
||||
| SEUserContactLinkNotFound
|
||||
@ -77,6 +79,7 @@ data StoreError
|
||||
| SERcvFileNotFoundXFTP {agentRcvFileId :: AgentRcvFileId}
|
||||
| SEConnectionNotFound {agentConnId :: AgentConnId}
|
||||
| SEConnectionNotFoundById {connId :: Int64}
|
||||
| SEConnectionNotFoundByMemberId {groupMemberId :: GroupMemberId}
|
||||
| SEPendingConnectionNotFound {connId :: Int64}
|
||||
| SEIntroNotFound
|
||||
| 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 ConnectionRow = (Int64, ConnId, Int, Maybe Int64, Maybe Int64, Bool, Maybe GroupLinkId, Maybe Int64, ConnStatus, ConnType, LocalAlias) :. EntityIdsRow :. (UTCTime, Maybe Text, Maybe UTCTime, Int)
|
||||
type ConnectionRow = (Int64, ConnId, Int, Maybe Int64, Maybe Int64, Bool, Maybe GroupLinkId, Maybe Int64, ConnStatus, ConnType, LocalAlias) :. EntityIdsRow :. (UTCTime, Maybe Text, Maybe UTCTime, Int, Version, Version)
|
||||
|
||||
type MaybeConnectionRow = (Maybe Int64, Maybe ConnId, Maybe Int, Maybe Int64, Maybe Int64, Maybe Bool, Maybe GroupLinkId, Maybe Int64, Maybe ConnStatus, Maybe ConnType, Maybe LocalAlias) :. EntityIdsRow :. (Maybe UTCTime, Maybe Text, Maybe UTCTime, Maybe Int)
|
||||
type MaybeConnectionRow = (Maybe Int64, Maybe ConnId, Maybe Int, Maybe Int64, Maybe Int64, Maybe Bool, Maybe GroupLinkId, Maybe Int64, Maybe ConnStatus, Maybe ConnType, Maybe LocalAlias) :. EntityIdsRow :. (Maybe UTCTime, Maybe Text, Maybe UTCTime, Maybe Int, Maybe Version, Maybe Version)
|
||||
|
||||
toConnection :: ConnectionRow -> Connection
|
||||
toConnection ((connId, acId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (createdAt, code_, verifiedAt_, authErrCounter)) =
|
||||
toConnection ((connId, acId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (createdAt, code_, verifiedAt_, authErrCounter, minVer, maxVer)) =
|
||||
let entityId = entityId_ connType
|
||||
connectionCode = SecurityCode <$> code_ <*> verifiedAt_
|
||||
in Connection {connId, agentConnId = AgentConnId acId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, localAlias, entityId, connectionCode, authErrCounter, createdAt}
|
||||
peerChatVRange = fromMaybe (versionToRange maxVer) $ safeVersionRange minVer maxVer
|
||||
in Connection {connId, agentConnId = AgentConnId acId, peerChatVRange, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, localAlias, entityId, connectionCode, authErrCounter, createdAt}
|
||||
where
|
||||
entityId_ :: ConnType -> Maybe Int64
|
||||
entityId_ ConnContact = contactId
|
||||
@ -150,12 +154,12 @@ toConnection ((connId, acId, connLevel, viaContact, viaUserContactLink, viaGroup
|
||||
entityId_ ConnUserContact = userContactLinkId
|
||||
|
||||
toMaybeConnection :: MaybeConnectionRow -> Maybe Connection
|
||||
toMaybeConnection ((Just connId, Just agentConnId, Just connLevel, viaContact, viaUserContactLink, Just viaGroupLink, groupLinkId, customUserProfileId, Just connStatus, Just connType, Just localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (Just createdAt, code_, verifiedAt_, Just authErrCounter)) =
|
||||
Just $ toConnection ((connId, agentConnId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (createdAt, code_, verifiedAt_, authErrCounter))
|
||||
toMaybeConnection ((Just connId, Just agentConnId, Just connLevel, viaContact, viaUserContactLink, Just viaGroupLink, groupLinkId, customUserProfileId, Just connStatus, Just connType, Just localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (Just createdAt, code_, verifiedAt_, Just authErrCounter, Just minVer, Just maxVer)) =
|
||||
Just $ toConnection ((connId, agentConnId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (createdAt, code_, verifiedAt_, authErrCounter, minVer, maxVer))
|
||||
toMaybeConnection _ = Nothing
|
||||
|
||||
createConnection_ :: DB.Connection -> UserId -> ConnType -> Maybe Int64 -> ConnId -> Maybe ContactId -> Maybe Int64 -> Maybe ProfileId -> Int -> UTCTime -> IO Connection
|
||||
createConnection_ db userId connType entityId acId viaContact viaUserContactLink customUserProfileId connLevel currentTs = do
|
||||
createConnection_ :: DB.Connection -> UserId -> ConnType -> Maybe Int64 -> ConnId -> VersionRange -> Maybe ContactId -> Maybe Int64 -> Maybe ProfileId -> Int -> UTCTime -> IO Connection
|
||||
createConnection_ db userId connType entityId acId peerChatVRange@(VersionRange minV maxV) viaContact viaUserContactLink customUserProfileId connLevel currentTs = do
|
||||
viaLinkGroupId :: Maybe Int64 <- fmap join . forM viaUserContactLink $ \ucLinkId ->
|
||||
maybeFirstRow fromOnly $ DB.query db "SELECT group_id FROM user_contact_links WHERE user_id = ? AND user_contact_link_id = ? AND group_id IS NOT NULL" (userId, ucLinkId)
|
||||
let viaGroupLink = isJust viaLinkGroupId
|
||||
@ -164,17 +168,30 @@ createConnection_ db userId connType entityId acId viaContact viaUserContactLink
|
||||
[sql|
|
||||
INSERT INTO connections (
|
||||
user_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, via_group_link, custom_user_profile_id, conn_status, conn_type,
|
||||
contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at, updated_at
|
||||
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at, updated_at,
|
||||
peer_chat_min_version, peer_chat_max_version
|
||||
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
|]
|
||||
( (userId, acId, connLevel, viaContact, viaUserContactLink, viaGroupLink, customUserProfileId, ConnNew, connType)
|
||||
:. (ent ConnContact, ent ConnMember, ent ConnSndFile, ent ConnRcvFile, ent ConnUserContact, currentTs, currentTs)
|
||||
:. (minV, maxV)
|
||||
)
|
||||
connId <- insertedRowId db
|
||||
pure Connection {connId, agentConnId = AgentConnId acId, connType, entityId, viaContact, viaUserContactLink, viaGroupLink, groupLinkId = Nothing, customUserProfileId, connLevel, connStatus = ConnNew, localAlias = "", createdAt = currentTs, connectionCode = Nothing, authErrCounter = 0}
|
||||
pure Connection {connId, agentConnId = AgentConnId acId, peerChatVRange, connType, entityId, viaContact, viaUserContactLink, viaGroupLink, groupLinkId = Nothing, customUserProfileId, connLevel, connStatus = ConnNew, localAlias = "", createdAt = currentTs, connectionCode = Nothing, authErrCounter = 0}
|
||||
where
|
||||
ent ct = if connType == ct then entityId else Nothing
|
||||
|
||||
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 User {userId} cmdId connId = do
|
||||
updatedAt <- getCurrentTime
|
||||
@ -256,12 +273,13 @@ getProfileById db userId profileId =
|
||||
toProfile :: (ContactName, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Maybe Preferences) -> LocalProfile
|
||||
toProfile (displayName, fullName, image, contactLink, localAlias, preferences) = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences, localAlias}
|
||||
|
||||
type ContactRequestRow = (Int64, ContactName, AgentInvId, Int64, AgentConnId, Int64, ContactName, Text, Maybe ImageData, Maybe ConnReqContact) :. (Maybe XContactId, Maybe Preferences, UTCTime, UTCTime)
|
||||
type ContactRequestRow = (Int64, ContactName, AgentInvId, Int64, AgentConnId, Int64, ContactName, Text, Maybe ImageData, Maybe ConnReqContact) :. (Maybe XContactId, Maybe Preferences, UTCTime, UTCTime, Version, Version)
|
||||
|
||||
toContactRequest :: ContactRequestRow -> UserContactRequest
|
||||
toContactRequest ((contactRequestId, localDisplayName, agentInvitationId, userContactLinkId, agentContactConnId, profileId, displayName, fullName, image, contactLink) :. (xContactId, preferences, createdAt, updatedAt)) = do
|
||||
toContactRequest ((contactRequestId, localDisplayName, agentInvitationId, userContactLinkId, agentContactConnId, profileId, displayName, fullName, image, contactLink) :. (xContactId, preferences, createdAt, updatedAt, minVer, maxVer)) = do
|
||||
let profile = Profile {displayName, fullName, image, contactLink, preferences}
|
||||
in UserContactRequest {contactRequestId, agentInvitationId, userContactLinkId, agentContactConnId, localDisplayName, profileId, profile, xContactId, createdAt, updatedAt}
|
||||
cReqChatVRange = fromMaybe (versionToRange maxVer) $ safeVersionRange minVer maxVer
|
||||
in UserContactRequest {contactRequestId, agentInvitationId, userContactLinkId, agentContactConnId, cReqChatVRange, localDisplayName, profileId, profile, xContactId, createdAt, updatedAt}
|
||||
|
||||
userQuery :: Query
|
||||
userQuery =
|
||||
|
@ -47,6 +47,7 @@ import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Parsers (dropPrefix, fromTextField_, sumTypeJSON, taggedObjectJSON)
|
||||
import Simplex.Messaging.Protocol (ProtoServerWithAuth, ProtocolTypeI)
|
||||
import Simplex.Messaging.Util ((<$?>))
|
||||
import Simplex.Messaging.Version
|
||||
|
||||
class IsContact a where
|
||||
contactId' :: a -> ContactId
|
||||
@ -232,6 +233,7 @@ data UserContactRequest = UserContactRequest
|
||||
agentInvitationId :: AgentInvId,
|
||||
userContactLinkId :: Int64,
|
||||
agentContactConnId :: AgentConnId, -- connection id of user contact
|
||||
cReqChatVRange :: VersionRange,
|
||||
localDisplayName :: ContactName,
|
||||
profileId :: Int64,
|
||||
profile :: Profile,
|
||||
@ -538,24 +540,31 @@ instance ToJSON MemberIdRole where toEncoding = J.genericToEncoding J.defaultOpt
|
||||
|
||||
data IntroInvitation = IntroInvitation
|
||||
{ groupConnReq :: ConnReqInvitation,
|
||||
directConnReq :: ConnReqInvitation
|
||||
directConnReq :: Maybe ConnReqInvitation
|
||||
}
|
||||
deriving (Eq, Show, Generic, FromJSON)
|
||||
|
||||
instance ToJSON IntroInvitation where toEncoding = J.genericToEncoding J.defaultOptions
|
||||
instance ToJSON IntroInvitation where
|
||||
toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True}
|
||||
toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True}
|
||||
|
||||
data MemberInfo = MemberInfo
|
||||
{ memberId :: MemberId,
|
||||
memberRole :: GroupMemberRole,
|
||||
v :: Maybe ChatVersionRange,
|
||||
profile :: Profile
|
||||
}
|
||||
deriving (Eq, Show, Generic, FromJSON)
|
||||
|
||||
instance ToJSON MemberInfo where toEncoding = J.genericToEncoding J.defaultOptions
|
||||
instance ToJSON MemberInfo where
|
||||
toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True}
|
||||
toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True}
|
||||
|
||||
memberInfo :: GroupMember -> MemberInfo
|
||||
memberInfo GroupMember {memberId, memberRole, memberProfile} =
|
||||
MemberInfo memberId memberRole (fromLocalProfile memberProfile)
|
||||
memberInfo GroupMember {memberId, memberRole, memberProfile, activeConn} =
|
||||
MemberInfo memberId memberRole memberChatVRange (fromLocalProfile memberProfile)
|
||||
where
|
||||
memberChatVRange = ChatVersionRange . peerChatVRange <$> activeConn
|
||||
|
||||
data ReceivedGroupInvitation = ReceivedGroupInvitation
|
||||
{ fromMember :: GroupMember,
|
||||
@ -1158,6 +1167,7 @@ type ConnReqContact = ConnectionRequestUri 'CMContact
|
||||
data Connection = Connection
|
||||
{ connId :: Int64,
|
||||
agentConnId :: AgentConnId,
|
||||
peerChatVRange :: VersionRange,
|
||||
connLevel :: Int,
|
||||
viaContact :: Maybe Int64, -- group member contact ID, if not direct connection
|
||||
viaUserContactLink :: Maybe Int64, -- user contact link ID, if connected via "user address"
|
||||
@ -1468,3 +1478,15 @@ instance ProtocolTypeI p => ToJSON (ServerCfg p) where
|
||||
|
||||
instance ProtocolTypeI p => FromJSON (ServerCfg p) where
|
||||
parseJSON = J.genericParseJSON J.defaultOptions {J.omitNothingFields = True}
|
||||
|
||||
newtype ChatVersionRange = ChatVersionRange {fromChatVRange :: VersionRange} deriving (Eq, Show)
|
||||
|
||||
chatInitialVRange :: VersionRange
|
||||
chatInitialVRange = versionToRange 1
|
||||
|
||||
instance FromJSON ChatVersionRange where
|
||||
parseJSON v = ChatVersionRange <$> strParseJSON "ChatVersionRange" v
|
||||
|
||||
instance ToJSON ChatVersionRange where
|
||||
toJSON (ChatVersionRange vr) = strToJSON vr
|
||||
toEncoding (ChatVersionRange vr) = strToJEncoding vr
|
||||
|
@ -59,6 +59,7 @@ import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType, Pro
|
||||
import qualified Simplex.Messaging.Protocol as SMP
|
||||
import Simplex.Messaging.Transport.Client (TransportHost (..))
|
||||
import Simplex.Messaging.Util (bshow, tshow)
|
||||
import Simplex.Messaging.Version hiding (version)
|
||||
import System.Console.ANSI.Types
|
||||
|
||||
type CurrentTime = UTCTime
|
||||
@ -952,7 +953,7 @@ viewNetworkConfig NetworkConfig {socksProxy, tcpTimeout} =
|
||||
]
|
||||
|
||||
viewContactInfo :: Contact -> ConnectionStats -> Maybe Profile -> [StyledString]
|
||||
viewContactInfo ct@Contact {contactId, profile = LocalProfile {localAlias, contactLink}} stats incognitoProfile =
|
||||
viewContactInfo ct@Contact {contactId, profile = LocalProfile {localAlias, contactLink}, activeConn} stats incognitoProfile =
|
||||
["contact ID: " <> sShow contactId]
|
||||
<> viewConnectionStats stats
|
||||
<> maybe [] (\l -> ["contact address: " <> (plain . strEncode) l]) contactLink
|
||||
@ -962,6 +963,7 @@ viewContactInfo ct@Contact {contactId, profile = LocalProfile {localAlias, conta
|
||||
incognitoProfile
|
||||
<> ["alias: " <> plain localAlias | localAlias /= ""]
|
||||
<> [viewConnectionVerified (contactSecurityCode ct)]
|
||||
<> [viewPeerChatVRange (peerChatVRange activeConn)]
|
||||
|
||||
viewGroupInfo :: GroupInfo -> GroupSummary -> [StyledString]
|
||||
viewGroupInfo GroupInfo {groupId} s =
|
||||
@ -970,18 +972,22 @@ viewGroupInfo GroupInfo {groupId} s =
|
||||
]
|
||||
|
||||
viewGroupMemberInfo :: GroupInfo -> GroupMember -> Maybe ConnectionStats -> [StyledString]
|
||||
viewGroupMemberInfo GroupInfo {groupId} m@GroupMember {groupMemberId, memberProfile = LocalProfile {localAlias}} stats =
|
||||
viewGroupMemberInfo GroupInfo {groupId} m@GroupMember {groupMemberId, memberProfile = LocalProfile {localAlias}, activeConn} stats =
|
||||
[ "group ID: " <> sShow groupId,
|
||||
"member ID: " <> sShow groupMemberId
|
||||
]
|
||||
<> maybe ["member not connected"] viewConnectionStats stats
|
||||
<> ["alias: " <> plain localAlias | localAlias /= ""]
|
||||
<> [viewConnectionVerified (memberSecurityCode m) | isJust stats]
|
||||
<> maybe [] (\ac -> [viewPeerChatVRange (peerChatVRange ac)]) activeConn
|
||||
|
||||
viewConnectionVerified :: Maybe SecurityCode -> StyledString
|
||||
viewConnectionVerified (Just _) = "connection verified" -- TODO show verification time?
|
||||
viewConnectionVerified _ = "connection not verified, use " <> highlight' "/code" <> " command to see security code"
|
||||
|
||||
viewPeerChatVRange :: VersionRange -> StyledString
|
||||
viewPeerChatVRange (VersionRange minVer maxVer) = "peer chat protocol version range: (" <> sShow minVer <> ", " <> sShow maxVer <> ")"
|
||||
|
||||
viewConnectionStats :: ConnectionStats -> [StyledString]
|
||||
viewConnectionStats ConnectionStats {rcvQueuesInfo, sndQueuesInfo} =
|
||||
["receiving messages via: " <> viewRcvQueuesInfo rcvQueuesInfo | not $ null rcvQueuesInfo]
|
||||
|
@ -49,7 +49,7 @@ extra-deps:
|
||||
# - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561
|
||||
# - ../simplexmq
|
||||
- github: simplex-chat/simplexmq
|
||||
commit: 980e5c4d1ec15f44290542fd2a5d1c08456f00d1
|
||||
commit: 351f42650c57f310fc1ea858ff9b7178823f1fd4
|
||||
- github: kazu-yamamoto/http2
|
||||
commit: b5a1b7200cf5bc7044af34ba325284271f6dff25
|
||||
# - ../direct-sqlcipher
|
||||
|
@ -13,13 +13,14 @@ import Control.Monad (forM_)
|
||||
import Directory.Options
|
||||
import Directory.Service
|
||||
import Directory.Store
|
||||
import GHC.IO.Handle (hClose)
|
||||
import Simplex.Chat.Bot.KnownContacts
|
||||
import Simplex.Chat.Controller (ChatConfig (..))
|
||||
import Simplex.Chat.Core
|
||||
import Simplex.Chat.Options (ChatOpts (..), CoreChatOpts (..))
|
||||
import Simplex.Chat.Types (GroupMemberRole (..), Profile (..))
|
||||
import System.FilePath ((</>))
|
||||
import Test.Hspec
|
||||
import GHC.IO.Handle (hClose)
|
||||
|
||||
directoryServiceTests :: SpecWith FilePath
|
||||
directoryServiceTests = do
|
||||
@ -232,10 +233,10 @@ testJoinGroup tmp =
|
||||
dan <## "bob (Bob): contact is connected"
|
||||
dan <## "#privacy: you joined the group"
|
||||
dan <# ("#privacy bob> " <> welcomeMsg)
|
||||
dan <###
|
||||
[ "#privacy: member SimpleX-Directory is connected",
|
||||
"#privacy: member cath (Catherine) is connected"
|
||||
],
|
||||
dan
|
||||
<### [ "#privacy: member SimpleX-Directory is connected",
|
||||
"#privacy: member cath (Catherine) is connected"
|
||||
],
|
||||
do
|
||||
cath <## "#privacy: bob added dan (Daniel) to the group (connecting...)"
|
||||
cath <## "#privacy: new member dan is connected"
|
||||
@ -243,9 +244,9 @@ testJoinGroup tmp =
|
||||
|
||||
testDelistedOwnerLeaves :: HasCallStack => FilePath -> IO ()
|
||||
testDelistedOwnerLeaves tmp =
|
||||
withDirectoryService tmp $ \superUser dsLink ->
|
||||
withNewTestChat tmp "bob" bobProfile $ \bob ->
|
||||
withNewTestChat tmp "cath" cathProfile $ \cath -> do
|
||||
withDirectoryServiceCfg tmp testCfgCreateGroupDirect $ \superUser dsLink ->
|
||||
withNewTestChatCfg tmp testCfgCreateGroupDirect "bob" bobProfile $ \bob ->
|
||||
withNewTestChatCfg tmp testCfgCreateGroupDirect "cath" cathProfile $ \cath -> do
|
||||
bob `connectVia` dsLink
|
||||
registerGroup superUser bob "privacy" "Privacy"
|
||||
addCathAsOwner bob cath
|
||||
@ -259,9 +260,9 @@ testDelistedOwnerLeaves tmp =
|
||||
|
||||
testDelistedOwnerRemoved :: HasCallStack => FilePath -> IO ()
|
||||
testDelistedOwnerRemoved tmp =
|
||||
withDirectoryService tmp $ \superUser dsLink ->
|
||||
withNewTestChat tmp "bob" bobProfile $ \bob ->
|
||||
withNewTestChat tmp "cath" cathProfile $ \cath -> do
|
||||
withDirectoryServiceCfg tmp testCfgCreateGroupDirect $ \superUser dsLink ->
|
||||
withNewTestChatCfg tmp testCfgCreateGroupDirect "bob" bobProfile $ \bob ->
|
||||
withNewTestChatCfg tmp testCfgCreateGroupDirect "cath" cathProfile $ \cath -> do
|
||||
bob `connectVia` dsLink
|
||||
registerGroup superUser bob "privacy" "Privacy"
|
||||
addCathAsOwner bob cath
|
||||
@ -274,9 +275,9 @@ testDelistedOwnerRemoved tmp =
|
||||
|
||||
testNotDelistedMemberLeaves :: HasCallStack => FilePath -> IO ()
|
||||
testNotDelistedMemberLeaves tmp =
|
||||
withDirectoryService tmp $ \superUser dsLink ->
|
||||
withNewTestChat tmp "bob" bobProfile $ \bob ->
|
||||
withNewTestChat tmp "cath" cathProfile $ \cath -> do
|
||||
withDirectoryServiceCfg tmp testCfgCreateGroupDirect $ \superUser dsLink ->
|
||||
withNewTestChatCfg tmp testCfgCreateGroupDirect "bob" bobProfile $ \bob ->
|
||||
withNewTestChatCfg tmp testCfgCreateGroupDirect "cath" cathProfile $ \cath -> do
|
||||
bob `connectVia` dsLink
|
||||
registerGroup superUser bob "privacy" "Privacy"
|
||||
addCathAsOwner bob cath
|
||||
@ -286,10 +287,10 @@ testNotDelistedMemberLeaves tmp =
|
||||
groupFound cath "privacy"
|
||||
|
||||
testNotDelistedMemberRemoved :: HasCallStack => FilePath -> IO ()
|
||||
testNotDelistedMemberRemoved tmp =
|
||||
withDirectoryService tmp $ \superUser dsLink ->
|
||||
withNewTestChat tmp "bob" bobProfile $ \bob ->
|
||||
withNewTestChat tmp "cath" cathProfile $ \cath -> do
|
||||
testNotDelistedMemberRemoved tmp =
|
||||
withDirectoryServiceCfg tmp testCfgCreateGroupDirect $ \superUser dsLink ->
|
||||
withNewTestChatCfg tmp testCfgCreateGroupDirect "bob" bobProfile $ \bob ->
|
||||
withNewTestChatCfg tmp testCfgCreateGroupDirect "cath" cathProfile $ \cath -> do
|
||||
bob `connectVia` dsLink
|
||||
registerGroup superUser bob "privacy" "Privacy"
|
||||
addCathAsOwner bob cath
|
||||
@ -299,9 +300,9 @@ testNotDelistedMemberRemoved tmp =
|
||||
|
||||
testDelistedServiceRemoved :: HasCallStack => FilePath -> IO ()
|
||||
testDelistedServiceRemoved tmp =
|
||||
withDirectoryService tmp $ \superUser dsLink ->
|
||||
withNewTestChat tmp "bob" bobProfile $ \bob ->
|
||||
withNewTestChat tmp "cath" cathProfile $ \cath -> do
|
||||
withDirectoryServiceCfg tmp testCfgCreateGroupDirect $ \superUser dsLink ->
|
||||
withNewTestChatCfg tmp testCfgCreateGroupDirect "bob" bobProfile $ \bob ->
|
||||
withNewTestChatCfg tmp testCfgCreateGroupDirect "cath" cathProfile $ \cath -> do
|
||||
bob `connectVia` dsLink
|
||||
registerGroup superUser bob "privacy" "Privacy"
|
||||
addCathAsOwner bob cath
|
||||
@ -316,9 +317,9 @@ testDelistedServiceRemoved tmp =
|
||||
|
||||
testDelistedRoleChanges :: HasCallStack => FilePath -> IO ()
|
||||
testDelistedRoleChanges tmp =
|
||||
withDirectoryService tmp $ \superUser dsLink ->
|
||||
withNewTestChat tmp "bob" bobProfile $ \bob ->
|
||||
withNewTestChat tmp "cath" cathProfile $ \cath -> do
|
||||
withDirectoryServiceCfg tmp testCfgCreateGroupDirect $ \superUser dsLink ->
|
||||
withNewTestChatCfg tmp testCfgCreateGroupDirect "bob" bobProfile $ \bob ->
|
||||
withNewTestChatCfg tmp testCfgCreateGroupDirect "cath" cathProfile $ \cath -> do
|
||||
bob `connectVia` dsLink
|
||||
registerGroup superUser bob "privacy" "Privacy"
|
||||
addCathAsOwner bob cath
|
||||
@ -362,9 +363,9 @@ testDelistedRoleChanges tmp =
|
||||
|
||||
testNotDelistedMemberRoleChanged :: HasCallStack => FilePath -> IO ()
|
||||
testNotDelistedMemberRoleChanged tmp =
|
||||
withDirectoryService tmp $ \superUser dsLink ->
|
||||
withNewTestChat tmp "bob" bobProfile $ \bob ->
|
||||
withNewTestChat tmp "cath" cathProfile $ \cath -> do
|
||||
withDirectoryServiceCfg tmp testCfgCreateGroupDirect $ \superUser dsLink ->
|
||||
withNewTestChatCfg tmp testCfgCreateGroupDirect "bob" bobProfile $ \bob ->
|
||||
withNewTestChatCfg tmp testCfgCreateGroupDirect "cath" cathProfile $ \cath -> do
|
||||
bob `connectVia` dsLink
|
||||
registerGroup superUser bob "privacy" "Privacy"
|
||||
addCathAsOwner bob cath
|
||||
@ -426,9 +427,9 @@ testNotApprovedBadRoles tmp =
|
||||
|
||||
testRegOwnerChangedProfile :: HasCallStack => FilePath -> IO ()
|
||||
testRegOwnerChangedProfile tmp =
|
||||
withDirectoryService tmp $ \superUser dsLink ->
|
||||
withNewTestChat tmp "bob" bobProfile $ \bob ->
|
||||
withNewTestChat tmp "cath" cathProfile $ \cath -> do
|
||||
withDirectoryServiceCfg tmp testCfgCreateGroupDirect $ \superUser dsLink ->
|
||||
withNewTestChatCfg tmp testCfgCreateGroupDirect "bob" bobProfile $ \bob ->
|
||||
withNewTestChatCfg tmp testCfgCreateGroupDirect "cath" cathProfile $ \cath -> do
|
||||
bob `connectVia` dsLink
|
||||
registerGroup superUser bob "privacy" "Privacy"
|
||||
addCathAsOwner bob cath
|
||||
@ -445,9 +446,9 @@ testRegOwnerChangedProfile tmp =
|
||||
|
||||
testAnotherOwnerChangedProfile :: HasCallStack => FilePath -> IO ()
|
||||
testAnotherOwnerChangedProfile tmp =
|
||||
withDirectoryService tmp $ \superUser dsLink ->
|
||||
withNewTestChat tmp "bob" bobProfile $ \bob ->
|
||||
withNewTestChat tmp "cath" cathProfile $ \cath -> do
|
||||
withDirectoryServiceCfg tmp testCfgCreateGroupDirect $ \superUser dsLink ->
|
||||
withNewTestChatCfg tmp testCfgCreateGroupDirect "bob" bobProfile $ \bob ->
|
||||
withNewTestChatCfg tmp testCfgCreateGroupDirect "cath" cathProfile $ \cath -> do
|
||||
bob `connectVia` dsLink
|
||||
registerGroup superUser bob "privacy" "Privacy"
|
||||
addCathAsOwner bob cath
|
||||
@ -464,9 +465,9 @@ testAnotherOwnerChangedProfile tmp =
|
||||
|
||||
testRegOwnerRemovedLink :: HasCallStack => FilePath -> IO ()
|
||||
testRegOwnerRemovedLink tmp =
|
||||
withDirectoryService tmp $ \superUser dsLink ->
|
||||
withNewTestChat tmp "bob" bobProfile $ \bob ->
|
||||
withNewTestChat tmp "cath" cathProfile $ \cath -> do
|
||||
withDirectoryServiceCfg tmp testCfgCreateGroupDirect $ \superUser dsLink ->
|
||||
withNewTestChatCfg tmp testCfgCreateGroupDirect "bob" bobProfile $ \bob ->
|
||||
withNewTestChatCfg tmp testCfgCreateGroupDirect "cath" cathProfile $ \cath -> do
|
||||
bob `connectVia` dsLink
|
||||
registerGroup superUser bob "privacy" "Privacy"
|
||||
addCathAsOwner bob cath
|
||||
@ -497,9 +498,9 @@ testRegOwnerRemovedLink tmp =
|
||||
|
||||
testAnotherOwnerRemovedLink :: HasCallStack => FilePath -> IO ()
|
||||
testAnotherOwnerRemovedLink tmp =
|
||||
withDirectoryService tmp $ \superUser dsLink ->
|
||||
withNewTestChat tmp "bob" bobProfile $ \bob ->
|
||||
withNewTestChat tmp "cath" cathProfile $ \cath -> do
|
||||
withDirectoryServiceCfg tmp testCfgCreateGroupDirect $ \superUser dsLink ->
|
||||
withNewTestChatCfg tmp testCfgCreateGroupDirect "bob" bobProfile $ \bob ->
|
||||
withNewTestChatCfg tmp testCfgCreateGroupDirect "cath" cathProfile $ \cath -> do
|
||||
bob `connectVia` dsLink
|
||||
registerGroup superUser bob "privacy" "Privacy"
|
||||
addCathAsOwner bob cath
|
||||
@ -646,9 +647,9 @@ testDuplicateProhibitApproval tmp =
|
||||
|
||||
testListUserGroups :: HasCallStack => FilePath -> IO ()
|
||||
testListUserGroups tmp =
|
||||
withDirectoryService tmp $ \superUser dsLink ->
|
||||
withNewTestChat tmp "bob" bobProfile $ \bob ->
|
||||
withNewTestChat tmp "cath" cathProfile $ \cath -> do
|
||||
withDirectoryServiceCfg tmp testCfgCreateGroupDirect $ \superUser dsLink ->
|
||||
withNewTestChatCfg tmp testCfgCreateGroupDirect "bob" bobProfile $ \bob ->
|
||||
withNewTestChatCfg tmp testCfgCreateGroupDirect "cath" cathProfile $ \cath -> do
|
||||
bob `connectVia` dsLink
|
||||
cath `connectVia` dsLink
|
||||
registerGroup superUser bob "privacy" "Privacy"
|
||||
@ -686,15 +687,15 @@ testRestoreDirectory tmp = do
|
||||
withTestChat tmp "bob" $ \bob ->
|
||||
withTestChat tmp "cath" $ \cath -> do
|
||||
bob <## "2 contacts connected (use /cs for the list)"
|
||||
bob <###
|
||||
[ "#privacy (Privacy): connected to server(s)",
|
||||
"#security (Security): connected to server(s)"
|
||||
]
|
||||
bob
|
||||
<### [ "#privacy (Privacy): connected to server(s)",
|
||||
"#security (Security): connected to server(s)"
|
||||
]
|
||||
cath <## "2 contacts connected (use /cs for the list)"
|
||||
cath <###
|
||||
[ "#privacy (Privacy): connected to server(s)",
|
||||
"#anonymity (Anonymity): connected to server(s)"
|
||||
]
|
||||
cath
|
||||
<### [ "#privacy (Privacy): connected to server(s)",
|
||||
"#anonymity (Anonymity): connected to server(s)"
|
||||
]
|
||||
listGroups superUser bob cath
|
||||
groupFoundN 3 bob "privacy"
|
||||
groupFound bob "security"
|
||||
@ -784,14 +785,17 @@ addCathAsOwner bob cath = do
|
||||
cath <## "#privacy: member SimpleX-Directory is connected"
|
||||
|
||||
withDirectoryService :: HasCallStack => FilePath -> (TestCC -> String -> IO ()) -> IO ()
|
||||
withDirectoryService tmp test = do
|
||||
withDirectoryService tmp = withDirectoryServiceCfg tmp testCfg
|
||||
|
||||
withDirectoryServiceCfg :: HasCallStack => FilePath -> ChatConfig -> (TestCC -> String -> IO ()) -> IO ()
|
||||
withDirectoryServiceCfg tmp cfg test = do
|
||||
dsLink <-
|
||||
withNewTestChat tmp serviceDbPrefix directoryProfile $ \ds ->
|
||||
withNewTestChat tmp "super_user" aliceProfile $ \superUser -> do
|
||||
withNewTestChatCfg tmp cfg serviceDbPrefix directoryProfile $ \ds ->
|
||||
withNewTestChatCfg tmp cfg "super_user" aliceProfile $ \superUser -> do
|
||||
connectUsers ds superUser
|
||||
ds ##> "/ad"
|
||||
getContactLink ds True
|
||||
withDirectory tmp dsLink test
|
||||
withDirectory tmp cfg dsLink test
|
||||
|
||||
restoreDirectoryService :: HasCallStack => FilePath -> Int -> Int -> (TestCC -> String -> IO ()) -> IO ()
|
||||
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 <## "Your address is active! To show: /sa"
|
||||
ds <## (show grCount <> " group links active")
|
||||
forM_ [1..grCount] $ \_ -> ds <##. "#"
|
||||
forM_ [1 .. grCount] $ \_ -> ds <##. "#"
|
||||
ds ##> "/sa"
|
||||
dsLink <- getContactLink ds False
|
||||
ds <## "auto_accept on"
|
||||
pure dsLink
|
||||
withDirectory tmp dsLink test
|
||||
withDirectory tmp testCfg dsLink test
|
||||
|
||||
withDirectory :: HasCallStack => FilePath -> String -> (TestCC -> String -> IO ()) -> IO ()
|
||||
withDirectory tmp dsLink test = do
|
||||
withDirectory :: HasCallStack => FilePath -> ChatConfig -> String -> (TestCC -> String -> IO ()) -> IO ()
|
||||
withDirectory tmp cfg dsLink test = do
|
||||
let opts = mkDirectoryOpts tmp [KnownContact 2 "alice"]
|
||||
runDirectory opts $
|
||||
withTestChat tmp "super_user" $ \superUser -> do
|
||||
runDirectory cfg opts $
|
||||
withTestChatCfg tmp cfg "super_user" $ \superUser -> do
|
||||
superUser <## "1 contacts connected (use /cs for the list)"
|
||||
test superUser dsLink
|
||||
|
||||
runDirectory :: DirectoryOpts -> IO () -> IO ()
|
||||
runDirectory opts@DirectoryOpts {directoryLog} action = do
|
||||
runDirectory :: ChatConfig -> DirectoryOpts -> IO () -> IO ()
|
||||
runDirectory cfg opts@DirectoryOpts {directoryLog} action = do
|
||||
st <- restoreDirectoryStore directoryLog
|
||||
t <- forkIO $ bot st
|
||||
threadDelay 500000
|
||||
action `finally` (mapM_ hClose (directoryLogFile st) >> killThread t)
|
||||
where
|
||||
bot st = simplexChatCore testCfg (mkChatOpts opts) Nothing $ directoryService st opts
|
||||
bot st = simplexChatCore cfg (mkChatOpts opts) Nothing $ directoryService st opts
|
||||
|
||||
registerGroup :: TestCC -> TestCC -> String -> String -> IO ()
|
||||
registerGroup su u n fn = registerGroupId su u n fn 1 1
|
||||
|
@ -133,6 +133,16 @@ testAgentCfgV1 =
|
||||
testCfgV1 :: ChatConfig
|
||||
testCfgV1 = testCfg {agentConfig = testAgentCfgV1}
|
||||
|
||||
testCfgCreateGroupDirect :: ChatConfig
|
||||
testCfgCreateGroupDirect =
|
||||
mkCfgCreateGroupDirect testCfg
|
||||
|
||||
mkCfgCreateGroupDirect :: ChatConfig -> ChatConfig
|
||||
mkCfgCreateGroupDirect cfg = cfg {chatVRange = groupCreateDirectVRange}
|
||||
|
||||
groupCreateDirectVRange :: VersionRange
|
||||
groupCreateDirectVRange = mkVersionRange 1 1
|
||||
|
||||
createTestChat :: FilePath -> ChatConfig -> ChatOpts -> String -> Profile -> IO TestCC
|
||||
createTestChat tmp cfg opts@ChatOpts {coreOptions = CoreChatOpts {dbKey}} dbPrefix profile = do
|
||||
Right db@ChatDatabase {chatStore} <- createChatDatabase (tmp </> dbPrefix) dbKey MCError
|
||||
@ -288,7 +298,10 @@ testChatCfgOpts3 cfg opts p1 p2 p3 test = testChatN cfg opts [p1, p2, p3] test_
|
||||
test_ _ = error "expected 3 chat clients"
|
||||
|
||||
testChat4 :: HasCallStack => Profile -> Profile -> Profile -> Profile -> (HasCallStack => TestCC -> TestCC -> TestCC -> TestCC -> IO ()) -> FilePath -> IO ()
|
||||
testChat4 p1 p2 p3 p4 test = testChatN testCfg testOpts [p1, p2, p3, p4] test_
|
||||
testChat4 = testChatCfg4 testCfg
|
||||
|
||||
testChatCfg4 :: HasCallStack => ChatConfig -> Profile -> Profile -> Profile -> Profile -> (HasCallStack => TestCC -> TestCC -> TestCC -> TestCC -> IO ()) -> FilePath -> IO ()
|
||||
testChatCfg4 cfg p1 p2 p3 p4 test = testChatN cfg testOpts [p1, p2, p3, p4] test_
|
||||
where
|
||||
test_ :: HasCallStack => [TestCC] -> IO ()
|
||||
test_ [tc1, tc2, tc3, tc4] = test tc1 tc2 tc3 tc4
|
||||
|
@ -17,9 +17,11 @@ import qualified Data.ByteString.Lazy.Char8 as LB
|
||||
import Simplex.Chat.Call
|
||||
import Simplex.Chat.Controller (ChatConfig (..))
|
||||
import Simplex.Chat.Options (ChatOpts (..))
|
||||
import Simplex.Chat.Protocol (supportedChatVRange)
|
||||
import Simplex.Chat.Store (agentStoreFile, chatStoreFile)
|
||||
import Simplex.Chat.Types (authErrDisableCount, sameVerificationCode, verificationCode)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Version
|
||||
import System.Directory (copyFile, doesDirectoryExist, doesFileExist)
|
||||
import System.FilePath ((</>))
|
||||
import Test.Hspec
|
||||
@ -94,6 +96,21 @@ chatDirectTests = do
|
||||
describe "delivery receipts" $ do
|
||||
it "should send delivery receipts" testSendDeliveryReceipts
|
||||
it "should send delivery receipts depending on configuration" testConfigureDeliveryReceipts
|
||||
describe "negotiate connection 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 = versionTestMatrix2 runTestAddContact
|
||||
@ -1939,8 +1956,7 @@ testMarkContactVerified =
|
||||
testChat2 aliceProfile bobProfile $ \alice bob -> do
|
||||
connectUsers alice bob
|
||||
alice ##> "/i bob"
|
||||
bobInfo alice
|
||||
alice <## "connection not verified, use /code command to see security code"
|
||||
bobInfo alice False
|
||||
alice ##> "/code bob"
|
||||
bCode <- getTermLine alice
|
||||
bob ##> "/code alice"
|
||||
@ -1951,28 +1967,31 @@ testMarkContactVerified =
|
||||
alice ##> ("/verify bob " <> aCode)
|
||||
alice <## "connection verified"
|
||||
alice ##> "/i bob"
|
||||
bobInfo alice
|
||||
alice <## "connection verified"
|
||||
bobInfo alice True
|
||||
alice ##> "/verify bob"
|
||||
alice <##. "connection not verified, current code is "
|
||||
alice ##> "/i bob"
|
||||
bobInfo alice
|
||||
alice <## "connection not verified, use /code command to see security code"
|
||||
bobInfo alice False
|
||||
where
|
||||
bobInfo :: HasCallStack => TestCC -> IO ()
|
||||
bobInfo alice = do
|
||||
bobInfo :: HasCallStack => TestCC -> Bool -> IO ()
|
||||
bobInfo alice verified = do
|
||||
alice <## "contact ID: 2"
|
||||
alice <## "receiving messages via: localhost"
|
||||
alice <## "sending messages via: localhost"
|
||||
alice <## "you've shared main profile with this contact"
|
||||
alice <## connVerified
|
||||
alice <## currentChatVRangeInfo
|
||||
where
|
||||
connVerified
|
||||
| verified = "connection verified"
|
||||
| otherwise = "connection not verified, use /code command to see security code"
|
||||
|
||||
testMarkGroupMemberVerified :: HasCallStack => FilePath -> IO ()
|
||||
testMarkGroupMemberVerified =
|
||||
testChat2 aliceProfile bobProfile $ \alice bob -> do
|
||||
createGroup2 "team" alice bob
|
||||
alice ##> "/i #team bob"
|
||||
bobInfo alice
|
||||
alice <## "connection not verified, use /code command to see security code"
|
||||
bobInfo alice False
|
||||
alice ##> "/code #team bob"
|
||||
bCode <- getTermLine alice
|
||||
bob ##> "/code #team alice"
|
||||
@ -1983,20 +2002,24 @@ testMarkGroupMemberVerified =
|
||||
alice ##> ("/verify #team bob " <> aCode)
|
||||
alice <## "connection verified"
|
||||
alice ##> "/i #team bob"
|
||||
bobInfo alice
|
||||
alice <## "connection verified"
|
||||
bobInfo alice True
|
||||
alice ##> "/verify #team bob"
|
||||
alice <##. "connection not verified, current code is "
|
||||
alice ##> "/i #team bob"
|
||||
bobInfo alice
|
||||
alice <## "connection not verified, use /code command to see security code"
|
||||
bobInfo alice False
|
||||
where
|
||||
bobInfo :: HasCallStack => TestCC -> IO ()
|
||||
bobInfo alice = do
|
||||
bobInfo :: HasCallStack => TestCC -> Bool -> IO ()
|
||||
bobInfo alice verified = do
|
||||
alice <## "group ID: 1"
|
||||
alice <## "member ID: 2"
|
||||
alice <## "receiving messages via: localhost"
|
||||
alice <## "sending messages via: localhost"
|
||||
alice <## connVerified
|
||||
alice <## currentChatVRangeInfo
|
||||
where
|
||||
connVerified
|
||||
| verified = "connection verified"
|
||||
| otherwise = "connection not verified, use /code command to see security code"
|
||||
|
||||
testMsgDecryptError :: HasCallStack => FilePath -> IO ()
|
||||
testMsgDecryptError tmp =
|
||||
@ -2088,8 +2111,7 @@ testSyncRatchetCodeReset tmp =
|
||||
alice <# "bob> hey"
|
||||
-- connection not verified
|
||||
bob ##> "/i alice"
|
||||
aliceInfo bob
|
||||
bob <## "connection not verified, use /code command to see security code"
|
||||
aliceInfo bob False
|
||||
-- verify connection
|
||||
alice ##> "/code bob"
|
||||
bCode <- getTermLine alice
|
||||
@ -2097,8 +2119,7 @@ testSyncRatchetCodeReset tmp =
|
||||
bob <## "connection verified"
|
||||
-- connection verified
|
||||
bob ##> "/i alice"
|
||||
aliceInfo bob
|
||||
bob <## "connection verified"
|
||||
aliceInfo bob True
|
||||
setupDesynchronizedRatchet tmp alice
|
||||
withTestChat tmp "bob_old" $ \bob -> do
|
||||
bob <## "1 contacts connected (use /cs for the list)"
|
||||
@ -2115,20 +2136,25 @@ testSyncRatchetCodeReset tmp =
|
||||
|
||||
-- connection not verified
|
||||
bob ##> "/i alice"
|
||||
aliceInfo bob
|
||||
bob <## "connection not verified, use /code command to see security code"
|
||||
aliceInfo bob False
|
||||
|
||||
alice #> "@bob hello again"
|
||||
bob <# "alice> hello again"
|
||||
bob #> "@alice received!"
|
||||
alice <# "bob> received!"
|
||||
where
|
||||
aliceInfo :: HasCallStack => TestCC -> IO ()
|
||||
aliceInfo bob = do
|
||||
aliceInfo :: HasCallStack => TestCC -> Bool -> IO ()
|
||||
aliceInfo bob verified = do
|
||||
bob <## "contact ID: 2"
|
||||
bob <## "receiving messages via: localhost"
|
||||
bob <## "sending messages via: localhost"
|
||||
bob <## "you've shared main profile with this contact"
|
||||
bob <## connVerified
|
||||
bob <## currentChatVRangeInfo
|
||||
where
|
||||
connVerified
|
||||
| verified = "connection verified"
|
||||
| otherwise = "connection not verified, use /code command to see security code"
|
||||
|
||||
testSetMessageReactions :: HasCallStack => FilePath -> IO ()
|
||||
testSetMessageReactions =
|
||||
@ -2271,3 +2297,85 @@ testConfigureDeliveryReceipts tmp =
|
||||
cc1 #> ("@" <> name2 <> " " <> msg)
|
||||
cc2 <# (name1 <> "> " <> msg)
|
||||
cc1 <// 50000
|
||||
|
||||
testConnInvChatVRange :: HasCallStack => VersionRange -> VersionRange -> FilePath -> IO ()
|
||||
testConnInvChatVRange ct1VRange ct2VRange tmp =
|
||||
withNewTestChatCfg tmp testCfg {chatVRange = ct1VRange} "alice" aliceProfile $ \alice -> do
|
||||
withNewTestChatCfg tmp testCfg {chatVRange = ct2VRange} "bob" bobProfile $ \bob -> do
|
||||
connectUsers alice bob
|
||||
|
||||
alice ##> "/i bob"
|
||||
contactInfoChatVRange alice ct2VRange
|
||||
|
||||
bob ##> "/i alice"
|
||||
contactInfoChatVRange bob ct1VRange
|
||||
|
||||
testConnReqChatVRange :: HasCallStack => VersionRange -> VersionRange -> FilePath -> IO ()
|
||||
testConnReqChatVRange ct1VRange ct2VRange tmp =
|
||||
withNewTestChatCfg tmp testCfg {chatVRange = ct1VRange} "alice" aliceProfile $ \alice -> do
|
||||
withNewTestChatCfg tmp testCfg {chatVRange = ct2VRange} "bob" bobProfile $ \bob -> do
|
||||
alice ##> "/ad"
|
||||
cLink <- getContactLink alice True
|
||||
bob ##> ("/c " <> cLink)
|
||||
alice <#? bob
|
||||
alice ##> "/ac bob"
|
||||
alice <## "bob (Bob): accepting contact request..."
|
||||
concurrently_
|
||||
(bob <## "alice (Alice): contact is connected")
|
||||
(alice <## "bob (Bob): contact is connected")
|
||||
|
||||
alice ##> "/i bob"
|
||||
contactInfoChatVRange alice ct2VRange
|
||||
|
||||
bob ##> "/i alice"
|
||||
contactInfoChatVRange bob ct1VRange
|
||||
|
||||
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 <> ")")
|
||||
|
@ -51,7 +51,7 @@ chatFileTests = do
|
||||
it "files folder: sender deleted file during transfer" testFilesFoldersImageSndDelete
|
||||
it "files folder: recipient deleted file during transfer" testFilesFoldersImageRcvDelete
|
||||
it "send and receive image with text and quote" testSendImageWithTextAndQuote
|
||||
describe "send and receive image to group" testGroupSendImage
|
||||
it "send and receive image to group" testGroupSendImage
|
||||
it "send and receive image with text and quote to group" testGroupSendImageWithTextAndQuote
|
||||
describe "async sending and receiving files" $ do
|
||||
-- fails on CI
|
||||
@ -730,11 +730,10 @@ testSendImageWithTextAndQuote =
|
||||
(alice <## "completed sending file 3 (test.jpg) to bob")
|
||||
B.readFile "./tests/tmp/test_1.jpg" `shouldReturn` src
|
||||
|
||||
testGroupSendImage :: SpecWith FilePath
|
||||
testGroupSendImage = versionTestMatrix3 runTestGroupSendImage
|
||||
where
|
||||
runTestGroupSendImage :: HasCallStack => TestCC -> TestCC -> TestCC -> IO ()
|
||||
runTestGroupSendImage alice bob cath = do
|
||||
testGroupSendImage :: HasCallStack => FilePath -> IO ()
|
||||
testGroupSendImage =
|
||||
testChat3 aliceProfile bobProfile cathProfile $
|
||||
\alice bob cath -> do
|
||||
createGroup3 "team" alice bob cath
|
||||
threadDelay 1000000
|
||||
alice ##> "/_send #1 json {\"filePath\": \"./tests/fixtures/test.jpg\", \"msgContent\": {\"text\":\"\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}"
|
||||
|
@ -10,8 +10,10 @@ import Control.Concurrent.Async (concurrently_)
|
||||
import Control.Monad (when)
|
||||
import qualified Data.Text as T
|
||||
import Simplex.Chat.Controller (ChatConfig (..))
|
||||
import Simplex.Chat.Protocol (supportedChatVRange)
|
||||
import Simplex.Chat.Store (agentStoreFile, chatStoreFile)
|
||||
import Simplex.Chat.Types (GroupMemberRole (..))
|
||||
import Simplex.Messaging.Version
|
||||
import System.Directory (copyFile)
|
||||
import System.FilePath ((</>))
|
||||
import Test.Hspec
|
||||
@ -19,7 +21,7 @@ import Test.Hspec
|
||||
chatGroupTests :: SpecWith FilePath
|
||||
chatGroupTests = do
|
||||
describe "chat groups" $ do
|
||||
describe "add contacts, create group and send/receive messages" testGroup
|
||||
it "add contacts, create group and send/receive messages" testGroup
|
||||
it "add contacts, create group and send/receive messages, check messages" testGroupCheckMessages
|
||||
it "create and join group with 4 members" testGroup2
|
||||
it "create and delete group" testGroupDelete
|
||||
@ -64,15 +66,54 @@ chatGroupTests = do
|
||||
describe "group delivery receipts" $ do
|
||||
it "should send delivery receipts in group" testSendGroupDeliveryReceipts
|
||||
it "should send delivery receipts in group depending on configuration" testConfigureGroupDeliveryReceipts
|
||||
|
||||
testGroup :: HasCallStack => SpecWith FilePath
|
||||
testGroup = versionTestMatrix3 runTestGroup
|
||||
describe "direct connections in group are not established based on chat protocol version" $ do
|
||||
describe "3 members group" $ do
|
||||
testNoDirect _0 _0 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
|
||||
runTestGroup alice bob cath = testGroupShared alice bob cath False
|
||||
_0 = supportedChatVRange -- don't create direct connections
|
||||
_1 = groupCreateDirectVRange
|
||||
-- having host configured with older version doesn't have effect in tests
|
||||
-- because host uses current code and sends version in MemberInfo
|
||||
testNoDirect vrMem2 vrMem3 noConns =
|
||||
it
|
||||
( "host " <> vRangeStr supportedChatVRange
|
||||
<> (", 2nd mem " <> vRangeStr vrMem2)
|
||||
<> (", 3rd mem " <> vRangeStr vrMem3)
|
||||
<> (if noConns then " : 2 <!!> 3" else " : 2 <##> 3")
|
||||
)
|
||||
$ testNoGroupDirectConns supportedChatVRange vrMem2 vrMem3 noConns
|
||||
testNoDirect4 vrMem2 vrMem3 vrMem4 noConns23 noConns24 noConns34 =
|
||||
it
|
||||
( "host " <> vRangeStr supportedChatVRange
|
||||
<> (", 2nd mem " <> vRangeStr vrMem2)
|
||||
<> (", 3rd mem " <> vRangeStr vrMem3)
|
||||
<> (", 4th mem " <> vRangeStr vrMem4)
|
||||
<> (if noConns23 then " : 2 <!!> 3" else " : 2 <##> 3")
|
||||
<> (if noConns24 then " : 2 <!!> 4" else " : 2 <##> 4")
|
||||
<> (if noConns34 then " : 3 <!!> 4" else " : 3 <##> 4")
|
||||
)
|
||||
$ testNoGroupDirectConns4Members supportedChatVRange vrMem2 vrMem3 vrMem4 noConns23 noConns24 noConns34
|
||||
|
||||
testGroup :: HasCallStack => FilePath -> IO ()
|
||||
testGroup =
|
||||
testChatCfg3 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile $
|
||||
\alice bob cath -> testGroupShared alice bob cath False
|
||||
|
||||
testGroupCheckMessages :: HasCallStack => FilePath -> IO ()
|
||||
testGroupCheckMessages =
|
||||
testChat3 aliceProfile bobProfile cathProfile $
|
||||
testChatCfg3 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile $
|
||||
\alice bob cath -> testGroupShared alice bob cath True
|
||||
|
||||
testGroupShared :: HasCallStack => TestCC -> TestCC -> TestCC -> Bool -> IO ()
|
||||
@ -233,7 +274,7 @@ testGroupShared alice bob cath checkMessages = do
|
||||
|
||||
testGroup2 :: HasCallStack => FilePath -> IO ()
|
||||
testGroup2 =
|
||||
testChat4 aliceProfile bobProfile cathProfile danProfile $
|
||||
testChatCfg4 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile danProfile $
|
||||
\alice bob cath dan -> do
|
||||
connectUsers alice bob
|
||||
connectUsers alice cath
|
||||
@ -679,7 +720,7 @@ testDeleteGroupMemberProfileKept =
|
||||
|
||||
testGroupRemoveAdd :: HasCallStack => FilePath -> IO ()
|
||||
testGroupRemoveAdd =
|
||||
testChat3 aliceProfile bobProfile cathProfile $
|
||||
testChatCfg3 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile $
|
||||
\alice bob cath -> do
|
||||
createGroup3 "team" alice bob cath
|
||||
-- remove member
|
||||
@ -754,7 +795,7 @@ testGroupList =
|
||||
|
||||
testGroupMessageQuotedReply :: HasCallStack => FilePath -> IO ()
|
||||
testGroupMessageQuotedReply =
|
||||
testChat3 aliceProfile bobProfile cathProfile $
|
||||
testChatCfg3 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile $
|
||||
\alice bob cath -> do
|
||||
createGroup3 "team" alice bob cath
|
||||
threadDelay 1000000
|
||||
@ -1232,7 +1273,7 @@ testGroupDeleteUnusedContacts =
|
||||
cath <## "alice (Alice)"
|
||||
cath `hasContactProfiles` ["alice", "cath"]
|
||||
where
|
||||
cfg = testCfg {initialCleanupManagerDelay = 0, cleanupManagerInterval = 1, cleanupManagerStepDelay = 0}
|
||||
cfg = mkCfgCreateGroupDirect $ testCfg {initialCleanupManagerDelay = 0, cleanupManagerInterval = 1, cleanupManagerStepDelay = 0}
|
||||
deleteGroup :: HasCallStack => TestCC -> TestCC -> TestCC -> String -> IO ()
|
||||
deleteGroup alice bob cath group = do
|
||||
alice ##> ("/d #" <> group)
|
||||
@ -1321,7 +1362,7 @@ testGroupDescription = testChat4 aliceProfile bobProfile cathProfile danProfile
|
||||
|
||||
testGroupModerate :: HasCallStack => FilePath -> IO ()
|
||||
testGroupModerate =
|
||||
testChat3 aliceProfile bobProfile cathProfile $
|
||||
testChatCfg3 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile $
|
||||
\alice bob cath -> do
|
||||
createGroup3 "team" alice bob cath
|
||||
alice ##> "/mr team cath member"
|
||||
@ -1352,7 +1393,7 @@ testGroupModerate =
|
||||
|
||||
testGroupModerateFullDelete :: HasCallStack => FilePath -> IO ()
|
||||
testGroupModerateFullDelete =
|
||||
testChat3 aliceProfile bobProfile cathProfile $
|
||||
testChatCfg3 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile $
|
||||
\alice bob cath -> do
|
||||
createGroup3 "team" alice bob cath
|
||||
alice ##> "/mr team cath member"
|
||||
@ -1390,10 +1431,10 @@ testGroupModerateFullDelete =
|
||||
|
||||
testGroupDelayedModeration :: HasCallStack => FilePath -> IO ()
|
||||
testGroupDelayedModeration tmp = do
|
||||
withNewTestChat tmp "alice" aliceProfile $ \alice -> do
|
||||
withNewTestChat tmp "bob" bobProfile $ \bob -> do
|
||||
withNewTestChatCfg tmp cfg "alice" aliceProfile $ \alice -> do
|
||||
withNewTestChatCfg tmp cfg "bob" bobProfile $ \bob -> do
|
||||
createGroup2 "team" alice bob
|
||||
withNewTestChat tmp "cath" cathProfile $ \cath -> do
|
||||
withNewTestChatCfg tmp cfg "cath" cathProfile $ \cath -> do
|
||||
connectUsers alice cath
|
||||
addMember "team" alice cath GRMember
|
||||
cath ##> "/j team"
|
||||
@ -1407,11 +1448,11 @@ testGroupDelayedModeration tmp = do
|
||||
alice ##> "\\\\ #team @cath hi"
|
||||
alice <## "message marked deleted by you"
|
||||
cath <# "#team cath> [marked deleted by alice] hi"
|
||||
withTestChat tmp "bob" $ \bob -> do
|
||||
withTestChatCfg tmp cfg "bob" $ \bob -> do
|
||||
bob <## "1 contacts connected (use /cs for the list)"
|
||||
bob <## "#team: connected to server(s)"
|
||||
bob <## "#team: alice added cath (Catherine) to the group (connecting...)"
|
||||
withTestChat tmp "cath" $ \cath -> do
|
||||
withTestChatCfg tmp cfg "cath" $ \cath -> do
|
||||
cath <## "2 contacts connected (use /cs for the list)"
|
||||
cath <## "#team: connected to server(s)"
|
||||
cath <## "#team: member bob (Bob) is connected"
|
||||
@ -1424,13 +1465,15 @@ testGroupDelayedModeration tmp = do
|
||||
bob ##> "/_get chat #1 count=2"
|
||||
r <- chat <$> getTermLine bob
|
||||
r `shouldMatchList` [(0, "connected"), (0, "hi [marked deleted by alice]")]
|
||||
where
|
||||
cfg = testCfgCreateGroupDirect
|
||||
|
||||
testGroupDelayedModerationFullDelete :: HasCallStack => FilePath -> IO ()
|
||||
testGroupDelayedModerationFullDelete tmp = do
|
||||
withNewTestChat tmp "alice" aliceProfile $ \alice -> do
|
||||
withNewTestChat tmp "bob" bobProfile $ \bob -> do
|
||||
withNewTestChatCfg tmp cfg "alice" aliceProfile $ \alice -> do
|
||||
withNewTestChatCfg tmp cfg "bob" bobProfile $ \bob -> do
|
||||
createGroup2 "team" alice bob
|
||||
withNewTestChat tmp "cath" cathProfile $ \cath -> do
|
||||
withNewTestChatCfg tmp cfg "cath" cathProfile $ \cath -> do
|
||||
connectUsers alice cath
|
||||
addMember "team" alice cath GRMember
|
||||
cath ##> "/j team"
|
||||
@ -1452,14 +1495,14 @@ testGroupDelayedModerationFullDelete tmp = do
|
||||
cath <## "alice updated group #team:"
|
||||
cath <## "updated group preferences:"
|
||||
cath <## "Full deletion: on"
|
||||
withTestChat tmp "bob" $ \bob -> do
|
||||
withTestChatCfg tmp cfg "bob" $ \bob -> do
|
||||
bob <## "1 contacts connected (use /cs for the list)"
|
||||
bob <## "#team: connected to server(s)"
|
||||
bob <## "#team: alice added cath (Catherine) to the group (connecting...)"
|
||||
bob <## "alice updated group #team:"
|
||||
bob <## "updated group preferences:"
|
||||
bob <## "Full deletion: on"
|
||||
withTestChat tmp "cath" $ \cath -> do
|
||||
withTestChatCfg tmp cfg "cath" $ \cath -> do
|
||||
cath <## "2 contacts connected (use /cs for the list)"
|
||||
cath <## "#team: connected to server(s)"
|
||||
cath <## "#team: member bob (Bob) is connected"
|
||||
@ -1472,6 +1515,8 @@ testGroupDelayedModerationFullDelete tmp = do
|
||||
bob ##> "/_get chat #1 count=3"
|
||||
r <- chat <$> getTermLine bob
|
||||
r `shouldMatchList` [(0, "Full deletion: on"), (0, "connected"), (0, "moderated [deleted by alice]")]
|
||||
where
|
||||
cfg = testCfgCreateGroupDirect
|
||||
|
||||
testGroupAsync :: HasCallStack => FilePath -> IO ()
|
||||
testGroupAsync tmp = do
|
||||
@ -2127,7 +2172,7 @@ testGroupLinkMemberRole =
|
||||
|
||||
testGroupLinkLeaveDelete :: HasCallStack => FilePath -> IO ()
|
||||
testGroupLinkLeaveDelete =
|
||||
testChat3 aliceProfile bobProfile cathProfile $
|
||||
testChatCfg3 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile $
|
||||
\alice bob cath -> do
|
||||
connectUsers alice bob
|
||||
connectUsers cath bob
|
||||
@ -2289,8 +2334,7 @@ testGroupSyncRatchetCodeReset tmp =
|
||||
alice <# "#team bob> hey"
|
||||
-- connection not verified
|
||||
bob ##> "/i #team alice"
|
||||
aliceInfo bob
|
||||
bob <## "connection not verified, use /code command to see security code"
|
||||
aliceInfo bob False
|
||||
-- verify connection
|
||||
alice ##> "/code #team bob"
|
||||
bCode <- getTermLine alice
|
||||
@ -2298,8 +2342,7 @@ testGroupSyncRatchetCodeReset tmp =
|
||||
bob <## "connection verified"
|
||||
-- connection verified
|
||||
bob ##> "/i #team alice"
|
||||
aliceInfo bob
|
||||
bob <## "connection verified"
|
||||
aliceInfo bob True
|
||||
setupDesynchronizedRatchet tmp alice
|
||||
withTestChat tmp "bob_old" $ \bob -> do
|
||||
bob <## "1 contacts connected (use /cs for the list)"
|
||||
@ -2317,20 +2360,25 @@ testGroupSyncRatchetCodeReset tmp =
|
||||
|
||||
-- connection not verified
|
||||
bob ##> "/i #team alice"
|
||||
aliceInfo bob
|
||||
bob <## "connection not verified, use /code command to see security code"
|
||||
aliceInfo bob False
|
||||
|
||||
alice #> "#team hello again"
|
||||
bob <# "#team alice> hello again"
|
||||
bob #> "#team received!"
|
||||
alice <# "#team bob> received!"
|
||||
where
|
||||
aliceInfo :: HasCallStack => TestCC -> IO ()
|
||||
aliceInfo bob = do
|
||||
aliceInfo :: HasCallStack => TestCC -> Bool -> IO ()
|
||||
aliceInfo bob verified = do
|
||||
bob <## "group ID: 1"
|
||||
bob <## "member ID: 1"
|
||||
bob <## "receiving messages via: localhost"
|
||||
bob <## "sending messages via: localhost"
|
||||
bob <## connVerified
|
||||
bob <## currentChatVRangeInfo
|
||||
where
|
||||
connVerified
|
||||
| verified = "connection verified"
|
||||
| otherwise = "connection not verified, use /code command to see security code"
|
||||
|
||||
testSetGroupMessageReactions :: HasCallStack => FilePath -> IO ()
|
||||
testSetGroupMessageReactions =
|
||||
@ -2559,7 +2607,7 @@ testConfigureGroupDeliveryReceipts tmp =
|
||||
receipt bob alice cath "team" "25"
|
||||
noReceipt bob alice cath "club" "26"
|
||||
where
|
||||
cfg = testCfg {showReceipts = True}
|
||||
cfg = mkCfgCreateGroupDirect $ testCfg {showReceipts = True}
|
||||
receipt cc1 cc2 cc3 gName msg = do
|
||||
name1 <- userName cc1
|
||||
cc1 #> ("#" <> gName <> " " <> msg)
|
||||
@ -2579,3 +2627,62 @@ testConfigureGroupDeliveryReceipts tmp =
|
||||
cc2 <# ("#" <> gName <> " " <> name1 <> "> " <> msg)
|
||||
cc3 <# ("#" <> gName <> " " <> name1 <> "> " <> msg)
|
||||
cc1 <// 50000
|
||||
|
||||
testNoGroupDirectConns :: HasCallStack => VersionRange -> VersionRange -> VersionRange -> Bool -> FilePath -> IO ()
|
||||
testNoGroupDirectConns hostVRange mem2VRange mem3VRange noDirectConns tmp =
|
||||
withNewTestChatCfg tmp testCfg {chatVRange = hostVRange} "alice" aliceProfile $ \alice -> do
|
||||
withNewTestChatCfg tmp testCfg {chatVRange = mem2VRange} "bob" bobProfile $ \bob -> do
|
||||
withNewTestChatCfg tmp testCfg {chatVRange = mem3VRange} "cath" cathProfile $ \cath -> do
|
||||
createGroup3 "team" alice bob cath
|
||||
if noDirectConns
|
||||
then contactsDontExist bob cath
|
||||
else bob <##> cath
|
||||
where
|
||||
contactsDontExist bob cath = do
|
||||
bob ##> "@cath hi"
|
||||
bob <## "no contact cath"
|
||||
cath ##> "@bob hi"
|
||||
cath <## "no contact bob"
|
||||
|
||||
testNoGroupDirectConns4Members :: HasCallStack => VersionRange -> VersionRange -> VersionRange -> VersionRange -> Bool -> Bool -> Bool -> FilePath -> IO ()
|
||||
testNoGroupDirectConns4Members hostVRange mem2VRange mem3VRange mem4VRange noConns23 noConns24 noConns34 tmp =
|
||||
withNewTestChatCfg tmp testCfg {chatVRange = hostVRange} "alice" aliceProfile $ \alice -> do
|
||||
withNewTestChatCfg tmp testCfg {chatVRange = mem2VRange} "bob" bobProfile $ \bob -> do
|
||||
withNewTestChatCfg tmp testCfg {chatVRange = mem3VRange} "cath" cathProfile $ \cath -> do
|
||||
withNewTestChatCfg tmp testCfg {chatVRange = mem4VRange} "dan" danProfile $ \dan -> do
|
||||
createGroup3 "team" alice bob cath
|
||||
connectUsers alice dan
|
||||
addMember "team" alice dan GRMember
|
||||
dan ##> "/j team"
|
||||
concurrentlyN_
|
||||
[ alice <## "#team: dan joined the group",
|
||||
do
|
||||
dan <## "#team: you joined the group"
|
||||
dan
|
||||
<### [ "#team: member bob (Bob) is connected",
|
||||
"#team: member cath (Catherine) is connected"
|
||||
],
|
||||
aliceAddedDan bob,
|
||||
aliceAddedDan cath
|
||||
]
|
||||
if noConns23
|
||||
then contactsDontExist bob cath
|
||||
else bob <##> cath
|
||||
if noConns24
|
||||
then contactsDontExist bob dan
|
||||
else bob <##> dan
|
||||
if noConns34
|
||||
then contactsDontExist cath dan
|
||||
else cath <##> dan
|
||||
where
|
||||
aliceAddedDan :: HasCallStack => TestCC -> IO ()
|
||||
aliceAddedDan cc = do
|
||||
cc <## "#team: alice added dan (Daniel) to the group (connecting...)"
|
||||
cc <## "#team: new member dan is connected"
|
||||
contactsDontExist cc1 cc2 = do
|
||||
name1 <- userName cc1
|
||||
name2 <- userName cc2
|
||||
cc1 ##> ("@" <> name2 <> " hi")
|
||||
cc1 <## ("no contact " <> name2)
|
||||
cc2 ##> ("@" <> name1 <> " hi")
|
||||
cc2 <## ("no contact " <> name1)
|
||||
|
@ -18,7 +18,7 @@ chatProfileTests = do
|
||||
it "update user profile and notify contacts" testUpdateProfile
|
||||
it "update user profile with image" testUpdateProfileImage
|
||||
describe "user contact link" $ do
|
||||
describe "create and connect via contact link" testUserContactLink
|
||||
it "create and connect via contact link" testUserContactLink
|
||||
it "add contact link to profile" testProfileLink
|
||||
it "auto accept contact requests" testUserContactLinkAutoAccept
|
||||
it "deduplicate contact requests" testDeduplicateContactRequests
|
||||
@ -57,7 +57,7 @@ chatProfileTests = do
|
||||
|
||||
testUpdateProfile :: HasCallStack => FilePath -> IO ()
|
||||
testUpdateProfile =
|
||||
testChat3 aliceProfile bobProfile cathProfile $
|
||||
testChatCfg3 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile $
|
||||
\alice bob cath -> do
|
||||
createGroup3 "team" alice bob cath
|
||||
alice ##> "/p"
|
||||
@ -117,33 +117,35 @@ testUpdateProfileImage =
|
||||
bob <## "use @alice2 <message> to send messages"
|
||||
(bob </)
|
||||
|
||||
testUserContactLink :: SpecWith FilePath
|
||||
testUserContactLink = versionTestMatrix3 $ \alice bob cath -> do
|
||||
alice ##> "/ad"
|
||||
cLink <- getContactLink alice True
|
||||
bob ##> ("/c " <> cLink)
|
||||
alice <#? bob
|
||||
alice @@@ [("<@bob", "")]
|
||||
alice ##> "/ac bob"
|
||||
alice <## "bob (Bob): accepting contact request..."
|
||||
concurrently_
|
||||
(bob <## "alice (Alice): contact is connected")
|
||||
(alice <## "bob (Bob): contact is connected")
|
||||
threadDelay 100000
|
||||
alice @@@ [("@bob", lastChatFeature)]
|
||||
alice <##> bob
|
||||
testUserContactLink :: HasCallStack => FilePath -> IO ()
|
||||
testUserContactLink =
|
||||
testChat3 aliceProfile bobProfile cathProfile $
|
||||
\alice bob cath -> do
|
||||
alice ##> "/ad"
|
||||
cLink <- getContactLink alice True
|
||||
bob ##> ("/c " <> cLink)
|
||||
alice <#? bob
|
||||
alice @@@ [("<@bob", "")]
|
||||
alice ##> "/ac bob"
|
||||
alice <## "bob (Bob): accepting contact request..."
|
||||
concurrently_
|
||||
(bob <## "alice (Alice): contact is connected")
|
||||
(alice <## "bob (Bob): contact is connected")
|
||||
threadDelay 100000
|
||||
alice @@@ [("@bob", lastChatFeature)]
|
||||
alice <##> bob
|
||||
|
||||
cath ##> ("/c " <> cLink)
|
||||
alice <#? cath
|
||||
alice @@@ [("<@cath", ""), ("@bob", "hey")]
|
||||
alice ##> "/ac cath"
|
||||
alice <## "cath (Catherine): accepting contact request..."
|
||||
concurrently_
|
||||
(cath <## "alice (Alice): contact is connected")
|
||||
(alice <## "cath (Catherine): contact is connected")
|
||||
threadDelay 100000
|
||||
alice @@@ [("@cath", lastChatFeature), ("@bob", "hey")]
|
||||
alice <##> cath
|
||||
cath ##> ("/c " <> cLink)
|
||||
alice <#? cath
|
||||
alice @@@ [("<@cath", ""), ("@bob", "hey")]
|
||||
alice ##> "/ac cath"
|
||||
alice <## "cath (Catherine): accepting contact request..."
|
||||
concurrently_
|
||||
(cath <## "alice (Alice): contact is connected")
|
||||
(alice <## "cath (Catherine): contact is connected")
|
||||
threadDelay 100000
|
||||
alice @@@ [("@cath", lastChatFeature), ("@bob", "hey")]
|
||||
alice <##> cath
|
||||
|
||||
testProfileLink :: HasCallStack => FilePath -> IO ()
|
||||
testProfileLink =
|
||||
@ -214,6 +216,7 @@ testProfileLink =
|
||||
cc <## ("contact address: " <> cLink)
|
||||
cc <## "you've shared main profile with this contact"
|
||||
cc <## "connection not verified, use /code command to see security code"
|
||||
cc <## currentChatVRangeInfo
|
||||
checkAliceNoProfileLink cc = do
|
||||
cc ##> "/info alice"
|
||||
cc <## "contact ID: 2"
|
||||
@ -221,6 +224,7 @@ testProfileLink =
|
||||
cc <##. "sending messages via"
|
||||
cc <## "you've shared main profile with this contact"
|
||||
cc <## "connection not verified, use /code command to see security code"
|
||||
cc <## currentChatVRangeInfo
|
||||
|
||||
testUserContactLinkAutoAccept :: HasCallStack => FilePath -> IO ()
|
||||
testUserContactLinkAutoAccept =
|
||||
@ -760,192 +764,193 @@ testSetResetSetConnectionIncognito = testChat2 aliceProfile bobProfile $
|
||||
bob `hasContactProfiles` ["bob", T.pack aliceIncognito]
|
||||
|
||||
testJoinGroupIncognito :: HasCallStack => FilePath -> IO ()
|
||||
testJoinGroupIncognito = testChat4 aliceProfile bobProfile cathProfile danProfile $
|
||||
\alice bob cath dan -> do
|
||||
-- non incognito connections
|
||||
connectUsers alice bob
|
||||
connectUsers alice dan
|
||||
connectUsers bob cath
|
||||
connectUsers bob dan
|
||||
connectUsers cath dan
|
||||
-- cath connected incognito to alice
|
||||
alice ##> "/c"
|
||||
inv <- getInvitation alice
|
||||
cath ##> ("/c i " <> inv)
|
||||
cath <## "confirmation sent!"
|
||||
cathIncognito <- getTermLine cath
|
||||
concurrentlyN_
|
||||
[ do
|
||||
cath <## ("alice (Alice): contact is connected, your incognito profile for this contact is " <> cathIncognito)
|
||||
cath <## "use /i alice to print out this incognito profile again",
|
||||
alice <## (cathIncognito <> ": contact is connected")
|
||||
]
|
||||
-- alice creates group
|
||||
alice ##> "/g secret_club"
|
||||
alice <## "group #secret_club is created"
|
||||
alice <## "to add members use /a secret_club <name> or /create link #secret_club"
|
||||
-- alice invites bob
|
||||
alice ##> "/a secret_club bob admin"
|
||||
concurrentlyN_
|
||||
[ alice <## "invitation to join the group #secret_club sent to bob",
|
||||
do
|
||||
bob <## "#secret_club: alice invites you to join the group as admin"
|
||||
bob <## "use /j secret_club to accept"
|
||||
]
|
||||
bob ##> "/j secret_club"
|
||||
concurrently_
|
||||
(alice <## "#secret_club: bob joined the group")
|
||||
(bob <## "#secret_club: you joined the group")
|
||||
-- alice invites cath
|
||||
alice ##> ("/a secret_club " <> cathIncognito <> " admin")
|
||||
concurrentlyN_
|
||||
[ alice <## ("invitation to join the group #secret_club sent to " <> cathIncognito),
|
||||
do
|
||||
cath <## "#secret_club: alice invites you to join the group as admin"
|
||||
cath <## ("use /j secret_club to join incognito as " <> cathIncognito)
|
||||
]
|
||||
-- cath uses the same incognito profile when joining group, cath and bob don't merge contacts
|
||||
cath ##> "/j secret_club"
|
||||
concurrentlyN_
|
||||
[ alice <## ("#secret_club: " <> cathIncognito <> " joined the group"),
|
||||
do
|
||||
cath <## ("#secret_club: you joined the group incognito as " <> cathIncognito)
|
||||
cath <## "#secret_club: member bob_1 (Bob) is connected",
|
||||
do
|
||||
bob <## ("#secret_club: alice added " <> cathIncognito <> " to the group (connecting...)")
|
||||
bob <## ("#secret_club: new member " <> cathIncognito <> " is connected")
|
||||
]
|
||||
-- cath cannot invite to the group because her membership is incognito
|
||||
cath ##> "/a secret_club dan"
|
||||
cath <## "you've connected to this group using an incognito profile - prohibited to invite contacts"
|
||||
-- alice invites dan
|
||||
alice ##> "/a secret_club dan admin"
|
||||
concurrentlyN_
|
||||
[ alice <## "invitation to join the group #secret_club sent to dan",
|
||||
do
|
||||
dan <## "#secret_club: alice invites you to join the group as admin"
|
||||
dan <## "use /j secret_club to accept"
|
||||
]
|
||||
dan ##> "/j secret_club"
|
||||
-- cath and dan don't merge contacts
|
||||
concurrentlyN_
|
||||
[ alice <## "#secret_club: dan joined the group",
|
||||
do
|
||||
dan <## "#secret_club: you joined the group"
|
||||
dan
|
||||
<### [ ConsoleString $ "#secret_club: member " <> cathIncognito <> " is connected",
|
||||
"#secret_club: member bob_1 (Bob) is connected",
|
||||
"contact bob_1 is merged into bob",
|
||||
"use @bob <message> to send messages"
|
||||
],
|
||||
do
|
||||
bob <## "#secret_club: alice added dan_1 (Daniel) to the group (connecting...)"
|
||||
bob <## "#secret_club: new member dan_1 is connected"
|
||||
bob <## "contact dan_1 is merged into dan"
|
||||
bob <## "use @dan <message> to send messages",
|
||||
do
|
||||
cath <## "#secret_club: alice added dan_1 (Daniel) to the group (connecting...)"
|
||||
cath <## "#secret_club: new member dan_1 is connected"
|
||||
]
|
||||
-- send messages - group is incognito for cath
|
||||
alice #> "#secret_club hello"
|
||||
concurrentlyN_
|
||||
[ bob <# "#secret_club alice> hello",
|
||||
cath ?<# "#secret_club alice> hello",
|
||||
dan <# "#secret_club alice> hello"
|
||||
]
|
||||
bob #> "#secret_club hi there"
|
||||
concurrentlyN_
|
||||
[ alice <# "#secret_club bob> hi there",
|
||||
cath ?<# "#secret_club bob_1> hi there",
|
||||
dan <# "#secret_club bob> hi there"
|
||||
]
|
||||
cath ?#> "#secret_club hey"
|
||||
concurrentlyN_
|
||||
[ alice <# ("#secret_club " <> cathIncognito <> "> hey"),
|
||||
bob <# ("#secret_club " <> cathIncognito <> "> hey"),
|
||||
dan <# ("#secret_club " <> cathIncognito <> "> hey")
|
||||
]
|
||||
dan #> "#secret_club how is it going?"
|
||||
concurrentlyN_
|
||||
[ alice <# "#secret_club dan> how is it going?",
|
||||
bob <# "#secret_club dan> how is it going?",
|
||||
cath ?<# "#secret_club dan_1> how is it going?"
|
||||
]
|
||||
-- cath and bob can send messages via new direct connection, cath is incognito
|
||||
bob #> ("@" <> cathIncognito <> " hi, I'm bob")
|
||||
cath ?<# "bob_1> hi, I'm bob"
|
||||
cath ?#> "@bob_1 hey, I'm incognito"
|
||||
bob <# (cathIncognito <> "> hey, I'm incognito")
|
||||
-- cath and dan can send messages via new direct connection, cath is incognito
|
||||
dan #> ("@" <> cathIncognito <> " hi, I'm dan")
|
||||
cath ?<# "dan_1> hi, I'm dan"
|
||||
cath ?#> "@dan_1 hey, I'm incognito"
|
||||
dan <# (cathIncognito <> "> hey, I'm incognito")
|
||||
-- non incognito connections are separate
|
||||
bob <##> cath
|
||||
dan <##> cath
|
||||
-- list groups
|
||||
cath ##> "/gs"
|
||||
cath <## "i #secret_club (4 members)"
|
||||
-- list group members
|
||||
alice ##> "/ms secret_club"
|
||||
alice
|
||||
<### [ "alice (Alice): owner, you, created group",
|
||||
"bob (Bob): admin, invited, connected",
|
||||
ConsoleString $ cathIncognito <> ": admin, invited, connected",
|
||||
"dan (Daniel): admin, invited, connected"
|
||||
]
|
||||
bob ##> "/ms secret_club"
|
||||
bob
|
||||
<### [ "alice (Alice): owner, host, connected",
|
||||
"bob (Bob): admin, you, connected",
|
||||
ConsoleString $ cathIncognito <> ": admin, connected",
|
||||
"dan (Daniel): admin, connected"
|
||||
]
|
||||
cath ##> "/ms secret_club"
|
||||
cath
|
||||
<### [ "alice (Alice): owner, host, connected",
|
||||
"bob_1 (Bob): admin, connected",
|
||||
ConsoleString $ "i " <> cathIncognito <> ": admin, you, connected",
|
||||
"dan_1 (Daniel): admin, connected"
|
||||
]
|
||||
dan ##> "/ms secret_club"
|
||||
dan
|
||||
<### [ "alice (Alice): owner, host, connected",
|
||||
"bob (Bob): admin, connected",
|
||||
ConsoleString $ cathIncognito <> ": admin, connected",
|
||||
"dan (Daniel): admin, you, connected"
|
||||
]
|
||||
-- remove member
|
||||
bob ##> ("/rm secret_club " <> cathIncognito)
|
||||
concurrentlyN_
|
||||
[ bob <## ("#secret_club: you removed " <> cathIncognito <> " from the group"),
|
||||
alice <## ("#secret_club: bob removed " <> cathIncognito <> " from the group"),
|
||||
dan <## ("#secret_club: bob removed " <> cathIncognito <> " from the group"),
|
||||
do
|
||||
cath <## "#secret_club: bob_1 removed you from the group"
|
||||
cath <## "use /d #secret_club to delete the group"
|
||||
]
|
||||
bob #> "#secret_club hi"
|
||||
concurrentlyN_
|
||||
[ alice <# "#secret_club bob> hi",
|
||||
dan <# "#secret_club bob> hi",
|
||||
(cath </)
|
||||
]
|
||||
alice #> "#secret_club hello"
|
||||
concurrentlyN_
|
||||
[ bob <# "#secret_club alice> hello",
|
||||
dan <# "#secret_club alice> hello",
|
||||
(cath </)
|
||||
]
|
||||
cath ##> "#secret_club hello"
|
||||
cath <## "you are no longer a member of the group"
|
||||
-- cath can still message members directly
|
||||
bob #> ("@" <> cathIncognito <> " I removed you from group")
|
||||
cath ?<# "bob_1> I removed you from group"
|
||||
cath ?#> "@bob_1 ok"
|
||||
bob <# (cathIncognito <> "> ok")
|
||||
testJoinGroupIncognito =
|
||||
testChatCfg4 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile danProfile $
|
||||
\alice bob cath dan -> do
|
||||
-- non incognito connections
|
||||
connectUsers alice bob
|
||||
connectUsers alice dan
|
||||
connectUsers bob cath
|
||||
connectUsers bob dan
|
||||
connectUsers cath dan
|
||||
-- cath connected incognito to alice
|
||||
alice ##> "/c"
|
||||
inv <- getInvitation alice
|
||||
cath ##> ("/c i " <> inv)
|
||||
cath <## "confirmation sent!"
|
||||
cathIncognito <- getTermLine cath
|
||||
concurrentlyN_
|
||||
[ do
|
||||
cath <## ("alice (Alice): contact is connected, your incognito profile for this contact is " <> cathIncognito)
|
||||
cath <## "use /i alice to print out this incognito profile again",
|
||||
alice <## (cathIncognito <> ": contact is connected")
|
||||
]
|
||||
-- alice creates group
|
||||
alice ##> "/g secret_club"
|
||||
alice <## "group #secret_club is created"
|
||||
alice <## "to add members use /a secret_club <name> or /create link #secret_club"
|
||||
-- alice invites bob
|
||||
alice ##> "/a secret_club bob admin"
|
||||
concurrentlyN_
|
||||
[ alice <## "invitation to join the group #secret_club sent to bob",
|
||||
do
|
||||
bob <## "#secret_club: alice invites you to join the group as admin"
|
||||
bob <## "use /j secret_club to accept"
|
||||
]
|
||||
bob ##> "/j secret_club"
|
||||
concurrently_
|
||||
(alice <## "#secret_club: bob joined the group")
|
||||
(bob <## "#secret_club: you joined the group")
|
||||
-- alice invites cath
|
||||
alice ##> ("/a secret_club " <> cathIncognito <> " admin")
|
||||
concurrentlyN_
|
||||
[ alice <## ("invitation to join the group #secret_club sent to " <> cathIncognito),
|
||||
do
|
||||
cath <## "#secret_club: alice invites you to join the group as admin"
|
||||
cath <## ("use /j secret_club to join incognito as " <> cathIncognito)
|
||||
]
|
||||
-- cath uses the same incognito profile when joining group, cath and bob don't merge contacts
|
||||
cath ##> "/j secret_club"
|
||||
concurrentlyN_
|
||||
[ alice <## ("#secret_club: " <> cathIncognito <> " joined the group"),
|
||||
do
|
||||
cath <## ("#secret_club: you joined the group incognito as " <> cathIncognito)
|
||||
cath <## "#secret_club: member bob_1 (Bob) is connected",
|
||||
do
|
||||
bob <## ("#secret_club: alice added " <> cathIncognito <> " to the group (connecting...)")
|
||||
bob <## ("#secret_club: new member " <> cathIncognito <> " is connected")
|
||||
]
|
||||
-- cath cannot invite to the group because her membership is incognito
|
||||
cath ##> "/a secret_club dan"
|
||||
cath <## "you've connected to this group using an incognito profile - prohibited to invite contacts"
|
||||
-- alice invites dan
|
||||
alice ##> "/a secret_club dan admin"
|
||||
concurrentlyN_
|
||||
[ alice <## "invitation to join the group #secret_club sent to dan",
|
||||
do
|
||||
dan <## "#secret_club: alice invites you to join the group as admin"
|
||||
dan <## "use /j secret_club to accept"
|
||||
]
|
||||
dan ##> "/j secret_club"
|
||||
-- cath and dan don't merge contacts
|
||||
concurrentlyN_
|
||||
[ alice <## "#secret_club: dan joined the group",
|
||||
do
|
||||
dan <## "#secret_club: you joined the group"
|
||||
dan
|
||||
<### [ ConsoleString $ "#secret_club: member " <> cathIncognito <> " is connected",
|
||||
"#secret_club: member bob_1 (Bob) is connected",
|
||||
"contact bob_1 is merged into bob",
|
||||
"use @bob <message> to send messages"
|
||||
],
|
||||
do
|
||||
bob <## "#secret_club: alice added dan_1 (Daniel) to the group (connecting...)"
|
||||
bob <## "#secret_club: new member dan_1 is connected"
|
||||
bob <## "contact dan_1 is merged into dan"
|
||||
bob <## "use @dan <message> to send messages",
|
||||
do
|
||||
cath <## "#secret_club: alice added dan_1 (Daniel) to the group (connecting...)"
|
||||
cath <## "#secret_club: new member dan_1 is connected"
|
||||
]
|
||||
-- send messages - group is incognito for cath
|
||||
alice #> "#secret_club hello"
|
||||
concurrentlyN_
|
||||
[ bob <# "#secret_club alice> hello",
|
||||
cath ?<# "#secret_club alice> hello",
|
||||
dan <# "#secret_club alice> hello"
|
||||
]
|
||||
bob #> "#secret_club hi there"
|
||||
concurrentlyN_
|
||||
[ alice <# "#secret_club bob> hi there",
|
||||
cath ?<# "#secret_club bob_1> hi there",
|
||||
dan <# "#secret_club bob> hi there"
|
||||
]
|
||||
cath ?#> "#secret_club hey"
|
||||
concurrentlyN_
|
||||
[ alice <# ("#secret_club " <> cathIncognito <> "> hey"),
|
||||
bob <# ("#secret_club " <> cathIncognito <> "> hey"),
|
||||
dan <# ("#secret_club " <> cathIncognito <> "> hey")
|
||||
]
|
||||
dan #> "#secret_club how is it going?"
|
||||
concurrentlyN_
|
||||
[ alice <# "#secret_club dan> how is it going?",
|
||||
bob <# "#secret_club dan> how is it going?",
|
||||
cath ?<# "#secret_club dan_1> how is it going?"
|
||||
]
|
||||
-- cath and bob can send messages via new direct connection, cath is incognito
|
||||
bob #> ("@" <> cathIncognito <> " hi, I'm bob")
|
||||
cath ?<# "bob_1> hi, I'm bob"
|
||||
cath ?#> "@bob_1 hey, I'm incognito"
|
||||
bob <# (cathIncognito <> "> hey, I'm incognito")
|
||||
-- cath and dan can send messages via new direct connection, cath is incognito
|
||||
dan #> ("@" <> cathIncognito <> " hi, I'm dan")
|
||||
cath ?<# "dan_1> hi, I'm dan"
|
||||
cath ?#> "@dan_1 hey, I'm incognito"
|
||||
dan <# (cathIncognito <> "> hey, I'm incognito")
|
||||
-- non incognito connections are separate
|
||||
bob <##> cath
|
||||
dan <##> cath
|
||||
-- list groups
|
||||
cath ##> "/gs"
|
||||
cath <## "i #secret_club (4 members)"
|
||||
-- list group members
|
||||
alice ##> "/ms secret_club"
|
||||
alice
|
||||
<### [ "alice (Alice): owner, you, created group",
|
||||
"bob (Bob): admin, invited, connected",
|
||||
ConsoleString $ cathIncognito <> ": admin, invited, connected",
|
||||
"dan (Daniel): admin, invited, connected"
|
||||
]
|
||||
bob ##> "/ms secret_club"
|
||||
bob
|
||||
<### [ "alice (Alice): owner, host, connected",
|
||||
"bob (Bob): admin, you, connected",
|
||||
ConsoleString $ cathIncognito <> ": admin, connected",
|
||||
"dan (Daniel): admin, connected"
|
||||
]
|
||||
cath ##> "/ms secret_club"
|
||||
cath
|
||||
<### [ "alice (Alice): owner, host, connected",
|
||||
"bob_1 (Bob): admin, connected",
|
||||
ConsoleString $ "i " <> cathIncognito <> ": admin, you, connected",
|
||||
"dan_1 (Daniel): admin, connected"
|
||||
]
|
||||
dan ##> "/ms secret_club"
|
||||
dan
|
||||
<### [ "alice (Alice): owner, host, connected",
|
||||
"bob (Bob): admin, connected",
|
||||
ConsoleString $ cathIncognito <> ": admin, connected",
|
||||
"dan (Daniel): admin, you, connected"
|
||||
]
|
||||
-- remove member
|
||||
bob ##> ("/rm secret_club " <> cathIncognito)
|
||||
concurrentlyN_
|
||||
[ bob <## ("#secret_club: you removed " <> cathIncognito <> " from the group"),
|
||||
alice <## ("#secret_club: bob removed " <> cathIncognito <> " from the group"),
|
||||
dan <## ("#secret_club: bob removed " <> cathIncognito <> " from the group"),
|
||||
do
|
||||
cath <## "#secret_club: bob_1 removed you from the group"
|
||||
cath <## "use /d #secret_club to delete the group"
|
||||
]
|
||||
bob #> "#secret_club hi"
|
||||
concurrentlyN_
|
||||
[ alice <# "#secret_club bob> hi",
|
||||
dan <# "#secret_club bob> hi",
|
||||
(cath </)
|
||||
]
|
||||
alice #> "#secret_club hello"
|
||||
concurrentlyN_
|
||||
[ bob <# "#secret_club alice> hello",
|
||||
dan <# "#secret_club alice> hello",
|
||||
(cath </)
|
||||
]
|
||||
cath ##> "#secret_club hello"
|
||||
cath <## "you are no longer a member of the group"
|
||||
-- cath can still message members directly
|
||||
bob #> ("@" <> cathIncognito <> " I removed you from group")
|
||||
cath ?<# "bob_1> I removed you from group"
|
||||
cath ?#> "@bob_1 ok"
|
||||
bob <# (cathIncognito <> "> ok")
|
||||
|
||||
testCantInviteContactIncognito :: HasCallStack => FilePath -> IO ()
|
||||
testCantInviteContactIncognito = testChat2 aliceProfile bobProfile $
|
||||
@ -1354,54 +1359,55 @@ testAllowFullDeletionGroup =
|
||||
|
||||
testProhibitDirectMessages :: HasCallStack => FilePath -> IO ()
|
||||
testProhibitDirectMessages =
|
||||
testChat4 aliceProfile bobProfile cathProfile danProfile $ \alice bob cath dan -> do
|
||||
createGroup3 "team" alice bob cath
|
||||
threadDelay 1000000
|
||||
alice ##> "/set direct #team off"
|
||||
alice <## "updated group preferences:"
|
||||
alice <## "Direct messages: off"
|
||||
directProhibited bob
|
||||
directProhibited cath
|
||||
threadDelay 1000000
|
||||
-- still can send direct messages to direct contacts
|
||||
alice #> "@bob hello again"
|
||||
bob <# "alice> hello again"
|
||||
alice #> "@cath hello again"
|
||||
cath <# "alice> hello again"
|
||||
bob ##> "@cath hello again"
|
||||
bob <## "direct messages to indirect contact cath are prohibited"
|
||||
(cath </)
|
||||
connectUsers cath dan
|
||||
addMember "team" cath dan GRMember
|
||||
dan ##> "/j #team"
|
||||
concurrentlyN_
|
||||
[ cath <## "#team: dan joined the group",
|
||||
do
|
||||
dan <## "#team: you joined the group"
|
||||
dan
|
||||
<### [ "#team: member alice (Alice) is connected",
|
||||
"#team: member bob (Bob) is connected"
|
||||
],
|
||||
do
|
||||
alice <## "#team: cath added dan (Daniel) to the group (connecting...)"
|
||||
alice <## "#team: new member dan is connected",
|
||||
do
|
||||
bob <## "#team: cath added dan (Daniel) to the group (connecting...)"
|
||||
bob <## "#team: new member dan is connected"
|
||||
]
|
||||
alice ##> "@dan hi"
|
||||
alice <## "direct messages to indirect contact dan are prohibited"
|
||||
bob ##> "@dan hi"
|
||||
bob <## "direct messages to indirect contact dan are prohibited"
|
||||
(dan </)
|
||||
dan ##> "@alice hi"
|
||||
dan <## "direct messages to indirect contact alice are prohibited"
|
||||
dan ##> "@bob hi"
|
||||
dan <## "direct messages to indirect contact bob are prohibited"
|
||||
dan #> "@cath hi"
|
||||
cath <# "dan> hi"
|
||||
cath #> "@dan hi"
|
||||
dan <# "cath> hi"
|
||||
testChatCfg4 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile danProfile $
|
||||
\alice bob cath dan -> do
|
||||
createGroup3 "team" alice bob cath
|
||||
threadDelay 1000000
|
||||
alice ##> "/set direct #team off"
|
||||
alice <## "updated group preferences:"
|
||||
alice <## "Direct messages: off"
|
||||
directProhibited bob
|
||||
directProhibited cath
|
||||
threadDelay 1000000
|
||||
-- still can send direct messages to direct contacts
|
||||
alice #> "@bob hello again"
|
||||
bob <# "alice> hello again"
|
||||
alice #> "@cath hello again"
|
||||
cath <# "alice> hello again"
|
||||
bob ##> "@cath hello again"
|
||||
bob <## "direct messages to indirect contact cath are prohibited"
|
||||
(cath </)
|
||||
connectUsers cath dan
|
||||
addMember "team" cath dan GRMember
|
||||
dan ##> "/j #team"
|
||||
concurrentlyN_
|
||||
[ cath <## "#team: dan joined the group",
|
||||
do
|
||||
dan <## "#team: you joined the group"
|
||||
dan
|
||||
<### [ "#team: member alice (Alice) is connected",
|
||||
"#team: member bob (Bob) is connected"
|
||||
],
|
||||
do
|
||||
alice <## "#team: cath added dan (Daniel) to the group (connecting...)"
|
||||
alice <## "#team: new member dan is connected",
|
||||
do
|
||||
bob <## "#team: cath added dan (Daniel) to the group (connecting...)"
|
||||
bob <## "#team: new member dan is connected"
|
||||
]
|
||||
alice ##> "@dan hi"
|
||||
alice <## "direct messages to indirect contact dan are prohibited"
|
||||
bob ##> "@dan hi"
|
||||
bob <## "direct messages to indirect contact dan are prohibited"
|
||||
(dan </)
|
||||
dan ##> "@alice hi"
|
||||
dan <## "direct messages to indirect contact alice are prohibited"
|
||||
dan ##> "@bob hi"
|
||||
dan <## "direct messages to indirect contact bob are prohibited"
|
||||
dan #> "@cath hi"
|
||||
cath <# "dan> hi"
|
||||
cath #> "@dan hi"
|
||||
dan <# "cath> hi"
|
||||
where
|
||||
directProhibited :: HasCallStack => TestCC -> IO ()
|
||||
directProhibited cc = do
|
||||
|
@ -18,11 +18,13 @@ import Data.Maybe (fromMaybe)
|
||||
import Data.String
|
||||
import qualified Data.Text as T
|
||||
import Simplex.Chat.Controller (ChatConfig (..), ChatController (..), InlineFilesConfig (..), defaultInlineFilesConfig)
|
||||
import Simplex.Chat.Protocol
|
||||
import Simplex.Chat.Store.Profiles (getUserContactProfiles)
|
||||
import Simplex.Chat.Types
|
||||
import Simplex.Chat.Types.Preferences
|
||||
import Simplex.Messaging.Agent.Store.SQLite (withTransaction)
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Version
|
||||
import System.Directory (doesFileExist)
|
||||
import System.Environment (lookupEnv)
|
||||
import System.FilePath ((</>))
|
||||
@ -65,9 +67,9 @@ versionTestMatrix2 runTest = do
|
||||
it "v1 to v2" $ runTestCfg2 testCfg testCfgV1 runTest
|
||||
it "v2 to v1" $ runTestCfg2 testCfgV1 testCfg runTest
|
||||
|
||||
versionTestMatrix3 :: (HasCallStack => TestCC -> TestCC -> TestCC -> IO ()) -> SpecWith FilePath
|
||||
versionTestMatrix3 runTest = do
|
||||
it "v2" $ testChat3 aliceProfile bobProfile cathProfile runTest
|
||||
-- versionTestMatrix3 :: (HasCallStack => TestCC -> TestCC -> TestCC -> IO ()) -> SpecWith FilePath
|
||||
-- versionTestMatrix3 runTest = do
|
||||
-- it "v2" $ testChat3 aliceProfile bobProfile cathProfile runTest
|
||||
|
||||
-- it "v1" $ testChatCfg3 testCfgV1 aliceProfile bobProfile cathProfile runTest
|
||||
-- it "v1 to v2" $ runTestCfg3 testCfg testCfgV1 testCfgV1 runTest
|
||||
@ -356,7 +358,7 @@ dropTime_ msg = case splitAt 6 msg of
|
||||
_ -> Nothing
|
||||
|
||||
dropStrPrefix :: HasCallStack => String -> String -> String
|
||||
dropStrPrefix pfx s =
|
||||
dropStrPrefix pfx s =
|
||||
let (p, rest) = splitAt (length pfx) s
|
||||
in if p == pfx then rest else error $ "no prefix " <> pfx <> " in string : " <> s
|
||||
|
||||
@ -523,3 +525,10 @@ startFileTransferWithDest' cc1 cc2 fileName fileSize fileDest_ = do
|
||||
concurrently_
|
||||
(cc2 <## ("started receiving file 1 (" <> fileName <> ") from " <> name1))
|
||||
(cc1 <## ("started sending file 1 (" <> fileName <> ") to " <> name2))
|
||||
|
||||
currentChatVRangeInfo :: String
|
||||
currentChatVRangeInfo =
|
||||
"peer chat protocol version range: " <> vRangeStr supportedChatVRange
|
||||
|
||||
vRangeStr :: VersionRange -> String
|
||||
vRangeStr (VersionRange minVer maxVer) = "(" <> show minVer <> ", " <> show maxVer <> ")"
|
||||
|
@ -76,10 +76,10 @@ s ##==## msg = do
|
||||
s ==## msg
|
||||
|
||||
(==#) :: MsgEncodingI e => ByteString -> ChatMsgEvent e -> Expectation
|
||||
s ==# msg = s ==## ChatMessage Nothing msg
|
||||
s ==# msg = s ==## ChatMessage chatInitialVRange Nothing msg
|
||||
|
||||
(#==) :: MsgEncodingI e => ByteString -> ChatMsgEvent e -> Expectation
|
||||
s #== msg = s ##== ChatMessage Nothing msg
|
||||
s #== msg = s ##== ChatMessage chatInitialVRange Nothing msg
|
||||
|
||||
(#==#) :: MsgEncodingI e => ByteString -> ChatMsgEvent e -> Expectation
|
||||
s #==# msg = do
|
||||
@ -101,59 +101,66 @@ testGroupProfile = GroupProfile {displayName = "team", fullName = "Team", descri
|
||||
decodeChatMessageTest :: Spec
|
||||
decodeChatMessageTest = describe "Chat message encoding/decoding" $ do
|
||||
it "x.msg.new simple text" $
|
||||
"{\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}"
|
||||
"{\"v\":\"1\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}"
|
||||
#==# XMsgNew (MCSimple (extMsgContent (MCText "hello") Nothing))
|
||||
it "x.msg.new simple text - timed message TTL" $
|
||||
"{\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"ttl\":3600}}"
|
||||
"{\"v\":\"1\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"ttl\":3600}}"
|
||||
#==# XMsgNew (MCSimple (ExtMsgContent (MCText "hello") Nothing (Just 3600) Nothing))
|
||||
it "x.msg.new simple text - live message" $
|
||||
"{\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"live\":true}}"
|
||||
"{\"v\":\"1\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"live\":true}}"
|
||||
#==# XMsgNew (MCSimple (ExtMsgContent (MCText "hello") Nothing Nothing (Just True)))
|
||||
it "x.msg.new simple link" $
|
||||
"{\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"https://simplex.chat\",\"type\":\"link\",\"preview\":{\"description\":\"SimpleX Chat\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgA\",\"title\":\"SimpleX Chat\",\"uri\":\"https://simplex.chat\"}}}}"
|
||||
"{\"v\":\"1\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"https://simplex.chat\",\"type\":\"link\",\"preview\":{\"description\":\"SimpleX Chat\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgA\",\"title\":\"SimpleX Chat\",\"uri\":\"https://simplex.chat\"}}}}"
|
||||
#==# XMsgNew (MCSimple (extMsgContent (MCLink "https://simplex.chat" $ LinkPreview {uri = "https://simplex.chat", title = "SimpleX Chat", description = "SimpleX Chat", image = ImageData "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgA", content = Nothing}) Nothing))
|
||||
it "x.msg.new simple image" $
|
||||
"{\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}}"
|
||||
"{\"v\":\"1\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}}"
|
||||
#==# XMsgNew (MCSimple (extMsgContent (MCImage "" $ ImageData "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=") Nothing))
|
||||
it "x.msg.new simple image with text" $
|
||||
"{\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"here's an image\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}}"
|
||||
"{\"v\":\"1\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"here's an image\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}}"
|
||||
#==# XMsgNew (MCSimple (extMsgContent (MCImage "here's an image" $ ImageData "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=") Nothing))
|
||||
it "x.msg.new chat message " $
|
||||
"{\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}"
|
||||
##==## ChatMessage (Just $ SharedMsgId "\1\2\3\4") (XMsgNew (MCSimple (extMsgContent (MCText "hello") Nothing)))
|
||||
it "x.msg.new chat message" $
|
||||
"{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}"
|
||||
##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew (MCSimple (extMsgContent (MCText "hello") Nothing)))
|
||||
it "x.msg.new chat message with chat version range" $
|
||||
"{\"v\":\"1-2\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}"
|
||||
##==## ChatMessage supportedChatVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew (MCSimple (extMsgContent (MCText "hello") Nothing)))
|
||||
it "x.msg.new quote" $
|
||||
"{\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello to you too\",\"type\":\"text\"},\"quote\":{\"content\":{\"text\":\"hello there!\",\"type\":\"text\"},\"msgRef\":{\"msgId\":\"BQYHCA==\",\"sent\":true,\"sentAt\":\"1970-01-01T00:00:01.000000001Z\"}}}}"
|
||||
"{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello to you too\",\"type\":\"text\"},\"quote\":{\"content\":{\"text\":\"hello there!\",\"type\":\"text\"},\"msgRef\":{\"msgId\":\"BQYHCA==\",\"sent\":true,\"sentAt\":\"1970-01-01T00:00:01.000000001Z\"}}}}"
|
||||
##==## ChatMessage
|
||||
chatInitialVRange
|
||||
(Just $ SharedMsgId "\1\2\3\4")
|
||||
(XMsgNew (MCQuote quotedMsg (extMsgContent (MCText "hello to you too") Nothing)))
|
||||
it "x.msg.new quote - timed message TTL" $
|
||||
"{\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello to you too\",\"type\":\"text\"},\"quote\":{\"content\":{\"text\":\"hello there!\",\"type\":\"text\"},\"msgRef\":{\"msgId\":\"BQYHCA==\",\"sent\":true,\"sentAt\":\"1970-01-01T00:00:01.000000001Z\"}},\"ttl\":3600}}"
|
||||
"{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello to you too\",\"type\":\"text\"},\"quote\":{\"content\":{\"text\":\"hello there!\",\"type\":\"text\"},\"msgRef\":{\"msgId\":\"BQYHCA==\",\"sent\":true,\"sentAt\":\"1970-01-01T00:00:01.000000001Z\"}},\"ttl\":3600}}"
|
||||
##==## ChatMessage
|
||||
chatInitialVRange
|
||||
(Just $ SharedMsgId "\1\2\3\4")
|
||||
(XMsgNew (MCQuote quotedMsg (ExtMsgContent (MCText "hello to you too") Nothing (Just 3600) Nothing)))
|
||||
it "x.msg.new quote - live message" $
|
||||
"{\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello to you too\",\"type\":\"text\"},\"quote\":{\"content\":{\"text\":\"hello there!\",\"type\":\"text\"},\"msgRef\":{\"msgId\":\"BQYHCA==\",\"sent\":true,\"sentAt\":\"1970-01-01T00:00:01.000000001Z\"}},\"live\":true}}"
|
||||
"{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello to you too\",\"type\":\"text\"},\"quote\":{\"content\":{\"text\":\"hello there!\",\"type\":\"text\"},\"msgRef\":{\"msgId\":\"BQYHCA==\",\"sent\":true,\"sentAt\":\"1970-01-01T00:00:01.000000001Z\"}},\"live\":true}}"
|
||||
##==## ChatMessage
|
||||
chatInitialVRange
|
||||
(Just $ SharedMsgId "\1\2\3\4")
|
||||
(XMsgNew (MCQuote quotedMsg (ExtMsgContent (MCText "hello to you too") Nothing Nothing (Just True))))
|
||||
it "x.msg.new forward" $
|
||||
"{\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"forward\":true}}"
|
||||
##==## ChatMessage (Just $ SharedMsgId "\1\2\3\4") (XMsgNew $ MCForward (extMsgContent (MCText "hello") Nothing))
|
||||
"{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"forward\":true}}"
|
||||
##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew $ MCForward (extMsgContent (MCText "hello") Nothing))
|
||||
it "x.msg.new forward - timed message TTL" $
|
||||
"{\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"forward\":true,\"ttl\":3600}}"
|
||||
##==## ChatMessage (Just $ SharedMsgId "\1\2\3\4") (XMsgNew $ MCForward (ExtMsgContent (MCText "hello") Nothing (Just 3600) Nothing))
|
||||
"{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"forward\":true,\"ttl\":3600}}"
|
||||
##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew $ MCForward (ExtMsgContent (MCText "hello") Nothing (Just 3600) Nothing))
|
||||
it "x.msg.new forward - live message" $
|
||||
"{\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"forward\":true,\"live\":true}}"
|
||||
##==## ChatMessage (Just $ SharedMsgId "\1\2\3\4") (XMsgNew $ MCForward (ExtMsgContent (MCText "hello") Nothing Nothing (Just True)))
|
||||
"{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"forward\":true,\"live\":true}}"
|
||||
##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew $ MCForward (ExtMsgContent (MCText "hello") Nothing Nothing (Just True)))
|
||||
it "x.msg.new simple text with file" $
|
||||
"{\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"file\":{\"fileSize\":12345,\"fileName\":\"photo.jpg\"}}}"
|
||||
"{\"v\":\"1\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"file\":{\"fileSize\":12345,\"fileName\":\"photo.jpg\"}}}"
|
||||
#==# XMsgNew (MCSimple (extMsgContent (MCText "hello") (Just FileInvitation {fileName = "photo.jpg", fileSize = 12345, fileDigest = Nothing, fileConnReq = Nothing, fileInline = Nothing, fileDescr = Nothing})))
|
||||
it "x.msg.new simple file with file" $
|
||||
"{\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"\",\"type\":\"file\"},\"file\":{\"fileSize\":12345,\"fileName\":\"file.txt\"}}}"
|
||||
"{\"v\":\"1\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"\",\"type\":\"file\"},\"file\":{\"fileSize\":12345,\"fileName\":\"file.txt\"}}}"
|
||||
#==# XMsgNew (MCSimple (extMsgContent (MCFile "") (Just FileInvitation {fileName = "file.txt", fileSize = 12345, fileDigest = Nothing, fileConnReq = Nothing, fileInline = Nothing, fileDescr = Nothing})))
|
||||
it "x.msg.new quote with file" $
|
||||
"{\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello to you too\",\"type\":\"text\"},\"quote\":{\"content\":{\"text\":\"hello there!\",\"type\":\"text\"},\"msgRef\":{\"msgId\":\"BQYHCA==\",\"sent\":true,\"sentAt\":\"1970-01-01T00:00:01.000000001Z\"}},\"file\":{\"fileSize\":12345,\"fileName\":\"photo.jpg\"}}}"
|
||||
"{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello to you too\",\"type\":\"text\"},\"quote\":{\"content\":{\"text\":\"hello there!\",\"type\":\"text\"},\"msgRef\":{\"msgId\":\"BQYHCA==\",\"sent\":true,\"sentAt\":\"1970-01-01T00:00:01.000000001Z\"}},\"file\":{\"fileSize\":12345,\"fileName\":\"photo.jpg\"}}}"
|
||||
##==## ChatMessage
|
||||
chatInitialVRange
|
||||
(Just $ SharedMsgId "\1\2\3\4")
|
||||
( XMsgNew
|
||||
( MCQuote
|
||||
@ -165,101 +172,113 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do
|
||||
)
|
||||
)
|
||||
it "x.msg.new forward with file" $
|
||||
"{\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"forward\":true,\"file\":{\"fileSize\":12345,\"fileName\":\"photo.jpg\"}}}"
|
||||
##==## ChatMessage (Just $ SharedMsgId "\1\2\3\4") (XMsgNew $ MCForward (extMsgContent (MCText "hello") (Just FileInvitation {fileName = "photo.jpg", fileSize = 12345, fileDigest = Nothing, fileConnReq = Nothing, fileInline = Nothing, fileDescr = Nothing})))
|
||||
"{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"forward\":true,\"file\":{\"fileSize\":12345,\"fileName\":\"photo.jpg\"}}}"
|
||||
##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew $ MCForward (extMsgContent (MCText "hello") (Just FileInvitation {fileName = "photo.jpg", fileSize = 12345, fileDigest = Nothing, fileConnReq = Nothing, fileInline = Nothing, fileDescr = Nothing})))
|
||||
it "x.msg.update" $
|
||||
"{\"event\":\"x.msg.update\",\"params\":{\"msgId\":\"AQIDBA==\", \"content\":{\"text\":\"hello\",\"type\":\"text\"}}}"
|
||||
"{\"v\":\"1\",\"event\":\"x.msg.update\",\"params\":{\"msgId\":\"AQIDBA==\", \"content\":{\"text\":\"hello\",\"type\":\"text\"}}}"
|
||||
#==# XMsgUpdate (SharedMsgId "\1\2\3\4") (MCText "hello") Nothing Nothing
|
||||
it "x.msg.del" $
|
||||
"{\"event\":\"x.msg.del\",\"params\":{\"msgId\":\"AQIDBA==\"}}"
|
||||
"{\"v\":\"1\",\"event\":\"x.msg.del\",\"params\":{\"msgId\":\"AQIDBA==\"}}"
|
||||
#==# XMsgDel (SharedMsgId "\1\2\3\4") Nothing
|
||||
it "x.msg.deleted" $
|
||||
"{\"event\":\"x.msg.deleted\",\"params\":{}}"
|
||||
"{\"v\":\"1\",\"event\":\"x.msg.deleted\",\"params\":{}}"
|
||||
#==# XMsgDeleted
|
||||
it "x.file" $
|
||||
"{\"event\":\"x.file\",\"params\":{\"file\":{\"fileConnReq\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"fileSize\":12345,\"fileName\":\"photo.jpg\"}}}"
|
||||
"{\"v\":\"1\",\"event\":\"x.file\",\"params\":{\"file\":{\"fileConnReq\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"fileSize\":12345,\"fileName\":\"photo.jpg\"}}}"
|
||||
#==# XFile FileInvitation {fileName = "photo.jpg", fileSize = 12345, fileDigest = Nothing, fileConnReq = Just testConnReq, fileInline = Nothing, fileDescr = Nothing}
|
||||
it "x.file without file invitation" $
|
||||
"{\"event\":\"x.file\",\"params\":{\"file\":{\"fileSize\":12345,\"fileName\":\"photo.jpg\"}}}"
|
||||
"{\"v\":\"1\",\"event\":\"x.file\",\"params\":{\"file\":{\"fileSize\":12345,\"fileName\":\"photo.jpg\"}}}"
|
||||
#==# XFile FileInvitation {fileName = "photo.jpg", fileSize = 12345, fileDigest = Nothing, fileConnReq = Nothing, fileInline = Nothing, fileDescr = Nothing}
|
||||
it "x.file.acpt" $
|
||||
"{\"event\":\"x.file.acpt\",\"params\":{\"fileName\":\"photo.jpg\"}}"
|
||||
"{\"v\":\"1\",\"event\":\"x.file.acpt\",\"params\":{\"fileName\":\"photo.jpg\"}}"
|
||||
#==# XFileAcpt "photo.jpg"
|
||||
it "x.file.acpt.inv" $
|
||||
"{\"event\":\"x.file.acpt.inv\",\"params\":{\"msgId\":\"AQIDBA==\",\"fileName\":\"photo.jpg\",\"fileConnReq\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}"
|
||||
"{\"v\":\"1\",\"event\":\"x.file.acpt.inv\",\"params\":{\"msgId\":\"AQIDBA==\",\"fileName\":\"photo.jpg\",\"fileConnReq\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}"
|
||||
#==# XFileAcptInv (SharedMsgId "\1\2\3\4") (Just testConnReq) "photo.jpg"
|
||||
it "x.file.acpt.inv" $
|
||||
"{\"event\":\"x.file.acpt.inv\",\"params\":{\"msgId\":\"AQIDBA==\",\"fileName\":\"photo.jpg\"}}"
|
||||
"{\"v\":\"1\",\"event\":\"x.file.acpt.inv\",\"params\":{\"msgId\":\"AQIDBA==\",\"fileName\":\"photo.jpg\"}}"
|
||||
#==# XFileAcptInv (SharedMsgId "\1\2\3\4") Nothing "photo.jpg"
|
||||
it "x.file.cancel" $
|
||||
"{\"event\":\"x.file.cancel\",\"params\":{\"msgId\":\"AQIDBA==\"}}"
|
||||
"{\"v\":\"1\",\"event\":\"x.file.cancel\",\"params\":{\"msgId\":\"AQIDBA==\"}}"
|
||||
#==# XFileCancel (SharedMsgId "\1\2\3\4")
|
||||
it "x.info" $
|
||||
"{\"event\":\"x.info\",\"params\":{\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}"
|
||||
"{\"v\":\"1\",\"event\":\"x.info\",\"params\":{\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}"
|
||||
#==# XInfo testProfile
|
||||
it "x.info with empty full name" $
|
||||
"{\"event\":\"x.info\",\"params\":{\"profile\":{\"fullName\":\"\",\"displayName\":\"alice\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}"
|
||||
"{\"v\":\"1\",\"event\":\"x.info\",\"params\":{\"profile\":{\"fullName\":\"\",\"displayName\":\"alice\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}"
|
||||
#==# XInfo Profile {displayName = "alice", fullName = "", image = Nothing, contactLink = Nothing, preferences = testChatPreferences}
|
||||
it "x.contact with xContactId" $
|
||||
"{\"event\":\"x.contact\",\"params\":{\"contactReqId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}"
|
||||
"{\"v\":\"1\",\"event\":\"x.contact\",\"params\":{\"contactReqId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}"
|
||||
#==# XContact testProfile (Just $ XContactId "\1\2\3\4")
|
||||
it "x.contact without XContactId" $
|
||||
"{\"event\":\"x.contact\",\"params\":{\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}"
|
||||
"{\"v\":\"1\",\"event\":\"x.contact\",\"params\":{\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}"
|
||||
#==# XContact testProfile Nothing
|
||||
it "x.contact with content null" $
|
||||
"{\"event\":\"x.contact\",\"params\":{\"content\":null,\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}"
|
||||
"{\"v\":\"1\",\"event\":\"x.contact\",\"params\":{\"content\":null,\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}"
|
||||
==# XContact testProfile Nothing
|
||||
it "x.contact with content (ignored)" $
|
||||
"{\"event\":\"x.contact\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}"
|
||||
"{\"v\":\"1\",\"event\":\"x.contact\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}"
|
||||
==# XContact testProfile Nothing
|
||||
it "x.grp.inv" $
|
||||
"{\"event\":\"x.grp.inv\",\"params\":{\"groupInvitation\":{\"connRequest\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"invitedMember\":{\"memberRole\":\"member\",\"memberId\":\"BQYHCA==\"},\"groupProfile\":{\"fullName\":\"Team\",\"displayName\":\"team\",\"groupPreferences\":{\"reactions\":{\"enable\":\"on\"},\"voice\":{\"enable\":\"on\"}}},\"fromMember\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\"}}}}"
|
||||
"{\"v\":\"1\",\"event\":\"x.grp.inv\",\"params\":{\"groupInvitation\":{\"connRequest\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"invitedMember\":{\"memberRole\":\"member\",\"memberId\":\"BQYHCA==\"},\"groupProfile\":{\"fullName\":\"Team\",\"displayName\":\"team\",\"groupPreferences\":{\"reactions\":{\"enable\":\"on\"},\"voice\":{\"enable\":\"on\"}}},\"fromMember\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\"}}}}"
|
||||
#==# XGrpInv GroupInvitation {fromMember = MemberIdRole (MemberId "\1\2\3\4") GRAdmin, invitedMember = MemberIdRole (MemberId "\5\6\7\8") GRMember, connRequest = testConnReq, groupProfile = testGroupProfile, groupLinkId = Nothing}
|
||||
it "x.grp.inv with group link id" $
|
||||
"{\"event\":\"x.grp.inv\",\"params\":{\"groupInvitation\":{\"connRequest\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"invitedMember\":{\"memberRole\":\"member\",\"memberId\":\"BQYHCA==\"},\"groupProfile\":{\"fullName\":\"Team\",\"displayName\":\"team\",\"groupPreferences\":{\"reactions\":{\"enable\":\"on\"},\"voice\":{\"enable\":\"on\"}}},\"fromMember\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\"}, \"groupLinkId\":\"AQIDBA==\"}}}"
|
||||
"{\"v\":\"1\",\"event\":\"x.grp.inv\",\"params\":{\"groupInvitation\":{\"connRequest\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"invitedMember\":{\"memberRole\":\"member\",\"memberId\":\"BQYHCA==\"},\"groupProfile\":{\"fullName\":\"Team\",\"displayName\":\"team\",\"groupPreferences\":{\"reactions\":{\"enable\":\"on\"},\"voice\":{\"enable\":\"on\"}}},\"fromMember\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\"}, \"groupLinkId\":\"AQIDBA==\"}}}"
|
||||
#==# XGrpInv GroupInvitation {fromMember = MemberIdRole (MemberId "\1\2\3\4") GRAdmin, invitedMember = MemberIdRole (MemberId "\5\6\7\8") GRMember, connRequest = testConnReq, groupProfile = testGroupProfile, groupLinkId = Just $ GroupLinkId "\1\2\3\4"}
|
||||
it "x.grp.acpt without incognito profile" $
|
||||
"{\"event\":\"x.grp.acpt\",\"params\":{\"memberId\":\"AQIDBA==\"}}"
|
||||
"{\"v\":\"1\",\"event\":\"x.grp.acpt\",\"params\":{\"memberId\":\"AQIDBA==\"}}"
|
||||
#==# XGrpAcpt (MemberId "\1\2\3\4")
|
||||
it "x.grp.mem.new" $
|
||||
"{\"event\":\"x.grp.mem.new\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}"
|
||||
#==# XGrpMemNew MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, profile = testProfile}
|
||||
"{\"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, 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" $
|
||||
"{\"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}
|
||||
"{\"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, 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" $
|
||||
"{\"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}
|
||||
"{\"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 = 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" $
|
||||
"{\"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}
|
||||
"{\"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, 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" $
|
||||
"{\"event\":\"x.grp.mem.info\",\"params\":{\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}"
|
||||
"{\"v\":\"1\",\"event\":\"x.grp.mem.info\",\"params\":{\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}"
|
||||
#==# XGrpMemInfo (MemberId "\1\2\3\4") testProfile
|
||||
it "x.grp.mem.con" $
|
||||
"{\"event\":\"x.grp.mem.con\",\"params\":{\"memberId\":\"AQIDBA==\"}}"
|
||||
"{\"v\":\"1\",\"event\":\"x.grp.mem.con\",\"params\":{\"memberId\":\"AQIDBA==\"}}"
|
||||
#==# XGrpMemCon (MemberId "\1\2\3\4")
|
||||
it "x.grp.mem.con.all" $
|
||||
"{\"event\":\"x.grp.mem.con.all\",\"params\":{\"memberId\":\"AQIDBA==\"}}"
|
||||
"{\"v\":\"1\",\"event\":\"x.grp.mem.con.all\",\"params\":{\"memberId\":\"AQIDBA==\"}}"
|
||||
#==# XGrpMemConAll (MemberId "\1\2\3\4")
|
||||
it "x.grp.mem.del" $
|
||||
"{\"event\":\"x.grp.mem.del\",\"params\":{\"memberId\":\"AQIDBA==\"}}"
|
||||
"{\"v\":\"1\",\"event\":\"x.grp.mem.del\",\"params\":{\"memberId\":\"AQIDBA==\"}}"
|
||||
#==# XGrpMemDel (MemberId "\1\2\3\4")
|
||||
it "x.grp.leave" $
|
||||
"{\"event\":\"x.grp.leave\",\"params\":{}}"
|
||||
"{\"v\":\"1\",\"event\":\"x.grp.leave\",\"params\":{}}"
|
||||
==# XGrpLeave
|
||||
it "x.grp.del" $
|
||||
"{\"event\":\"x.grp.del\",\"params\":{}}"
|
||||
"{\"v\":\"1\",\"event\":\"x.grp.del\",\"params\":{}}"
|
||||
==# XGrpDel
|
||||
it "x.info.probe" $
|
||||
"{\"event\":\"x.info.probe\",\"params\":{\"probe\":\"AQIDBA==\"}}"
|
||||
"{\"v\":\"1\",\"event\":\"x.info.probe\",\"params\":{\"probe\":\"AQIDBA==\"}}"
|
||||
#==# XInfoProbe (Probe "\1\2\3\4")
|
||||
it "x.info.probe.check" $
|
||||
"{\"event\":\"x.info.probe.check\",\"params\":{\"probeHash\":\"AQIDBA==\"}}"
|
||||
"{\"v\":\"1\",\"event\":\"x.info.probe.check\",\"params\":{\"probeHash\":\"AQIDBA==\"}}"
|
||||
#==# XInfoProbeCheck (ProbeHash "\1\2\3\4")
|
||||
it "x.info.probe.ok" $
|
||||
"{\"event\":\"x.info.probe.ok\",\"params\":{\"probe\":\"AQIDBA==\"}}"
|
||||
"{\"v\":\"1\",\"event\":\"x.info.probe.ok\",\"params\":{\"probe\":\"AQIDBA==\"}}"
|
||||
#==# XInfoProbeOk (Probe "\1\2\3\4")
|
||||
it "x.ok" $
|
||||
"{\"event\":\"x.ok\",\"params\":{}}"
|
||||
"{\"v\":\"1\",\"event\":\"x.ok\",\"params\":{}}"
|
||||
==# XOk
|
||||
|
Loading…
Reference in New Issue
Block a user