core: fix connecting to prepared group incognito; test connecting to prepared contact incognito (#5994)

This commit is contained in:
spaced4ndy
2025-06-17 14:20:47 +00:00
committed by GitHub
parent e3e9ae2ffd
commit 94d866e2c0
9 changed files with 181 additions and 55 deletions

View File

@@ -688,10 +688,10 @@ data ChatResponse
| CRNewPreparedGroup {user :: User, groupInfo :: GroupInfo}
| CRContactUserChanged {user :: User, fromContact :: Contact, newUser :: User, toContact :: Contact}
| CRGroupUserChanged {user :: User, fromGroup :: GroupInfo, newUser :: User, toGroup :: GroupInfo}
| CRSentConfirmation {user :: User, connection :: PendingContactConnection}
| CRSentConfirmation {user :: User, connection :: PendingContactConnection, customUserProfile :: Maybe Profile}
| CRSentInvitation {user :: User, connection :: PendingContactConnection, customUserProfile :: Maybe Profile}
| CRStartedConnectionToContact {user :: User, contact :: Contact}
| CRStartedConnectionToGroup {user :: User, groupInfo :: GroupInfo}
| CRStartedConnectionToContact {user :: User, contact :: Contact, customUserProfile :: Maybe Profile}
| CRStartedConnectionToGroup {user :: User, groupInfo :: GroupInfo, customUserProfile :: Maybe Profile}
| CRSentInvitationToContact {user :: User, contact :: Contact, customUserProfile :: Maybe Profile}
| CRItemsReadForChat {user :: User, chatInfo :: AChatInfo}
| CRContactDeleted {user :: User, contact :: Contact}

View File

@@ -1763,7 +1763,7 @@ processChatCommand' vr = \case
Nothing -> throwCmdError "contact doesn't have link to connect"
Just (ACCL SCMInvitation ccLink) ->
connectViaInvitation user incognito ccLink (Just contactId) >>= \case
CRSentConfirmation {} -> do
CRSentConfirmation {customUserProfile} -> do
-- get updated contact with connection
ct' <- withFastStore $ \db -> getContact db vr user contactId
forM_ msgContent_ $ \mc -> do
@@ -1771,16 +1771,16 @@ processChatCommand' vr = \case
(msg, _) <- sendDirectContactMessage user ct' evt
ci <- saveSndChatItem user (CDDirectSnd ct') msg (CISndMsgContent mc)
toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDSnd (DirectChat ct') ci]
pure $ CRStartedConnectionToContact user ct'
pure $ CRStartedConnectionToContact user ct' customUserProfile
cr -> pure cr
Just (ACCL SCMContact ccLink) ->
connectViaContact user incognito ccLink msgContent_ (Just $ CGMContactId contactId) >>= \case
CRSentInvitation {} -> do
connectViaContact user incognito ccLink msgContent_ (Just $ ACCGContact contactId) >>= \case
CRSentInvitation {customUserProfile} -> do
-- get updated contact with connection
ct' <- withFastStore $ \db -> getContact db vr user contactId
forM_ msgContent_ $ \mc ->
createInternalChatItem user (CDDirectSnd ct') (CISndMsgContent mc) Nothing
pure $ CRStartedConnectionToContact user ct'
pure $ CRStartedConnectionToContact user ct' customUserProfile
cr -> pure cr
APIConnectPreparedGroup groupId incognito -> withUser $ \user -> do
(gInfo, hostMember) <- withFastStore $ \db -> (,) <$> getGroupInfo db vr user groupId <*> getHostMember db vr user groupId
@@ -1788,12 +1788,11 @@ processChatCommand' vr = \case
case connLinkToConnect of
Nothing -> throwCmdError "group doesn't have link to connect"
Just ccLink ->
connectViaContact user incognito ccLink Nothing (Just $ CGMGroupMemberId (groupMemberId' hostMember)) >>= \case
CRSentInvitation {connection = PendingContactConnection {pccConnId}} -> do
gInfo' <- withStore' $ \db -> do
setViaGroupLinkHash db groupId pccConnId
setGroupConnLinkStartedConnection db gInfo True
pure $ CRStartedConnectionToGroup user gInfo'
connectViaContact user incognito ccLink Nothing (Just $ ACCGGroup gInfo (groupMemberId' hostMember)) >>= \case
CRSentInvitation {customUserProfile} -> do
-- get updated group info (connLinkStartedConnection and incognito membership)
gInfo' <- withFastStore $ \db -> getGroupInfo db vr user groupId
pure $ CRStartedConnectionToGroup user gInfo' customUserProfile
cr -> pure cr
APIConnect userId incognito (Just (ACCL SCMInvitation ccLink)) mc_ -> withUserId userId $ \user -> do
when (isJust mc_) $ throwChatError CEConnReqMessageProhibited
@@ -2874,13 +2873,13 @@ processChatCommand' vr = \case
(sqSecured, _serviceId) <- withAgent $ \a -> joinConnection a (aUserId user) connId True cReq dm pqSup' subMode
let newStatus = if sqSecured then ConnSndReady else ConnJoined
withFastStore' $ \db -> updateConnectionStatusFromTo db pccConnId ConnPrepared newStatus
pure $ CRSentConfirmation user pcc {pccConnStatus = newStatus}
pure $ CRSentConfirmation user pcc {pccConnStatus = newStatus} incognitoProfile
cReqs =
( CRInvitationUri crData {crScheme = SSSimplex} e2e,
CRInvitationUri crData {crScheme = simplexChat} e2e
)
connectViaContact :: User -> IncognitoEnabled -> CreatedLinkContact -> Maybe MsgContent -> Maybe ContactOrGroupMemberId -> CM ChatResponse
connectViaContact user@User {userId} incognito (CCLink cReq@(CRContactUri ConnReqUriData {crClientData}) sLnk) mc_ comId_ = withInvitationLock "connectViaContact" (strEncode cReq) $ do
connectViaContact :: User -> IncognitoEnabled -> CreatedLinkContact -> Maybe MsgContent -> Maybe AttachConnToContactOrGroup -> CM ChatResponse
connectViaContact user@User {userId} incognito (CCLink cReq@(CRContactUri ConnReqUriData {crClientData}) sLnk) mc_ attachConnTo_ = withInvitationLock "connectViaContact" (strEncode cReq) $ do
let groupLinkId = crClientData >>= decodeJSON >>= \(CRDataGroup gli) -> Just gli
cReqHash = ConnReqUriHash . C.sha256Hash $ strEncode cReq
case groupLinkId of
@@ -2912,7 +2911,7 @@ processChatCommand' vr = \case
incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing
subMode <- chatReadVar subscriptionMode
let sLnk' = serverShortLink <$> sLnk
conn@PendingContactConnection {pccConnId} <- withFastStore' $ \db -> createConnReqConnection db userId connId cReqHash sLnk' comId_ xContactId incognitoProfile groupLinkId subMode chatV pqSup
conn@PendingContactConnection {pccConnId} <- withFastStore' $ \db -> createConnReqConnection db userId connId cReqHash sLnk' attachConnTo_ xContactId incognitoProfile groupLinkId subMode chatV pqSup
joinContact user pccConnId connId cReq incognitoProfile xContactId mc_ inGroup pqSup chatV
pure $ CRSentInvitation user conn incognitoProfile
connectContactViaAddress :: User -> IncognitoEnabled -> Contact -> CreatedLinkContact -> CM ChatResponse
@@ -3314,7 +3313,7 @@ processChatCommand' vr = \case
(cReq, cData) <- getShortLinkConnReq user l'
let cl = ACCL SCMContact $ CCLink cReq (Just l')
withFastStore' (\db -> getContactWithoutConnViaShortAddress db vr user l') >>= \case
Just ct -> pure (cl, CPContactAddress (CAPContactViaAddress ct))
Just ct' -> pure (cl, CPContactAddress (CAPContactViaAddress ct'))
Nothing -> do
let contactSLinkData_ = J.decodeStrict $ linkUserData' cData
plan <- contactRequestPlan user cReq contactSLinkData_

View File

@@ -153,13 +153,13 @@ deletePendingContactConnection db userId connId =
createAddressContactConnection :: DB.Connection -> VersionRangeChat -> User -> Contact -> ConnId -> ConnReqUriHash -> Maybe ShortLinkContact -> XContactId -> Maybe Profile -> SubscriptionMode -> VersionChat -> PQSupport -> ExceptT StoreError IO (Int64, Contact)
createAddressContactConnection db vr user@User {userId} Contact {contactId} acId cReqHash sLnk xContactId incognitoProfile subMode chatV pqSup = do
PendingContactConnection {pccConnId} <- liftIO $ createConnReqConnection db userId acId cReqHash sLnk (Just $ CGMContactId contactId) xContactId incognitoProfile Nothing subMode chatV pqSup
PendingContactConnection {pccConnId} <- liftIO $ createConnReqConnection db userId acId cReqHash sLnk (Just $ ACCGContact contactId) xContactId incognitoProfile Nothing subMode chatV pqSup
(pccConnId,) <$> getContact db vr user contactId
createConnReqConnection :: DB.Connection -> UserId -> ConnId -> ConnReqUriHash -> Maybe ShortLinkContact -> Maybe ContactOrGroupMemberId -> XContactId -> Maybe Profile -> Maybe GroupLinkId -> SubscriptionMode -> VersionChat -> PQSupport -> IO PendingContactConnection
createConnReqConnection db userId acId cReqHash sLnk comId_ xContactId incognitoProfile groupLinkId subMode chatV pqSup = do
createdAt <- getCurrentTime
customUserProfileId <- mapM (createIncognitoProfile_ db userId createdAt) incognitoProfile
createConnReqConnection :: DB.Connection -> UserId -> ConnId -> ConnReqUriHash -> Maybe ShortLinkContact -> Maybe AttachConnToContactOrGroup -> XContactId -> Maybe Profile -> Maybe GroupLinkId -> SubscriptionMode -> VersionChat -> PQSupport -> IO PendingContactConnection
createConnReqConnection db userId acId cReqHash sLnk attachConnTo_ xContactId incognitoProfile groupLinkId subMode chatV pqSup = do
currentTs <- getCurrentTime
customUserProfileId <- mapM (createIncognitoProfile_ db userId currentTs) incognitoProfile
let pccConnStatus = ConnJoined
DB.execute
db
@@ -174,15 +174,29 @@ createConnReqConnection db userId acId cReqHash sLnk comId_ xContactId incognito
( (userId, acId, pccConnStatus, connType, BI True)
:. (cReqHash, sLnk, contactId_, groupMemberId_)
:. (xContactId, customUserProfileId, BI (isJust groupLinkId), groupLinkId)
:. (createdAt, createdAt, BI (subMode == SMOnlyCreate), chatV, pqSup, pqSup)
:. (currentTs, currentTs, BI (subMode == SMOnlyCreate), chatV, pqSup, pqSup)
)
pccConnId <- insertedRowId db
pure PendingContactConnection {pccConnId, pccAgentConnId = AgentConnId acId, pccConnStatus, viaContactUri = True, viaUserContactLink = Nothing, groupLinkId, customUserProfileId, connLinkInv = Nothing, localAlias = "", createdAt, updatedAt = createdAt}
case attachConnTo_ of
Just (ACCGGroup gInfo _gmId) -> updatePreparedGroup gInfo pccConnId customUserProfileId currentTs
_ -> pure ()
pure PendingContactConnection {pccConnId, pccAgentConnId = AgentConnId acId, pccConnStatus, viaContactUri = True, viaUserContactLink = Nothing, groupLinkId, customUserProfileId, connLinkInv = Nothing, localAlias = "", createdAt = currentTs, updatedAt = currentTs}
where
(connType, contactId_, groupMemberId_) = case comId_ of
Just (CGMContactId ctId) -> (ConnContact, Just ctId, Nothing)
Just (CGMGroupMemberId gmId) -> (ConnMember, Nothing, Just gmId)
(connType, contactId_, groupMemberId_) = case attachConnTo_ of
Just (ACCGContact ctId) -> (ConnContact, Just ctId, Nothing)
Just (ACCGGroup _gInfo gmId) -> (ConnMember, Nothing, Just gmId)
Nothing -> (ConnContact, Nothing, Nothing)
updatePreparedGroup GroupInfo {groupId, membership} pccConnId customUserProfileId currentTs = do
setViaGroupLinkHash db groupId pccConnId
DB.execute
db
"UPDATE groups SET conn_link_started_connection = ?, updated_at = ? WHERE group_id = ?"
(BI True, currentTs, groupId)
when (isJust customUserProfileId) $
DB.execute
db
"UPDATE group_members SET member_profile_id = ?, updated_at = ? WHERE group_member_id = ?"
(customUserProfileId, currentTs, groupMemberId' membership)
getConnReqContactXContactId :: DB.Connection -> VersionRangeChat -> User -> ConnReqUriHash -> IO (Maybe Contact, Maybe XContactId)
getConnReqContactXContactId db vr user@User {userId} cReqHash = do

View File

@@ -37,12 +37,10 @@ module Simplex.Chat.Store.Groups
deleteContactCardKeepConn,
createPreparedGroup,
updatePreparedGroupUser,
setGroupConnLinkStartedConnection,
updatePreparedUserAndHostMembersInvited,
updatePreparedUserAndHostMembersRejected,
createGroupInvitedViaLink,
createGroupRejectedViaLink,
setViaGroupLinkHash,
setGroupInvitationChatItemId,
getGroup,
getGroupInfo,
@@ -672,15 +670,6 @@ updatePreparedGroupUser db vr user gInfo@GroupInfo {groupId, membership} hostMem
(newUserId, currentTs, hostProfileId)
safeDeleteLDN db user oldHostLDN
setGroupConnLinkStartedConnection :: DB.Connection -> GroupInfo -> Bool -> IO GroupInfo
setGroupConnLinkStartedConnection db groupInfo@GroupInfo {groupId} connLinkStartedConnection = do
currentTs <- getCurrentTime
DB.execute
db
"UPDATE groups SET conn_link_started_connection = ?, updated_at = ? WHERE group_id = ?"
(BI connLinkStartedConnection, currentTs, groupId)
pure groupInfo {connLinkStartedConnection = connLinkStartedConnection}
updatePreparedUserAndHostMembersInvited :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> GroupMember -> GroupLinkInvitation -> ExceptT StoreError IO (GroupInfo, GroupMember)
updatePreparedUserAndHostMembersInvited db vr user gInfo hostMember GroupLinkInvitation {fromMember, fromMemberName, invitedMember, groupProfile, accepted} = do
let fromMemberProfile = profileFromName fromMemberName
@@ -815,17 +804,6 @@ createGroup_ db userId groupProfile connLinkToConnect business currentTs = Excep
groupId <- insertedRowId db
pure (groupId, localDisplayName)
setViaGroupLinkHash :: DB.Connection -> GroupId -> Int64 -> IO ()
setViaGroupLinkHash db groupId connId =
DB.execute
db
[sql|
UPDATE groups
SET via_group_link_uri_hash = (SELECT via_contact_uri_hash FROM connections WHERE connection_id = ?)
WHERE group_id = ?
|]
(connId, groupId)
setGroupInvitationChatItemId :: DB.Connection -> User -> GroupId -> ChatItemId -> IO ()
setGroupInvitationChatItemId db User {userId} groupId chatItemId = do
currentTs <- getCurrentTime

View File

@@ -6132,6 +6132,10 @@ Query: UPDATE group_members SET member_id = ?, member_role = ? WHERE group_membe
Plan:
SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?)
Query: UPDATE group_members SET member_profile_id = ?, updated_at = ? WHERE group_member_id = ?
Plan:
SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?)
Query: UPDATE group_members SET member_role = ? WHERE user_id = ? AND group_member_id = ?
Plan:
SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?)

View File

@@ -728,3 +728,14 @@ addGroupChatTags :: DB.Connection -> GroupInfo -> IO GroupInfo
addGroupChatTags db g@GroupInfo {groupId} = do
chatTags <- getGroupChatTags db groupId
pure (g :: GroupInfo) {chatTags}
setViaGroupLinkHash :: DB.Connection -> GroupId -> Int64 -> IO ()
setViaGroupLinkHash db groupId connId =
DB.execute
db
[sql|
UPDATE groups
SET via_group_link_uri_hash = (SELECT via_contact_uri_hash FROM connections WHERE connection_id = ?)
WHERE group_id = ?
|]
(connId, groupId)

View File

@@ -459,7 +459,7 @@ data GroupSummary = GroupSummary
data ContactOrGroup = CGContact Contact | CGGroup GroupInfo [GroupMember]
data ContactOrGroupMemberId = CGMContactId ContactId | CGMGroupMemberId GroupMemberId
data AttachConnToContactOrGroup = ACCGContact ContactId | ACCGGroup GroupInfo GroupMemberId
contactAndGroupIds :: ContactOrGroup -> (Maybe ContactId, Maybe GroupId)
contactAndGroupIds = \case

View File

@@ -194,10 +194,10 @@ chatResponseToView hu cfg@ChatConfig {logLevel, showReactions, testView} liveIte
CRNewPreparedGroup u g -> ttyUser u [ttyGroup' g <> ": group is prepared"]
CRContactUserChanged u c nu c' -> ttyUser u $ viewContactUserChanged u c nu c'
CRGroupUserChanged u g nu g' -> ttyUser u $ viewGroupUserChanged u g nu g'
CRSentConfirmation u _ -> ttyUser u ["confirmation sent!"]
CRSentConfirmation u _ _customUserProfile -> ttyUser u ["confirmation sent!"]
CRSentInvitation u _ customUserProfile -> ttyUser u $ viewSentInvitation customUserProfile testView
CRStartedConnectionToContact u c -> ttyUser u [ttyContact' c <> ": connection started"]
CRStartedConnectionToGroup u g -> ttyUser u [ttyGroup' g <> ": connection started"]
CRStartedConnectionToContact u c customUserProfile -> ttyUser u $ viewStartedConnectionToContact c customUserProfile testView
CRStartedConnectionToGroup u g customUserProfile -> ttyUser u $ viewStartedConnectionToGroup g customUserProfile testView
CRSentInvitationToContact u _c customUserProfile -> ttyUser u $ viewSentInvitation customUserProfile testView
CRItemsReadForChat u _chatId -> ttyUser u ["items read for chat"]
CRContactDeleted u c -> ttyUser u [ttyContact' c <> ": contact is deleted"]
@@ -1123,6 +1123,28 @@ viewSentInvitation incognitoProfile testView =
message = ["connection request sent incognito!"]
Nothing -> ["connection request sent!"]
viewStartedConnectionToContact :: Contact -> Maybe Profile -> Bool -> [StyledString]
viewStartedConnectionToContact ct incognitoProfile testView =
case incognitoProfile of
Just profile ->
if testView
then incognitoProfile' profile : message
else message
where
message = [ttyContact' ct <> ": connection started incognito"]
Nothing -> [ttyContact' ct <> ": connection started"]
viewStartedConnectionToGroup :: GroupInfo -> Maybe Profile -> Bool -> [StyledString]
viewStartedConnectionToGroup g incognitoProfile testView =
case incognitoProfile of
Just profile ->
if testView
then incognitoProfile' profile : message
else message
where
message = [ttyGroup' g <> ": connection started incognito"]
Nothing -> [ttyGroup' g <> ": connection started"]
viewAcceptingContactRequest :: Contact -> [StyledString]
viewAcceptingContactRequest ct
| contactReady ct = [ttyFullContact ct <> ": accepting contact request, you can send messages to contact"]

View File

@@ -115,8 +115,11 @@ chatProfileTests = do
it "prepare contact using address short link data and connect" testShortLinkAddressPrepareContact
it "prepare group using group short link data and connect" testShortLinkPrepareGroup
it "prepare group using group short link data and connect, host rejects" testShortLinkPrepareGroupReject
it "connect to prepared contact incognito (via invitation)" testShortLinkInvitationConnectPreparedContactIncognito
it "connect to prepared contact incognito (via address)" testShortLinkAddressConnectPreparedContactIncognito
it "change prepared contact user" testShortLinkChangePreparedContactUser
it "change prepared contact user, new user has contact with the same name" testShortLinkChangePreparedContactUserDuplicate
it "connect to prepared group incognito" testShortLinkConnectPreparedGroupIncognito
it "change prepared group user" testShortLinkChangePreparedGroupUser
it "change prepared group user, new user has group with the same name" testShortLinkChangePreparedGroupUserDuplicate
it "setting incognito for invitation should update short link data" testShortLinkInvitationSetIncognito
@@ -3012,6 +3015,63 @@ testShortLinkPrepareGroupReject =
where
cfg = testCfg {chatHooks = defaultChatHooks {acceptMember = Just (\_ _ _ -> pure $ Left GRRBlockedName)}}
testShortLinkInvitationConnectPreparedContactIncognito :: HasCallStack => TestParams -> IO ()
testShortLinkInvitationConnectPreparedContactIncognito =
testChat2 aliceProfile bobProfile $
\alice bob -> do
alice ##> "/_connect 1"
(shortLink, fullLink) <- getInvitations alice
bob ##> ("/_connect plan 1 " <> shortLink)
bob <## "invitation link: ok to connect"
contactSLinkData <- getTermLine bob
bob ##> ("/_prepare contact 1 " <> fullLink <> " " <> shortLink <> " " <> contactSLinkData)
bob <## "alice: contact is prepared"
bob ##> "/_connect contact @2 incognito=on"
bobIncognito <- getTermLine bob
bob <## "alice: connection started incognito"
_ <- getTermLine bob
concurrentlyN_
[ alice <## (bobIncognito <> ": contact is connected"),
do
bob <## ("alice (Alice): contact is connected, your incognito profile for this contact is " <> bobIncognito)
bob <## ("use /i alice to print out this incognito profile again")
]
alice #> ("@" <> bobIncognito <> " hi")
bob ?<# "alice> hi"
bob ?#> "@alice hey"
alice <# (bobIncognito <> "> hey")
testShortLinkAddressConnectPreparedContactIncognito :: HasCallStack => TestParams -> IO ()
testShortLinkAddressConnectPreparedContactIncognito =
testChat2 aliceProfile bobProfile $
\alice bob -> do
alice ##> "/ad"
(shortLink, fullLink) <- getContactLinks alice True
bob ##> ("/_connect plan 1 " <> shortLink)
bob <## "contact address: ok to connect"
contactSLinkData <- getTermLine bob
bob ##> ("/_prepare contact 1 " <> fullLink <> " " <> shortLink <> " " <> contactSLinkData)
bob <## "alice: contact is prepared"
bob ##> "/_connect contact @2 incognito=on"
bobIncognito <- getTermLine bob
bob <## "alice: connection started incognito"
alice <## (bobIncognito <> " wants to connect to you!")
alice <## ("to accept: /ac " <> bobIncognito)
alice <## ("to reject: /rc " <> bobIncognito <> " (the sender will NOT be notified)")
alice ##> ("/ac " <> bobIncognito)
alice <## (bobIncognito <> ": accepting contact request, you can send messages to contact")
_ <- getTermLine bob
concurrentlyN_
[ alice <## (bobIncognito <> ": contact is connected"),
do
bob <## ("alice (Alice): contact is connected, your incognito profile for this contact is " <> bobIncognito)
bob <## ("use /i alice to print out this incognito profile again")
]
alice #> ("@" <> bobIncognito <> " hi")
bob ?<# "alice> hi"
bob ?#> "@alice hey"
alice <# (bobIncognito <> "> hey")
testShortLinkChangePreparedContactUser :: HasCallStack => TestParams -> IO ()
testShortLinkChangePreparedContactUser =
testChat2 aliceProfile bobProfile $
@@ -3114,6 +3174,44 @@ testShortLinkChangePreparedContactUserDuplicate =
bob @@@ []
bob `hasContactProfiles` ["bob"]
testShortLinkConnectPreparedGroupIncognito :: HasCallStack => TestParams -> IO ()
testShortLinkConnectPreparedGroupIncognito =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
createGroup2 "team" alice cath
alice ##> "/create link #team"
(shortLink, fullLink) <- getGroupLinks alice "team" GRMember True
bob ##> ("/_connect plan 1 " <> shortLink)
bob <## "group link: ok to connect"
groupSLinkData <- getTermLine bob
bob ##> ("/_prepare group 1 " <> fullLink <> " " <> shortLink <> " " <> groupSLinkData)
bob <## "#team: group is prepared"
bob ##> "/_connect group #1 incognito=on"
bobIncognito <- getTermLine bob
bob <## "#team: connection started incognito"
alice <## (bobIncognito <> ": accepting request to join group #team...")
concurrentlyN_
[ alice <## ("#team: " <> bobIncognito <> " joined the group"),
do
bob <## "#team: joining the group..."
bob <## ("#team: you joined the group incognito as " <> bobIncognito)
bob <## "#team: member cath (Catherine) is connected",
do
cath <## ("#team: alice added " <> bobIncognito <> " to the group (connecting...)")
cath <## ("#team: new member " <> bobIncognito <> " is connected")
]
alice #> "#team 1"
bob ?<# "#team alice> 1"
cath <# "#team alice> 1"
bob ?#> "#team 2"
[alice, cath] *<# ("#team " <> bobIncognito <> "> 2")
cath #> "#team 3"
alice <# "#team cath> 3"
bob ?<# "#team cath> 3"
testShortLinkChangePreparedGroupUser :: HasCallStack => TestParams -> IO ()
testShortLinkChangePreparedGroupUser =
testChat3 aliceProfile bobProfile cathProfile $