From c4675bf89f82a6b517852dacf4d653ead370b13d Mon Sep 17 00:00:00 2001 From: "Evgeny @ SimpleX Chat" <259188159+evgeny-simplex@users.noreply.github.com> Date: Sat, 1 Aug 2026 19:13:33 +0000 Subject: [PATCH] add key distribution steps, and fix some tests --- src/Simplex/Chat/Controller.hs | 2 +- src/Simplex/Chat/Library/Commands.hs | 19 +- src/Simplex/Chat/Library/Internal.hs | 2 + src/Simplex/Chat/Library/Subscriber.hs | 27 +- src/Simplex/Chat/Protocol.hs | 8 +- src/Simplex/Chat/Store/Groups.hs | 67 ++-- src/Simplex/Chat/Terminal/Input.hs | 2 +- src/Simplex/Chat/Types.hs | 2 + src/Simplex/Chat/View.hs | 2 +- tests/ChatTests/Groups.hs | 512 ++++++++++++------------- tests/ChatTests/Profiles.hs | 52 +-- tests/ProtocolTests.hs | 6 +- 12 files changed, 361 insertions(+), 340 deletions(-) diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 528c30dfdf..598b602ef7 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -856,7 +856,7 @@ data ChatResponse | CRAcceptingContactRequest {user :: User, contact :: Contact} | CRContactAlreadyExists {user :: User, contact :: Contact} | CRLeftMemberUser {user :: User, groupInfo :: GroupInfo} - | CRGroupDeletedUser {user :: User, groupInfo :: GroupInfo, msgSigned :: Bool} + | CRGroupDeletedUser {user :: User, groupInfo :: GroupInfo, msgSigned :: Bool, localDeletion :: Bool} | CRForwardPlan {user :: User, itemsCount :: Int, chatItemIds :: [ChatItemId], forwardConfirmation :: Maybe ForwardConfirmation} | CRChatMsgContent {user :: User, msgContent :: MsgContent} | CRRcvFileAccepted {user :: User, chatItem :: AChatItem} diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index 12ad89563d..107241a766 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -1357,7 +1357,7 @@ processChatCommand cxt nm = \case withFastStore' $ \db -> cleanupHostGroupLinkConn db user gInfo withFastStore' $ \db -> deleteGroupMembers db user gInfo withFastStore' $ \db -> deleteGroup db user gInfo - pure $ CRGroupDeletedUser user gInfo msgSigned + pure $ CRGroupDeletedUser user gInfo msgSigned (not doSendDel) where getRecipients gInfo | useRelays' gInfo = do @@ -2639,15 +2639,15 @@ processChatCommand cxt nm = \case APINewGroup userId incognito gProfile -> withUserId userId $ \user -> do g <- asks random memberId <- liftIO $ MemberId <$> encodedRandomBytes g 12 - (_, memberPrivKey) <- liftIO $ atomically $ C.generateKeyPair g - gInfo <- newGroup user incognito gProfile False memberId (Just GroupKeys {publicGroupKeys = Nothing, memberPrivKey}) Nothing + memberKeys <- liftIO $ atomically $ C.generateKeyPair g + gInfo <- newGroup user incognito gProfile False memberId memberKeys Nothing Nothing createNewGroupItems user gInfo pure $ CRGroupCreated user gInfo NewGroup incognito gProfile -> withUser $ \User {userId} -> processChatCommand cxt nm $ APINewGroup userId incognito gProfile APINewPublicGroup userId incognito relayIds groupProfile -> withUserId userId $ \user -> do - (gProfile', memberId, groupKeys, setupLink) <- prepareGroupLink user - gInfo <- newGroup user incognito gProfile' True memberId (Just groupKeys) (Just 1) + (gProfile', memberId, GroupKeys {publicGroupKeys, memberPrivKey}, setupLink) <- prepareGroupLink user + gInfo <- newGroup user incognito gProfile' True memberId (C.publicKey memberPrivKey, memberPrivKey) publicGroupKeys (Just 1) (gLink, results) <- setupLink gInfo `catchAllErrors` \e -> do deleteInProgressGroup user gInfo throwError e @@ -2784,7 +2784,7 @@ processChatCommand cxt nm = \case case activeConn of Just Connection {peerChatVRange} -> do subMode <- chatReadVar subscriptionMode - dm <- encodeConnInfo $ XGrpAcpt membershipMemId + dm <- encodeConnInfo $ XGrpAcpt membershipMemId (groupMemberKey g) agentConnId <- case memberConn fromMember of Nothing -> do agentConnId <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True connRequest PQSupportOff @@ -4178,14 +4178,14 @@ processChatCommand cxt nm = \case groupId <- getGroupIdByName db user gName groupMemberId <- getGroupMemberIdByName db user groupId groupMemberName pure (groupId, groupMemberId) - newGroup :: User -> IncognitoEnabled -> GroupProfile -> Bool -> MemberId -> Maybe GroupKeys -> Maybe Int64 -> CM GroupInfo - newGroup user incognito gProfile@GroupProfile {displayName, image} useRelays memberId groupKeys_ publicMemberCount_ = do + newGroup :: User -> IncognitoEnabled -> GroupProfile -> Bool -> MemberId -> C.KeyPairEd25519 -> Maybe PublicGroupKeys -> Maybe Int64 -> CM GroupInfo + newGroup user incognito gProfile@GroupProfile {displayName, image} useRelays memberId memberKeys publicGroupKeys publicMemberCount_ = do checkValidName displayName checkProfileImageSize image checkGroupProfileSize gProfile -- [incognito] generate incognito profile for group membership incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing - withFastStore $ \db -> createNewGroup db cxt user gProfile incognitoProfile useRelays memberId groupKeys_ publicMemberCount_ + withFastStore $ \db -> createNewGroup db cxt user gProfile incognitoProfile useRelays memberId memberKeys publicGroupKeys publicMemberCount_ createNewGroupItems :: User -> GroupInfo -> CM () createNewGroupItems user gInfo = do let cd = CDGroupSnd gInfo Nothing @@ -4199,6 +4199,7 @@ processChatCommand cxt nm = \case groupInv = GroupInvitation { fromMember = MemberIdRole userMemberId userRole, + fromMemberKey = groupMemberKey gInfo, invitedMember = MemberIdRole memberId memRole, connRequest = cReq, groupProfile, diff --git a/src/Simplex/Chat/Library/Internal.hs b/src/Simplex/Chat/Library/Internal.hs index 88359f1c80..3fb5121a81 100644 --- a/src/Simplex/Chat/Library/Internal.hs +++ b/src/Simplex/Chat/Library/Internal.hs @@ -980,6 +980,7 @@ acceptGroupJoinRequestAsync GroupLinkInvitation { fromMember = MemberIdRole userMemberId userRole, fromMemberName = displayName, + fromMemberKey = groupMemberKey gInfo, invitedMember = MemberIdRole memberId gLinkMemRole, groupProfile, accepted = Just gAccepted, @@ -1043,6 +1044,7 @@ acceptBusinessJoinRequestAsync GroupLinkInvitation { fromMember = MemberIdRole userMemberId userRole, fromMemberName = displayName, + fromMemberKey = groupMemberKey gInfo, invitedMember = MemberIdRole memberId GRMember, groupProfile = businessGroupProfile userProfile groupPreferences, accepted = Just GAAccepted, diff --git a/src/Simplex/Chat/Library/Subscriber.hs b/src/Simplex/Chat/Library/Subscriber.hs index 508efc9eb5..e323a3599a 100644 --- a/src/Simplex/Chat/Library/Subscriber.hs +++ b/src/Simplex/Chat/Library/Subscriber.hs @@ -477,7 +477,11 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = Just gInfo -> userProfileInGroup user gInfo (fromLocalProfile <$> incognitoProfile) Nothing -> userProfileDirect user (fromLocalProfile <$> incognitoProfile) Nothing True -- [async agent commands] no continuation needed, but command should be asynchronous for stability - allowAgentConnectionAsync user conn'' confId $ XInfo profileToSend Nothing + case gInfo_ of + Just gInfo | Just gks <- groupKeys gInfo, maxVersion (peerChatVRange conn'') >= relayWebCapVersion -> do + dm <- encodeSignedGroupConnInfo gInfo gks $ XInfo profileToSend (groupMemberKey gInfo) + allowAgentConnectionInfo user conn'' confId dm + _ -> allowAgentConnectionAsync user conn'' confId $ XInfo profileToSend Nothing INFO pqSupport connInfo -> do processINFOpqSupport conn pqSupport void $ saveConnInfo conn connInfo @@ -594,9 +598,11 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = void $ withStore' $ \db -> resetMemberContactFields db ct' XGrpLinkInv glInv -> do -- XGrpLinkInv here means we are connecting via business contact card, so we replace contact with group + g <- asks random + memberKeys <- liftIO $ atomically $ C.generateKeyPair g (gInfo, host) <- withStore $ \db -> do liftIO $ deleteContactCardKeepConn db connId ct - createGroupInvitedViaLink db cxt user conn'' glInv + createGroupInvitedViaLink db cxt user conn'' memberKeys glInv void $ createChatItem user (CDGroupSnd gInfo Nothing) False CIChatBanner Nothing Nothing (Just epochStart) -- [incognito] send saved profile incognitoProfile <- forM customUserProfileId $ \pId -> withStore (\db -> getProfileById db userId pId) @@ -747,9 +753,10 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = case memberCategory m of GCInviteeMember -> case chatMsgEvent of - XGrpAcpt memId + XGrpAcpt memId mKey | sameMemberId memId m -> do withStore $ \db -> liftIO $ updateGroupMemberStatus db userId m GSMemAccepted + forM_ mKey $ \(MemberKey k) -> withStore' $ \db -> setMemberPubKey db (groupMemberId' m) k -- [async agent commands] no continuation needed, but command should be asynchronous for stability allowAgentConnectionAsync user conn' confId XOk | otherwise -> messageError "x.grp.acpt: memberId is different from expected" @@ -2607,13 +2614,15 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = when (fromRole < GRAdmin || fromRole < memRole) $ throwChatError (CEGroupContactRole c) when (fromMemId == memId) $ throwChatError CEGroupDuplicateMemberId -- [incognito] if direct connection with host is incognito, create membership using the same incognito profile - (gInfo@GroupInfo {groupId, localDisplayName, groupProfile, membership}, hostId) <- withStore $ \db -> createGroupInvitation db cxt user ct inv customUserProfileId + g <- asks random + memberKeys <- liftIO $ atomically $ C.generateKeyPair g + (gInfo@GroupInfo {groupId, localDisplayName, groupProfile, membership}, hostId) <- withStore $ \db -> createGroupInvitation db cxt user ct inv customUserProfileId memberKeys void $ createChatItem user (CDGroupSnd gInfo Nothing) False CIChatBanner Nothing Nothing (Just epochStart) let GroupMember {groupMemberId, memberId = membershipMemId} = membership if sameGroupLinkId groupLinkId groupLinkId' then do subMode <- chatReadVar subscriptionMode - dm <- encodeConnInfo $ XGrpAcpt membershipMemId + dm <- encodeConnInfo $ XGrpAcpt membershipMemId (groupMemberKey gInfo) connIds@(cmdId, acId) <- prepareAgentJoin user Nothing True connRequest withStore' $ \db -> do setViaGroupLinkUri db groupId connId @@ -3072,11 +3081,15 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = toView $ CEvtContactConnecting user ct pure (conn', Nothing) XGrpLinkInv glInv -> do - (gInfo, host) <- withStore $ \db -> createGroupInvitedViaLink db cxt user conn' glInv + g <- asks random + memberKeys <- liftIO $ atomically $ C.generateKeyPair g + (gInfo, host) <- withStore $ \db -> createGroupInvitedViaLink db cxt user conn' memberKeys glInv toView $ CEvtGroupLinkConnecting user gInfo host pure (conn', Just gInfo) XGrpLinkReject glRjct@GroupLinkRejection {rejectionReason} -> do - (gInfo, host) <- withStore $ \db -> createGroupRejectedViaLink db cxt user conn' glRjct + g <- asks random + memberKeys <- liftIO $ atomically $ C.generateKeyPair g + (gInfo, host) <- withStore $ \db -> createGroupRejectedViaLink db cxt user conn' memberKeys glRjct toView $ CEvtGroupLinkConnecting user gInfo host toViewTE $ TEGroupLinkRejected user gInfo rejectionReason pure (conn', Just gInfo) diff --git a/src/Simplex/Chat/Protocol.hs b/src/Simplex/Chat/Protocol.hs index 0ae4012561..ede67f6ce9 100644 --- a/src/Simplex/Chat/Protocol.hs +++ b/src/Simplex/Chat/Protocol.hs @@ -464,7 +464,7 @@ data ChatMsgEvent (e :: MsgEncoding) where XMember :: {profile :: Profile, newMemberId :: MemberId, newMemberKey :: MemberKey, viaRelay :: Maybe MemberId} -> ChatMsgEvent 'Json XDirectDel :: ChatMsgEvent 'Json XGrpInv :: GroupInvitation -> ChatMsgEvent 'Json - XGrpAcpt :: MemberId -> ChatMsgEvent 'Json + XGrpAcpt :: MemberId -> Maybe MemberKey -> ChatMsgEvent 'Json XGrpLinkInv :: GroupLinkInvitation -> ChatMsgEvent 'Json XGrpLinkReject :: GroupLinkRejection -> ChatMsgEvent 'Json XGrpLinkMem :: Profile -> Maybe MemberKey -> ChatMsgEvent 'Json @@ -1233,7 +1233,7 @@ toCMEventTag msg = case msg of XMember {} -> XMember_ XDirectDel -> XDirectDel_ XGrpInv _ -> XGrpInv_ - XGrpAcpt _ -> XGrpAcpt_ + XGrpAcpt {} -> XGrpAcpt_ XGrpLinkInv _ -> XGrpLinkInv_ XGrpLinkReject _ -> XGrpLinkReject_ XGrpLinkMem {} -> XGrpLinkMem_ @@ -1402,7 +1402,7 @@ appJsonToCM AppMessageJson {v, msgId, event, params} = do XMember_ -> XMember <$> p "profile" <*> p "newMemberId" <*> p "newMemberKey" <*> opt "viaRelay" XDirectDel_ -> pure XDirectDel XGrpInv_ -> XGrpInv <$> p "groupInvitation" - XGrpAcpt_ -> XGrpAcpt <$> p "memberId" + XGrpAcpt_ -> XGrpAcpt <$> p "memberId" <*> opt "memberKey" XGrpLinkInv_ -> XGrpLinkInv <$> p "groupLinkInvitation" XGrpLinkReject_ -> XGrpLinkReject <$> p "groupLinkRejection" XGrpLinkMem_ -> XGrpLinkMem <$> p "profile" <*> opt "memberKey" @@ -1478,7 +1478,7 @@ chatToAppMessage chatMsg@ChatMessage {chatVRange, msgId, chatMsgEvent} = case en XMember {profile, newMemberId, newMemberKey, viaRelay} -> o $ ("viaRelay" .=? viaRelay) ["profile" .= profile, "newMemberId" .= newMemberId, "newMemberKey" .= newMemberKey] XDirectDel -> JM.empty XGrpInv groupInv -> o ["groupInvitation" .= groupInv] - XGrpAcpt memId -> o ["memberId" .= memId] + XGrpAcpt memId memberKey -> o $ ("memberKey" .=? memberKey) ["memberId" .= memId] XGrpLinkInv groupLinkInv -> o ["groupLinkInvitation" .= groupLinkInv] XGrpLinkReject groupLinkRjct -> o ["groupLinkRejection" .= groupLinkRjct] XGrpLinkMem profile memberKey -> o $ ("memberKey" .=? memberKey) ["profile" .= profile] diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs index 217ae74448..7ddb9b572b 100644 --- a/src/Simplex/Chat/Store/Groups.hs +++ b/src/Simplex/Chat/Store/Groups.hs @@ -376,8 +376,8 @@ setGroupLinkShortLink db gLnk@GroupLink {userContactLinkId, connLinkContact = CC pure gLnk {connLinkContact = CCLink connFullLink (Just shortLink), shortLinkDataSet = True, shortLinkLargeDataSet = BoolDef True} -- | creates completely new group with a single member - the current user -createNewGroup :: DB.Connection -> StoreCxt -> User -> GroupProfile -> Maybe Profile -> Bool -> MemberId -> Maybe GroupKeys -> Maybe Int64 -> ExceptT StoreError IO GroupInfo -createNewGroup db cxt user@User {userId} groupProfile incognitoProfile useRelays memberId groupKeys publicMemberCount_ = ExceptT $ do +createNewGroup :: DB.Connection -> StoreCxt -> User -> GroupProfile -> Maybe Profile -> Bool -> MemberId -> C.KeyPairEd25519 -> Maybe PublicGroupKeys -> Maybe Int64 -> ExceptT StoreError IO GroupInfo +createNewGroup db cxt user@User {userId} groupProfile incognitoProfile useRelays memberId memberKeys publicGroupKeys publicMemberCount_ = ExceptT $ do let GroupProfile {displayName, fullName, shortDescr, description, image, publicGroup, groupPreferences, memberAdmission} = groupProfile (groupType_, groupLink_, publicGroupId_) = case publicGroup of Just PublicGroupProfile {groupType, groupLink, publicGroupId} -> (Just groupType, Just groupLink, Just publicGroupId) @@ -387,15 +387,12 @@ createNewGroup db cxt user@User {userId} groupProfile incognitoProfile useRelays currentTs <- getCurrentTime customUserProfileId <- mapM (createIncognitoProfile_ db userId currentTs) incognitoProfile withLocalDisplayName db userId displayName $ \ldn -> runExceptT $ do - let (rootPrivKey_, rootPubKey_, memberPrivKey_) = case groupKeys of - Nothing -> (Nothing, Nothing, Nothing) - Just GroupKeys {publicGroupKeys, memberPrivKey} -> - let (rpk, rpub) = case publicGroupKeys of - Just PublicGroupKeys {groupRootKey} -> case groupRootKey of - GRKPrivate pk -> (Just pk, Nothing) - GRKPublic k -> (Nothing, Just k) - Nothing -> (Nothing, Nothing) - in (rpk, rpub, Just memberPrivKey) + let (rootPrivKey_, rootPubKey_) = case publicGroupKeys of + Just PublicGroupKeys {groupRootKey} -> case groupRootKey of + GRKPrivate pk -> (Just pk, Nothing) + GRKPublic k -> (Nothing, Just k) + Nothing -> (Nothing, Nothing) + memberPrivKey_ = snd memberKeys groupId <- liftIO $ do DB.execute db @@ -423,7 +420,7 @@ createNewGroup db cxt user@User {userId} groupProfile incognitoProfile useRelays :. (rootPrivKey_, rootPubKey_, memberPrivKey_, publicMemberCount_, rosterVersion0) ) insertedRowId db - let memberPubKey = C.publicKey . memberPrivKey <$> groupKeys + let memberPubKey = Just $ fst memberKeys membership <- createContactMemberInv_ db user groupId Nothing user (MemberIdRole memberId GROwner) GCUserMember GSMemCreator IBUser customUserProfileId memberPubKey currentTs (vr cxt) let chatSettings = ChatSettings {enableNtfs = MFAll, sendRcpts = Nothing, favorite = False} pure @@ -451,14 +448,14 @@ createNewGroup db cxt user@User {userId} groupProfile incognitoProfile useRelays customData = Nothing, membersRequireAttention = 0, viaGroupLinkUri = Nothing, - groupKeys, + groupKeys = Just GroupKeys {publicGroupKeys, memberPrivKey = snd memberKeys}, groupDomainVerified = Nothing } -- | creates a new group record for the group the current user was invited to, or returns an existing one -createGroupInvitation :: DB.Connection -> StoreCxt -> User -> Contact -> GroupInvitation -> Maybe ProfileId -> ExceptT StoreError IO (GroupInfo, GroupMemberId) -createGroupInvitation _ _ _ Contact {localDisplayName, activeConn = Nothing} _ _ = throwError $ SEContactNotReady localDisplayName -createGroupInvitation db cxt user@User {userId} contact@Contact {contactId, activeConn = Just Connection {peerChatVRange}} GroupInvitation {fromMember, invitedMember, connRequest, groupProfile, business} incognitoProfileId = do +createGroupInvitation :: DB.Connection -> StoreCxt -> User -> Contact -> GroupInvitation -> Maybe ProfileId -> C.KeyPairEd25519 -> ExceptT StoreError IO (GroupInfo, GroupMemberId) +createGroupInvitation _ _ _ Contact {localDisplayName, activeConn = Nothing} _ _ _ = throwError $ SEContactNotReady localDisplayName +createGroupInvitation db cxt user@User {userId} contact@Contact {contactId, activeConn = Just Connection {peerChatVRange}} GroupInvitation {fromMember, fromMemberKey, invitedMember, connRequest, groupProfile, business} incognitoProfileId memberKeys = do liftIO getInvitationGroupId_ >>= \case Nothing -> createGroupInvitation_ Just gId -> do @@ -496,14 +493,14 @@ createGroupInvitation db cxt user@User {userId} contact@Contact {contactId, acti [sql| INSERT INTO groups (group_profile_id, local_display_name, inv_queue_info, user_id, enable_ntfs, - created_at, updated_at, chat_ts, user_member_profile_sent_at, business_chat, business_member_id, customer_member_id) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?) + created_at, updated_at, chat_ts, user_member_profile_sent_at, member_priv_key, business_chat, business_member_id, customer_member_id) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?) |] - ((profileId, localDisplayName, connRequest, userId, BI True, currentTs, currentTs, currentTs, currentTs) :. businessChatInfoRow business) + ((profileId, localDisplayName, connRequest, userId, BI True, currentTs, currentTs, currentTs, currentTs, snd memberKeys) :. businessChatInfoRow business) insertedRowId db let hostVRange = adjustedMemberVRange (vr cxt) peerChatVRange - GroupMember {groupMemberId} <- createContactMemberInv_ db user groupId Nothing contact fromMember GCHostMember GSMemInvited IBUnknown Nothing Nothing currentTs hostVRange - membership <- createContactMemberInv_ db user groupId (Just groupMemberId) user invitedMember GCUserMember GSMemInvited (IBContact contactId) incognitoProfileId Nothing currentTs (vr cxt) + GroupMember {groupMemberId} <- createContactMemberInv_ db user groupId Nothing contact fromMember GCHostMember GSMemInvited IBUnknown Nothing ((\(MemberKey k) -> k) <$> fromMemberKey) currentTs hostVRange + membership <- createContactMemberInv_ db user groupId (Just groupMemberId) user invitedMember GCUserMember GSMemInvited (IBContact contactId) incognitoProfileId (Just $ fst memberKeys) currentTs (vr cxt) let chatSettings = ChatSettings {enableNtfs = MFAll, sendRcpts = Nothing, favorite = False} pure ( GroupInfo @@ -530,7 +527,7 @@ createGroupInvitation db cxt user@User {userId} contact@Contact {contactId, acti customData = Nothing, membersRequireAttention = 0, viaGroupLinkUri = Nothing, - groupKeys = Nothing, + groupKeys = Just GroupKeys {publicGroupKeys = Nothing, memberPrivKey = snd memberKeys}, groupDomainVerified = Nothing }, groupMemberId @@ -786,10 +783,12 @@ updatePreparedGroupUser db cxt user gInfo@GroupInfo {groupId, membership} hostMe safeDeleteLDN db user oldHostLDN updatePreparedUserAndHostMembersInvited :: DB.Connection -> StoreCxt -> User -> GroupInfo -> GroupMember -> GroupLinkInvitation -> ExceptT StoreError IO (GroupInfo, GroupMember) -updatePreparedUserAndHostMembersInvited db cxt user gInfo hostMember GroupLinkInvitation {fromMember, fromMemberName, invitedMember, groupProfile, accepted, business} = do +updatePreparedUserAndHostMembersInvited db cxt user gInfo hostMember GroupLinkInvitation {fromMember, fromMemberKey, fromMemberName, invitedMember, groupProfile, accepted, business} = do let fromMemberProfile = profileFromName fromMemberName initialStatus = maybe GSMemAccepted (acceptanceToStatus $ memberAdmission groupProfile) accepted - updatePreparedUserAndHostMembers' db cxt user gInfo hostMember fromMember fromMemberProfile invitedMember groupProfile business initialStatus + r@(_, hostMember') <- updatePreparedUserAndHostMembers' db cxt user gInfo hostMember fromMember fromMemberProfile invitedMember groupProfile business initialStatus + forM_ fromMemberKey $ \(MemberKey k) -> liftIO $ setMemberPubKey db (groupMemberId' hostMember') k + pure r updatePreparedUserAndHostMembersRejected :: DB.Connection -> StoreCxt -> User -> GroupInfo -> GroupMember -> GroupLinkRejection -> ExceptT StoreError IO (GroupInfo, GroupMember) updatePreparedUserAndHostMembersRejected db cxt user gInfo hostMember GroupLinkRejection {fromMember = fromMember@MemberIdRole {memberId}, invitedMember, groupProfile} = do @@ -851,23 +850,26 @@ updatePreparedUserAndHostMembers' (memberId, memberRole, currentTs, gmId) getGroupMemberById db cxt user gmId -createGroupInvitedViaLink :: DB.Connection -> StoreCxt -> User -> Connection -> GroupLinkInvitation -> ExceptT StoreError IO (GroupInfo, GroupMember) -createGroupInvitedViaLink db cxt user conn GroupLinkInvitation {fromMember, fromMemberName, invitedMember, groupProfile, accepted, business} = do +createGroupInvitedViaLink :: DB.Connection -> StoreCxt -> User -> Connection -> C.KeyPairEd25519 -> GroupLinkInvitation -> ExceptT StoreError IO (GroupInfo, GroupMember) +createGroupInvitedViaLink db cxt user conn memberKeys GroupLinkInvitation {fromMember, fromMemberKey, fromMemberName, invitedMember, groupProfile, accepted, business} = do let fromMemberProfile = profileFromName fromMemberName initialStatus = maybe GSMemAccepted (acceptanceToStatus $ memberAdmission groupProfile) accepted - createGroupViaLink' db cxt user conn fromMember fromMemberProfile invitedMember groupProfile business initialStatus + r@(_, host) <- createGroupViaLink' db cxt user conn memberKeys fromMember fromMemberProfile invitedMember groupProfile business initialStatus + forM_ fromMemberKey $ \(MemberKey k) -> liftIO $ setMemberPubKey db (groupMemberId' host) k + pure r -createGroupRejectedViaLink :: DB.Connection -> StoreCxt -> User -> Connection -> GroupLinkRejection -> ExceptT StoreError IO (GroupInfo, GroupMember) -createGroupRejectedViaLink db cxt user conn GroupLinkRejection {fromMember = fromMember@MemberIdRole {memberId}, invitedMember, groupProfile} = do +createGroupRejectedViaLink :: DB.Connection -> StoreCxt -> User -> Connection -> C.KeyPairEd25519 -> GroupLinkRejection -> ExceptT StoreError IO (GroupInfo, GroupMember) +createGroupRejectedViaLink db cxt user conn memberKeys GroupLinkRejection {fromMember = fromMember@MemberIdRole {memberId}, invitedMember, groupProfile} = do let fromMemberProfile = profileFromName $ nameFromMemberId memberId - createGroupViaLink' db cxt user conn fromMember fromMemberProfile invitedMember groupProfile Nothing GSMemRejected + createGroupViaLink' db cxt user conn memberKeys fromMember fromMemberProfile invitedMember groupProfile Nothing GSMemRejected -createGroupViaLink' :: DB.Connection -> StoreCxt -> User -> Connection -> MemberIdRole -> Profile -> MemberIdRole -> GroupProfile -> Maybe BusinessChatInfo -> GroupMemberStatus -> ExceptT StoreError IO (GroupInfo, GroupMember) +createGroupViaLink' :: DB.Connection -> StoreCxt -> User -> Connection -> C.KeyPairEd25519 -> MemberIdRole -> Profile -> MemberIdRole -> GroupProfile -> Maybe BusinessChatInfo -> GroupMemberStatus -> ExceptT StoreError IO (GroupInfo, GroupMember) createGroupViaLink' db cxt user@User {userId, userContactId} Connection {connId, customUserProfileId} + memberKeys fromMember fromMemberProfile invitedMember @@ -880,7 +882,8 @@ createGroupViaLink' liftIO $ DB.execute db "UPDATE connections SET conn_type = ?, group_member_id = ?, updated_at = ? WHERE connection_id = ?" (ConnMember, hostMemberId, currentTs, connId) -- using IBUnknown since host is created without contact -- TODO [member keys] this is currently not used with public groups. If it needs to be used, member keys need to be added - void $ createContactMemberInv_ db user groupId (Just hostMemberId) user invitedMember GCUserMember membershipStatus IBUnknown customUserProfileId Nothing currentTs (vr cxt) + membership <- createContactMemberInv_ db user groupId (Just hostMemberId) user invitedMember GCUserMember membershipStatus IBUnknown customUserProfileId Nothing currentTs (vr cxt) + liftIO $ setUserMemberKey db groupId (groupMemberId' membership) (snd memberKeys) liftIO $ setViaGroupLinkUri db groupId connId (,) <$> getGroupInfo db cxt user groupId <*> getGroupMemberById db cxt user hostMemberId where diff --git a/src/Simplex/Chat/Terminal/Input.hs b/src/Simplex/Chat/Terminal/Input.hs index e0ee10aff9..e4520fc520 100644 --- a/src/Simplex/Chat/Terminal/Input.hs +++ b/src/Simplex/Chat/Terminal/Input.hs @@ -80,7 +80,7 @@ runInputLoop ct@ChatTerminal {termState, liveMessageState} cc = forever $ do CRChatItemUpdated u (AChatItem _ SMDSnd cInfo _) -> whenCurrUser cc u $ setActiveChat ct cInfo CRChatItemsDeleted u ((ChatItemDeletion (AChatItem _ _ cInfo _) _) : _) _ _ -> whenCurrUser cc u $ setActiveChat ct cInfo CRContactDeleted u c -> whenCurrUser cc u $ unsetActiveContact ct c - CRGroupDeletedUser u g _ -> whenCurrUser cc u $ unsetActiveGroup ct g + CRGroupDeletedUser u g _ _ -> whenCurrUser cc u $ unsetActiveGroup ct g CRSentGroupInvitation u g _ _ -> whenCurrUser cc u $ setActiveGroup ct g CRCmdOk _ -> case cmd of Right APIDeleteUser {} -> setActive ct "" diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index 44c7517ff8..b8802c3517 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -933,6 +933,7 @@ instance ToJSON GroupLinkId where data GroupInvitation = GroupInvitation { fromMember :: MemberIdRole, + fromMemberKey :: Maybe MemberKey, invitedMember :: MemberIdRole, connRequest :: ConnReqInvitation, groupProfile :: GroupProfile, @@ -945,6 +946,7 @@ data GroupInvitation = GroupInvitation data GroupLinkInvitation = GroupLinkInvitation { fromMember :: MemberIdRole, fromMemberName :: ContactName, + fromMemberKey :: Maybe MemberKey, invitedMember :: MemberIdRole, groupProfile :: GroupProfile, accepted :: Maybe GroupAcceptance, diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index bdc708b069..985baad656 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -235,7 +235,7 @@ chatResponseToView hu cfg@ChatConfig {logLevel, showReactions, testView} liveIte "use " <> highlight ("/d #" <> viewGroupName g) <> " to delete the group (also clears the rejection)" ] | otherwise -> ttyUser u $ [ttyGroup' g <> ": you left the group"] <> groupPreserved g - CRGroupDeletedUser u g signed -> ttyUser u [ttyGroup' g <> ": you deleted the group" <> signedStr signed] + CRGroupDeletedUser u g signed local -> ttyUser u [ttyGroup' g <> (if local then ": you deleted your local copy of the group" else ": you deleted the group" <> signedStr signed)] CRForwardPlan u count itemIds fc -> ttyUser u $ viewForwardPlan count itemIds fc CRChatMsgContent u mc -> ttyUser u $ ttyMsgContent mc <> viewMsgTestInfo testView mc CRRcvFileAccepted u ci -> ttyUser u $ savingFile' ci diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs index 1c16f5d976..e795b5a093 100644 --- a/tests/ChatTests/Groups.hs +++ b/tests/ChatTests/Groups.hs @@ -548,7 +548,7 @@ testGroupLargeMessage = alice `send` ("/_group_profile #1 {\"displayName\": \"team\", \"fullName\": \"\", \"image\": \"" <> profileImage <> "\", \"groupPreferences\": {\"directMessages\": {\"enable\": \"on\"}, \"history\": {\"enable\": \"on\"}}}") _trimmedCmd1 <- getTermLine alice alice <## "profile image updated" - bob <## "alice updated group #team:" + bob <## "alice updated group #team: (signed)" bob <## "profile image updated" testNewGroupIncognito :: HasCallStack => TestParams -> IO () @@ -760,11 +760,11 @@ testGroup2 = -- remove member cath ##> "/rm club dan" concurrentlyN_ - [ cath <## "#club: you removed dan from the group", - alice <## "#club: cath removed dan from the group", - bob <## "#club: cath removed dan from the group", + [ cath <## "#club: you removed dan from the group (signed)", + alice <## "#club: cath removed dan from the group (signed)", + bob <## "#club: cath removed dan from the group (signed)", do - dan <## "#club: cath removed you from the group" + dan <## "#club: cath removed you from the group (signed)" dan <## "use /d #club to delete the group" ] alice #> "#club hello" @@ -788,7 +788,7 @@ testGroup2 = dan ##> "#club how is it going?" dan <## "bad chat command: not current member" dan ##> "/d #club" - dan <## "#club: you deleted the group" + dan <## "#club: you deleted your local copy of the group" dan <##> alice -- member leaves bob ##> "/l club" @@ -796,8 +796,8 @@ testGroup2 = [ do bob <## "#club: you left the group" bob <## "use /d #club to delete the group", - alice <## "#club: bob left the group", - cath <## "#club: bob left the group" + alice <## "#club: bob left the group (signed)", + cath <## "#club: bob left the group (signed)" ] alice #> "#club hello" concurrently_ @@ -810,7 +810,7 @@ testGroup2 = bob ##> "#club how is it going?" bob <## "bad chat command: not current member" bob ##> "/d #club" - bob <## "#club: you deleted the group" + bob <## "#club: you deleted your local copy of the group" bob <##> alice testGroupDelete :: HasCallStack => TestParams -> IO () @@ -820,22 +820,22 @@ testGroupDelete = createGroup3' "team" alice (bob, GRMember) (cath, GRMember) alice ##> "/d #team" concurrentlyN_ - [ alice <## "#team: you deleted the group", + [ alice <## "#team: you deleted the group (signed)", do - bob <## "#team: alice deleted the group" + bob <## "#team: alice deleted the group (signed)" bob <## "use /d #team to delete the local copy of the group", do - cath <## "#team: alice deleted the group" + cath <## "#team: alice deleted the group (signed)" cath <## "use /d #team to delete the local copy of the group" ] alice ##> "#team hi" alice <## "no group #team" bob ##> "/d #team" - bob <## "#team: you deleted the group" + bob <## "#team: you deleted your local copy of the group" cath ##> "#team hi" cath <## "bad chat command: not current member" cath ##> "/d #team" - cath <## "#team: you deleted the group" + cath <## "#team: you deleted your local copy of the group" alice <##> bob alice <##> cath -- unused group contacts are deleted @@ -877,7 +877,7 @@ testGroupDeleteWhenInvited = bob <## "use /j team to accept" ] bob ##> "/d #team" - bob <## "#team: you deleted the group" + bob <## "#team: you deleted your local copy of the group" -- alice doesn't receive notification that bob deleted group, -- but she can re-add bob alice ##> "/a team bob" @@ -953,15 +953,15 @@ testGroupReAddInvitedChangeRole = (bob <## "#team: you joined the group") bob ##> "/d #team" concurrentlyN_ - [ bob <## "#team: you deleted the group", + [ bob <## "#team: you deleted the group (signed)", do - alice <## "#team: bob deleted the group" + alice <## "#team: bob deleted the group (signed)" alice <## "use /d #team to delete the local copy of the group" ] bob ##> "#team hi" bob <## "no group #team" alice ##> "/d #team" - alice <## "#team: you deleted the group" + alice <## "#team: you deleted your local copy of the group" testGroupDeleteInvitedContact :: HasCallStack => TestParams -> IO () testGroupDeleteInvitedContact = @@ -1066,15 +1066,15 @@ testDeleteGroupMemberProfileKept = -- delete group 1 alice ##> "/d #team" concurrentlyN_ - [ alice <## "#team: you deleted the group", + [ alice <## "#team: you deleted the group (signed)", do - bob <## "#team: alice deleted the group" + bob <## "#team: alice deleted the group (signed)" bob <## "use /d #team to delete the local copy of the group" ] alice ##> "#team hi" alice <## "no group #team" bob ##> "/d #team" - bob <## "#team: you deleted the group" + bob <## "#team: you deleted your local copy of the group" -- group 2 still works alice #> "#club checking connection" bob <# "#club alice> checking connection" @@ -1092,11 +1092,11 @@ testGroupRemoveAdd = -- remove member alice ##> "/rm team bob" concurrentlyN_ - [ alice <## "#team: you removed bob from the group", + [ alice <## "#team: you removed bob from the group (signed)", do - bob <## "#team: alice removed you from the group" + bob <## "#team: alice removed you from the group (signed)" bob <## "use /d #team to delete the group", - cath <## "#team: alice removed bob from the group" + cath <## "#team: alice removed bob from the group (signed)" ] threadDelay 100000 @@ -1154,7 +1154,7 @@ testGroupList = ] -- after deleting invitation bob sees only one group bob ##> "/d #tennis" - bob <## "#tennis: you deleted the group" + bob <## "#tennis: you deleted your local copy of the group" bob ##> "/gs" bob <## "#team (2 members)" @@ -1589,10 +1589,10 @@ testUpdateGroupProfile = alice <## "changed to #my_team" concurrentlyN_ [ do - bob <## "alice updated group #team:" + bob <## "alice updated group #team: (signed)" bob <## "changed to #my_team", do - cath <## "alice updated group #team:" + cath <## "alice updated group #team: (signed)" cath <## "changed to #my_team" ] bob #> "#my_team hi" @@ -1603,20 +1603,20 @@ testUpdateGroupProfile = alice <## "description changed to: My team" concurrentlyN_ [ do - bob <## "alice updated group #my_team:" + bob <## "alice updated group #my_team: (signed)" bob <## "description changed to: My team", do - cath <## "alice updated group #my_team:" + cath <## "alice updated group #my_team: (signed)" cath <## "description changed to: My team" ] alice ##> "/gp my_team my_team My team updated" alice <## "description changed to: My team updated" concurrentlyN_ [ do - bob <## "alice updated group #my_team:" + bob <## "alice updated group #my_team: (signed)" bob <## "description changed to: My team updated", do - cath <## "alice updated group #my_team:" + cath <## "alice updated group #my_team: (signed)" cath <## "description changed to: My team updated" ] @@ -1642,8 +1642,8 @@ testUpdateMemberRole = bob <## "#team: you have insufficient permissions for this action, the required role is admin" alice ##> "/mr team bob admin" concurrently_ - (alice <## "#team: you changed the role of bob to admin") - (bob <## "#team: alice changed your role from member to admin") + (alice <## "#team: you changed the role of bob to admin (signed)") + (bob <## "#team: alice changed your role from member to admin (signed)") bob ##> "/a team cath owner" bob <## "#team: you have insufficient permissions for this action, the required role is owner" addMember "team" bob cath GRMember @@ -1678,7 +1678,7 @@ testOwnerRoleChange = |] cath ##> "/mr #team bob owner" - cath <## "#team: you changed the role of bob to owner" + cath <## "#team: you changed the role of bob to owner (signed)" concurrentlyN_ [ alice <## "error: x.grp.mem.role with insufficient member permissions", bob <## "error: x.grp.mem.role with insufficient member permissions" @@ -1712,7 +1712,7 @@ testGroupDescription = testChat4 aliceProfile bobProfile cathProfile danProfile alice ##> "/set welcome team Welcome to the team!" alice <## "welcome message changed to:" alice <## "Welcome to the team!" - bob <## "alice updated group #team:" + bob <## "alice updated group #team: (signed)" bob <## "welcome message changed to:" bob <## "Welcome to the team!" alice ##> "/group_profile team" @@ -1775,9 +1775,9 @@ testGroupModerate = -- disableFullDeletion3 "team" alice bob cath alice ##> "/mr team cath member" concurrentlyN_ - [ alice <## "#team: you changed the role of cath to member", - bob <## "#team: alice changed the role of cath from admin to member", - cath <## "#team: alice changed your role from admin to member" + [ alice <## "#team: you changed the role of cath to member (signed)", + bob <## "#team: alice changed the role of cath from admin to member (signed)", + cath <## "#team: alice changed your role from admin to member (signed)" ] alice #> "#team hello" concurrently_ @@ -1858,20 +1858,20 @@ testGroupModerateFullDelete = -- disableFullDeletion3 "team" alice bob cath alice ##> "/mr team cath member" concurrentlyN_ - [ alice <## "#team: you changed the role of cath to member", - bob <## "#team: alice changed the role of cath from admin to member", - cath <## "#team: alice changed your role from admin to member" + [ alice <## "#team: you changed the role of cath to member (signed)", + bob <## "#team: alice changed the role of cath from admin to member (signed)", + cath <## "#team: alice changed your role from admin to member (signed)" ] alice ##> "/set delete #team on" alice <## "updated group preferences:" alice <## "Full deletion: on" concurrentlyN_ [ do - bob <## "alice updated group #team:" + bob <## "alice updated group #team: (signed)" bob <## "updated group preferences:" bob <## "Full deletion: on", do - cath <## "alice updated group #team:" + cath <## "alice updated group #team: (signed)" cath <## "updated group preferences:" cath <## "Full deletion: on" ] @@ -1965,13 +1965,13 @@ testGroupDelayedModerationFullDelete ps = do alice ##> "/set delete #team on" alice <## "updated group preferences:" alice <## "Full deletion: on" - cath <## "alice updated group #team:" + cath <## "alice updated group #team: (signed)" cath <## "updated group preferences:" cath <## "Full deletion: on" withTestChatCfg ps cfg "bob" $ \bob -> do bob <## "subscribed 2 connections on server localhost" bob <## "#team: alice added cath (Catherine) to the group (connecting...)" - bob <## "alice updated group #team:" + bob <## "alice updated group #team: (signed)" bob <## "updated group preferences:" bob <## "Full deletion: on" withTestChatCfg ps cfg "cath" $ \cath -> do @@ -2002,11 +2002,11 @@ testDeleteMemberWithMessages = threadDelay 750000 concurrentlyN_ [ do - bob <## "alice updated group #team:" + bob <## "alice updated group #team: (signed)" bob <## "updated group preferences:" bob <## "Full deletion: on", do - cath <## "alice updated group #team:" + cath <## "alice updated group #team: (signed)" cath <## "updated group preferences:" cath <## "Full deletion: on" ] @@ -2053,19 +2053,19 @@ testDeleteMemberWithMessages = threadDelay 1000000 alice ##> "/rm #team bob messages=on" - alice <## "#team: you removed bob from the group with all messages" - bob <## "#team: alice removed you from the group with all messages" + alice <## "#team: you removed bob from the group with all messages (signed)" + bob <## "#team: alice removed you from the group with all messages (signed)" bob <## "use /d #team to delete the group" - cath <## "#team: alice removed bob from the group with all messages" + cath <## "#team: alice removed bob from the group with all messages (signed)" doesFileExist "./tests/tmp/alice_app_files/test.jpg" `shouldReturn` False doesFileExist "./tests/tmp/bob_app_files/test.jpg" `shouldReturn` False doesFileExist "./tests/tmp/cath_app_files/test.jpg" `shouldReturn` False -- Under fullDelete, bob's items are physically deleted on all sides; only the system event remains. - alice #$> ("/_get chat #1 count=1", chat, [(1, "removed bob")]) - bob #$> ("/_get chat #1 count=1", chat, [(0, "removed you")]) - cath #$> ("/_get chat #1 count=1", chat, [(0, "removed bob")]) + alice #$> ("/_get chat #1 count=1", chat, [(1, "removed bob (signed)")]) + bob #$> ("/_get chat #1 count=1", chat, [(0, "removed you (signed)")]) + cath #$> ("/_get chat #1 count=1", chat, [(0, "removed bob (signed)")]) testDeleteMemberMarkMessagesDeleted :: HasCallStack => TestParams -> IO () testDeleteMemberMarkMessagesDeleted = @@ -2082,13 +2082,13 @@ testDeleteMemberMarkMessagesDeleted = cath #$> ("/_get chat #1 count=1", chat, [(0, "hello")]) threadDelay 1000000 alice ##> "/rm #team bob messages=on" - alice <## "#team: you removed bob from the group with all messages" - bob <## "#team: alice removed you from the group with all messages" + alice <## "#team: you removed bob from the group with all messages (signed)" + bob <## "#team: alice removed you from the group with all messages (signed)" bob <## "use /d #team to delete the group" - cath <## "#team: alice removed bob from the group with all messages" - alice #$> ("/_get chat #1 count=2", chat, [(0, "hello [marked deleted by you]"), (1, "removed bob")]) - bob #$> ("/_get chat #1 count=2", chat, [(1, "hello [marked deleted by alice]"), (0, "removed you")]) - cath #$> ("/_get chat #1 count=2", chat, [(0, "hello [marked deleted by alice]"), (0, "removed bob")]) + cath <## "#team: alice removed bob from the group with all messages (signed)" + alice #$> ("/_get chat #1 count=2", chat, [(0, "hello [marked deleted by you]"), (1, "removed bob (signed)")]) + bob #$> ("/_get chat #1 count=2", chat, [(1, "hello [marked deleted by alice]"), (0, "removed you (signed)")]) + cath #$> ("/_get chat #1 count=2", chat, [(0, "hello [marked deleted by alice]"), (0, "removed bob (signed)")]) testDeleteMemberMessagesLeftRemoved :: HasCallStack => TestParams -> IO () testDeleteMemberMessagesLeftRemoved = @@ -2115,33 +2115,33 @@ testDeleteMemberMessagesLeftRemoved = [ do cath <## "#team: you left the group" cath <## "use /d #team to delete the group", - alice <## "#team: cath left the group", - bob <## "#team: cath left the group", - dan <## "#team: cath left the group" + alice <## "#team: cath left the group (signed)", + bob <## "#team: cath left the group (signed)", + dan <## "#team: cath left the group (signed)" ] threadDelay 1000000 alice ##> "/rm team dan" concurrentlyN_ - [ alice <## "#team: you removed dan from the group", + [ alice <## "#team: you removed dan from the group (signed)", do - dan <## "#team: alice removed you from the group" + dan <## "#team: alice removed you from the group (signed)" dan <## "use /d #team to delete the group", - bob <## "#team: alice removed dan from the group" + bob <## "#team: alice removed dan from the group (signed)" ] alice ##> "/rm #team cath messages=on" - alice <## "#team: you removed cath from the group with all messages" - bob <## "#team: alice removed cath from the group with all messages" + alice <## "#team: you removed cath from the group with all messages (signed)" + bob <## "#team: alice removed cath from the group with all messages (signed)" alice ##> "/rm #team dan messages=on" - alice <## "#team: you removed dan from the group with all messages" - bob <## "#team: alice removed dan from the group with all messages" + alice <## "#team: you removed dan from the group with all messages (signed)" + bob <## "#team: alice removed dan from the group with all messages (signed)" - alice #$> ("/_get chat #1 count=4", chat, [(0, "1 [marked deleted by you]"), (0, "2 [marked deleted by you]"), (0, "left [marked deleted by you]"), (1, "removed dan")]) - bob #$> ("/_get chat #1 count=4", chat, [(0, "1 [marked deleted by alice]"), (0, "2 [marked deleted by alice]"), (0, "left [marked deleted by alice]"), (0, "removed dan")]) - cath #$> ("/_get chat #1 count=3", chat, [(1, "1"), (0, "2"), (1, "left")]) - dan #$> ("/_get chat #1 count=4", chat, [(0, "1"), (1, "2"), (0, "left"), (0, "removed you")]) + alice #$> ("/_get chat #1 count=4", chat, [(0, "1 [marked deleted by you]"), (0, "2 [marked deleted by you]"), (0, "left (signed) [marked deleted by you]"), (1, "removed dan (signed)")]) + bob #$> ("/_get chat #1 count=4", chat, [(0, "1 [marked deleted by alice]"), (0, "2 [marked deleted by alice]"), (0, "left (signed) [marked deleted by alice]"), (0, "removed dan (signed)")]) + cath #$> ("/_get chat #1 count=3", chat, [(1, "1"), (0, "2"), (1, "left (signed)")]) + dan #$> ("/_get chat #1 count=4", chat, [(0, "1"), (1, "2"), (0, "left (signed)"), (0, "removed you (signed)")]) testSendMulti :: HasCallStack => TestParams -> IO () testSendMulti = @@ -2166,10 +2166,10 @@ testSendMultiTimed = alice ##> "/set disappear #team on 1" alice <## "updated group preferences:" alice <## "Disappearing messages: on (1 sec)" - bob <## "alice updated group #team:" + bob <## "alice updated group #team: (signed)" bob <## "updated group preferences:" bob <## "Disappearing messages: on (1 sec)" - cath <## "alice updated group #team:" + cath <## "alice updated group #team: (signed)" cath <## "updated group preferences:" cath <## "Disappearing messages: on (1 sec)" @@ -2472,10 +2472,10 @@ testGroupLinkDeleteGroupRejoin = [ do bob <## "#team: you left the group" bob <## "use /d #team to delete the group", - alice <## "#team: bob left the group" + alice <## "#team: bob left the group (signed)" ] bob ##> "/d #team" - bob <## "#team: you deleted the group" + bob <## "#team: you deleted your local copy of the group" -- re-join via same link bob ##> ("/c " <> gLink) bob <## "connection request sent!" @@ -2666,7 +2666,7 @@ testPlanGroupLinkLeaveRejoin = [ do bob <## "#team: you left the group" bob <## "use /d #team to delete the group", - alice <## "#team: bob left the group" + alice <## "#team: bob left the group (signed)" ] threadDelay 100000 @@ -2793,8 +2793,8 @@ testGroupLink = [ do alice <## "#team: you left the group" alice <## "use /d #team to delete the group", - bob <## "#team: alice left the group", - cath <## "#team: alice left the group" + bob <## "#team: alice left the group (signed)", + cath <## "#team: alice left the group (signed)" ] alice ##> "/show link #team" alice <## "no group link, to create: /create link #team" @@ -2803,7 +2803,7 @@ testGroupLink = alice ##> "/contacts" alice <## "cath (Catherine)" alice ##> "/d #team" - alice <## "#team: you deleted the group" + alice <## "#team: you deleted your local copy of the group" alice ##> "/contacts" alice <## "cath (Catherine)" @@ -3008,8 +3008,8 @@ testGroupLinkMemberRole = bob <## "#team: you don't have permission to send messages" alice ##> "/mr #team bob member" - alice <## "#team: you changed the role of bob to member" - bob <## "#team: alice changed your role from observer to member" + alice <## "#team: you changed the role of bob to member (signed)" + bob <## "#team: alice changed your role from observer to member (signed)" bob #> "#team hey now" alice <# "#team bob> hey now" @@ -3038,18 +3038,18 @@ testGroupLinkMemberRole = cath <## "#team: you don't have permission to send messages" alice ##> "/mr #team cath admin" - alice <## "#team: you changed the role of cath to admin" - cath <## "#team: alice changed your role from observer to admin" - bob <## "#team: alice changed the role of cath from observer to admin" + alice <## "#team: you changed the role of cath to admin (signed)" + cath <## "#team: alice changed your role from observer to admin (signed)" + bob <## "#team: alice changed the role of cath from observer to admin (signed)" cath #> "#team hey" alice <# "#team cath> hey" bob <# "#team cath> hey" cath ##> "/mr #team bob admin" - cath <## "#team: you changed the role of bob to admin" - bob <## "#team: cath changed your role from member to admin" - alice <## "#team: cath changed the role of bob from member to admin" + cath <## "#team: you changed the role of bob to admin (signed)" + bob <## "#team: cath changed your role from member to admin (signed)" + alice <## "#team: cath changed the role of bob from member to admin (signed)" testGroupLinkDemotedAdmin :: HasCallStack => TestParams -> IO () testGroupLinkDemotedAdmin = @@ -3062,8 +3062,8 @@ testGroupLinkDemotedAdmin = alice ##> "/mr #team bob member" concurrentlyN_ - [ alice <## "#team: you changed the role of bob to member", - bob <## "#team: alice changed your role from admin to member" + [ alice <## "#team: you changed the role of bob to member (signed)", + bob <## "#team: alice changed your role from admin to member (signed)" ] -- demotion does not remove bob's group link (it is preserved, usable again on re-promotion) @@ -3315,13 +3315,13 @@ testGLinkReviewMember = alice <## "changed member admission rules" concurrentlyN_ [ do - bob <## "alice updated group #team:" + bob <## "alice updated group #team: (signed)" bob <## "changed member admission rules", do - cath <## "alice updated group #team:" + cath <## "alice updated group #team: (signed)" cath <## "changed member admission rules", do - dan <## "alice updated group #team:" + dan <## "alice updated group #team: (signed)" dan <## "changed member admission rules" ] @@ -3442,13 +3442,13 @@ testGLinkApproveThenReviewMember = alice <## "changed member admission rules" concurrentlyN_ [ do - bob <## "alice updated group #team:" + bob <## "alice updated group #team: (signed)" bob <## "changed member admission rules", do - cath <## "alice updated group #team:" + cath <## "alice updated group #team: (signed)" cath <## "changed member admission rules", do - dan <## "alice updated group #team:" + dan <## "alice updated group #team: (signed)" dan <## "changed member admission rules" ] @@ -3613,8 +3613,8 @@ testGLinkDeletePendingApprovalMember = ] alice ##> "/rm team cath" - alice <## "#team: you removed cath from the group" - cath <## "#team: alice removed you from the group" + alice <## "#team: you removed cath from the group (signed)" + cath <## "#team: alice removed you from the group (signed)" cath <## "use /d #team to delete the group" where cfg = testCfg {chatHooks = defaultChatHooks {acceptMember = Just (\_ _ _ -> pure $ Right (GAPendingApproval, GRObserver))}} @@ -3649,23 +3649,23 @@ testGLinkReviewIntroduce = alice ##> "/mr team dan admin" concurrentlyN_ - [ alice <## "#team: you changed the role of dan to admin", - bob <## "#team: alice changed the role of dan from member to admin", - cath <## "#team: alice changed the role of dan from member to admin", - dan <## "#team: alice changed your role from member to admin" + [ alice <## "#team: you changed the role of dan to admin (signed)", + bob <## "#team: alice changed the role of dan from member to admin (signed)", + cath <## "#team: alice changed the role of dan from member to admin (signed)", + dan <## "#team: alice changed your role from member to admin (signed)" ] alice ##> "/set admission review #team all" alice <## "changed member admission rules" concurrentlyN_ [ do - bob <## "alice updated group #team:" + bob <## "alice updated group #team: (signed)" bob <## "changed member admission rules", do - cath <## "alice updated group #team:" + cath <## "alice updated group #team: (signed)" cath <## "changed member admission rules", do - dan <## "alice updated group #team:" + dan <## "alice updated group #team: (signed)" dan <## "changed member admission rules" ] @@ -4916,12 +4916,12 @@ testMemberContactAccept = -- if group is deleted, bob and cath keep contact with each other alice ##> "/d #team" concurrentlyN_ - [ alice <## "#team: you deleted the group", + [ alice <## "#team: you deleted the group (signed)", do - bob <## "#team: alice deleted the group" + bob <## "#team: alice deleted the group (signed)" bob <## "use /d #team to delete the local copy of the group", do - cath <## "#team: alice deleted the group" + cath <## "#team: alice deleted the group (signed)" cath <## "use /d #team to delete the local copy of the group" ] @@ -5016,12 +5016,12 @@ testMemberContactAcceptIncognito = -- if group is deleted, bob and cath keep contact with each other alice ##> "/d #team" concurrentlyN_ - [ alice <## "#team: you deleted the group", + [ alice <## "#team: you deleted the group (signed)", do - bob <## "#team: alice deleted the group" + bob <## "#team: alice deleted the group (signed)" bob <## "use /d #team to delete the local copy of the group", do - cath <## "#team: alice deleted the group" + cath <## "#team: alice deleted the group (signed)" cath <## "use /d #team to delete the local copy of the group" ] @@ -5127,16 +5127,16 @@ testGroupMsgForwardReport = alice ##> "/mr team bob moderator" concurrentlyN_ - [ alice <## "#team: you changed the role of bob to moderator", - bob <## "#team: alice changed your role from admin to moderator", - cath <## "#team: alice changed the role of bob from admin to moderator" + [ alice <## "#team: you changed the role of bob to moderator (signed)", + bob <## "#team: alice changed your role from admin to moderator (signed)", + cath <## "#team: alice changed the role of bob from admin to moderator (signed)" ] alice ##> "/mr team cath member" concurrentlyN_ - [ alice <## "#team: you changed the role of cath to member", - bob <## "#team: alice changed the role of cath from admin to member", - cath <## "#team: alice changed your role from admin to member" + [ alice <## "#team: you changed the role of cath to member (signed)", + bob <## "#team: alice changed the role of cath from admin to member (signed)", + cath <## "#team: alice changed your role from admin to member (signed)" ] cath ##> "/report #team content hi there" cath <# "#team (support) > bob hi there" @@ -5152,9 +5152,9 @@ testGroupMsgForwardReport = alice ##> "/mr team bob member" concurrentlyN_ - [ alice <## "#team: you changed the role of bob to member", - bob <## "#team: alice changed your role from moderator to member", - cath <## "#team: alice changed the role of bob from moderator to member" + [ alice <## "#team: you changed the role of bob to member (signed)", + bob <## "#team: alice changed your role from moderator to member (signed)", + cath <## "#team: alice changed the role of bob from moderator to member (signed)" ] cath ##> "/report #team content hi there" @@ -5371,9 +5371,9 @@ testGroupMsgForwardChangeRole = setupGroupForwarding alice bob cath cath ##> "/mr #team bob member" - cath <## "#team: you changed the role of bob to member" - alice <## "#team: cath changed the role of bob from admin to member" - bob <## "#team: cath changed your role from admin to member" -- TODO show as forwarded + cath <## "#team: you changed the role of bob to member (signed)" + alice <## "#team: cath changed the role of bob from admin to member (signed)" + bob <## "#team: cath changed your role from admin to member (signed)" -- TODO show as forwarded testGroupMsgForwardNewMember :: HasCallStack => TestParams -> IO () testGroupMsgForwardNewMember = @@ -5427,8 +5427,8 @@ testGroupMsgForwardLeave = bob ##> "/leave #team" bob <## "#team: you left the group" bob <## "use /d #team to delete the group" - alice <## "#team: bob left the group" - cath <## "#team: bob left the group" + alice <## "#team: bob left the group (signed)" + cath <## "#team: bob left the group (signed)" testGroupMsgForwardMemberRemoval :: HasCallStack => TestParams -> IO () testGroupMsgForwardMemberRemoval = @@ -5440,10 +5440,10 @@ testGroupMsgForwardMemberRemoval = -- remove member bob ##> "/rm team cath" concurrentlyN_ - [ bob <## "#team: you removed cath from the group", - alice <## "#team: bob removed cath from the group", + [ bob <## "#team: you removed cath from the group (signed)", + alice <## "#team: bob removed cath from the group (signed)", do - cath <## "#team: bob removed you from the group" + cath <## "#team: bob removed you from the group (signed)" cath <## "use /d #team to delete the group" ] bob #> "#team hi" @@ -5476,11 +5476,11 @@ testGroupMsgForwardAdminRemoval = -- if alice is removed, she forwards message of her own removal bob ##> "/rm team alice" concurrentlyN_ - [ bob <## "#team: you removed alice from the group", + [ bob <## "#team: you removed alice from the group (signed)", do - alice <## "#team: bob removed you from the group" + alice <## "#team: bob removed you from the group (signed)" alice <## "use /d #team to delete the group", - cath <## "#team: bob removed alice from the group" + cath <## "#team: bob removed alice from the group (signed)" ] -- there is no forwarding admin anymore between bob and cath, so messages don't get delivered @@ -5515,12 +5515,12 @@ testGroupMsgForwardGroupDeletion = -- if bob deletes the group, alice forwards it to cath bob ##> "/d #team" concurrentlyN_ - [ bob <## "#team: you deleted the group", + [ bob <## "#team: you deleted the group (signed)", do - alice <## "#team: bob deleted the group" + alice <## "#team: bob deleted the group (signed)" alice <## "use /d #team to delete the local copy of the group", do - cath <## "#team: bob deleted the group" + cath <## "#team: bob deleted the group (signed)" cath <## "use /d #team to delete the local copy of the group" ] @@ -5665,11 +5665,11 @@ testGroupHistoryPreferenceOff = alice <## "Recent history: off" concurrentlyN_ [ do - bob <## "alice updated group #team:" + bob <## "alice updated group #team: (signed)" bob <## "updated group preferences:" bob <## "Recent history: off", do - cath <## "alice updated group #team:" + cath <## "alice updated group #team: (signed)" cath <## "updated group preferences:" cath <## "Recent history: off" ] @@ -6201,7 +6201,7 @@ testGroupHistoryDisappearingMessage = alice ##> "/set disappear #team on 4" alice <## "updated group preferences:" alice <## "Disappearing messages: on (4 sec)" - bob <## "alice updated group #team:" + bob <## "alice updated group #team: (signed)" bob <## "updated group preferences:" bob <## "Disappearing messages: on (4 sec)" @@ -6218,7 +6218,7 @@ testGroupHistoryDisappearingMessage = alice ##> "/set disappear #team off" alice <## "updated group preferences:" alice <## "Disappearing messages: off" - bob <## "alice updated group #team:" + bob <## "alice updated group #team: (signed)" bob <## "updated group preferences:" bob <## "Disappearing messages: off" @@ -6278,7 +6278,7 @@ testGroupHistoryWelcomeMessage = alice <## "welcome message changed to:" alice <## "welcome to team" - bob <## "alice updated group #team:" + bob <## "alice updated group #team: (signed)" bob <## "welcome message changed to:" bob <## "welcome to team" @@ -6351,8 +6351,8 @@ testGroupHistoryUnknownMember = [ do bob <## "#team: you left the group" bob <## "use /d #team to delete the group", - alice <## "#team: bob left the group", - cath <## "#team: bob left the group" + alice <## "#team: bob left the group (signed)", + cath <## "#team: bob left the group (signed)" ] connectUsers alice dan @@ -6465,7 +6465,7 @@ testMembershipProfileUpdateNextGroupMessage = bob ##> "/_get chat #1 count=100" rb <- chat <$> getTermLine bob - rb `shouldContain` [(0, "updated profile")] + rb `shouldContain` [(0, "updated profile (signed)")] -- update profile in group 2 @@ -6489,7 +6489,7 @@ testMembershipProfileUpdateNextGroupMessage = cath ##> "/_get chat #1 count=100" rc <- chat <$> getTermLine cath - rc `shouldContain` [(0, "updated profile")] + rc `shouldContain` [(0, "updated profile (signed)")] testMembershipProfileUpdateSameMember :: HasCallStack => TestParams -> IO () testMembershipProfileUpdateSameMember = @@ -6543,7 +6543,7 @@ testMembershipProfileUpdateSameMember = bob ##> "/_get chat #1 count=100" rTeam <- chat <$> getTermLine bob - rTeam `shouldContain` [(0, "updated profile")] + rTeam `shouldContain` [(0, "updated profile (signed)")] bob ##> "/_get chat #2 count=100" rClub <- chat <$> getTermLine bob @@ -6689,7 +6689,7 @@ testMembershipProfileUpdateContactDeleted = bob ##> "/_get chat #1 count=100" rGrp <- chat <$> getTermLine bob - rGrp `shouldContain` [(0, "updated profile")] + rGrp `shouldContain` [(0, "updated profile (signed)")] checkAliceNoProfileLink bob name = do bob ##> ("/info #team " <> name) bob <## "group ID: 1" @@ -6752,7 +6752,7 @@ testMembershipProfileUpdateContactDisabled = bob ##> "/_get chat #1 count=100" rGrp <- chat <$> getTermLine bob - rGrp `shouldContain` [(0, "updated profile")] + rGrp `shouldContain` [(0, "updated profile (signed)")] testMembershipProfileUpdateNoChangeIgnored :: HasCallStack => TestParams -> IO () testMembershipProfileUpdateNoChangeIgnored = @@ -6850,8 +6850,8 @@ testBlockForAllMarkedBlocked = threadDelay 1000000 alice ##> "/block for all #team bob" - alice <## "#team: you blocked bob" - cath <## "#team: alice blocked bob" + alice <## "#team: you blocked bob (signed)" + cath <## "#team: alice blocked bob (signed)" bob "/ms team" @@ -6890,8 +6890,8 @@ testBlockForAllMarkedBlocked = threadDelay 1000000 alice ##> "/unblock for all #team bob" - alice <## "#team: you unblocked bob" - cath <## "#team: alice unblocked bob" + alice <## "#team: you unblocked bob (signed)" + cath <## "#team: alice unblocked bob (signed)" bob ( "/_get chat #1 count=6", chat, [ (0, "1"), - (1, "blocked bob"), + (1, "blocked bob (signed)"), (0, "2 [blocked by admin]"), (0, "3 [blocked by admin]"), - (1, "unblocked bob"), + (1, "unblocked bob (signed)"), (0, "4") ] ) @@ -6914,10 +6914,10 @@ testBlockForAllMarkedBlocked = #$> ( "/_get chat #1 count=6", chat, [ (0, "1"), - (0, "blocked bob"), + (0, "blocked bob (signed)"), (0, "2 [blocked by admin]"), (0, "3 [blocked by admin]"), - (0, "unblocked bob"), + (0, "unblocked bob (signed)"), (0, "4") ] ) @@ -6939,8 +6939,8 @@ testBlockForAllMentionsIgnored = threadDelay 1000000 alice ##> "/block for all #team bob" - alice <## "#team: you blocked bob" - cath <## "#team: alice blocked bob" + alice <## "#team: you blocked bob (signed)" + cath <## "#team: alice blocked bob (signed)" bob "/block for all #team bob" - alice <## "#team: you blocked bob" - cath <## "#team: alice blocked bob" + alice <## "#team: you blocked bob (signed)" + cath <## "#team: alice blocked bob (signed)" bob "/unblock for all #team bob" - alice <## "#team: you unblocked bob" - cath <## "#team: alice unblocked bob" + alice <## "#team: you unblocked bob (signed)" + cath <## "#team: alice unblocked bob (signed)" bob ( "/_get chat #1 count=6", chat, [ (0, "1"), - (1, "blocked bob"), + (1, "blocked bob (signed)"), (0, "blocked [blocked by admin]"), (0, "blocked [blocked by admin]"), - (1, "unblocked bob"), + (1, "unblocked bob (signed)"), (0, "4") ] ) @@ -7062,10 +7062,10 @@ testBlockForAllFullDelete = #$> ( "/_get chat #1 count=6", chat, [ (0, "1"), - (0, "blocked bob"), + (0, "blocked bob (signed)"), (0, "blocked [blocked by admin]"), (0, "blocked [blocked by admin]"), - (0, "unblocked bob"), + (0, "unblocked bob (signed)"), (0, "4") ] ) @@ -7082,8 +7082,8 @@ testBlockForAllAnotherAdminUnblocks = [alice, cath] *<# "#team bob> 1" alice ##> "/block for all #team bob" - alice <## "#team: you blocked bob" - cath <## "#team: alice blocked bob" + alice <## "#team: you blocked bob (signed)" + cath <## "#team: alice blocked bob (signed)" bob "#team 2" @@ -7091,8 +7091,8 @@ testBlockForAllAnotherAdminUnblocks = cath <# "#team bob> 2 [blocked by admin] " cath ##> "/unblock for all #team bob" - cath <## "#team: you unblocked bob" - alice <## "#team: cath unblocked bob" + cath <## "#team: you unblocked bob (signed)" + alice <## "#team: cath unblocked bob (signed)" bob "#team 3" @@ -7111,8 +7111,8 @@ testBlockForAllBeforeJoining = [alice, cath] *<# "#team bob> 1" alice ##> "/block for all #team bob" - alice <## "#team: you blocked bob" - cath <## "#team: alice blocked bob" + alice <## "#team: you blocked bob (signed)" + cath <## "#team: alice blocked bob (signed)" bob "#team 2" @@ -7146,9 +7146,9 @@ testBlockForAllBeforeJoining = threadDelay 1000000 alice ##> "/unblock for all #team bob" - alice <## "#team: you unblocked bob" - cath <## "#team: alice unblocked bob" - dan <## "#team: alice unblocked bob" + alice <## "#team: you unblocked bob (signed)" + cath <## "#team: alice unblocked bob (signed)" + dan <## "#team: alice unblocked bob (signed)" bob "/_get chat #1 count=100" r <- chat <$> getTermLine dan - r `shouldContain` [(0, "3 [blocked by admin]"), (0, "4 [blocked by admin]"), (0, "unblocked bob"), (0, "5")] + r `shouldContain` [(0, "3 [blocked by admin]"), (0, "4 [blocked by admin]"), (0, "unblocked bob (signed)"), (0, "5")] r `shouldNotContain` [(0, "1")] r `shouldNotContain` [(0, "1 [blocked by admin]")] r `shouldNotContain` [(0, "2")] @@ -7180,30 +7180,30 @@ testBlockForAllRepeat = [alice, cath] *<# "#team bob> 1" alice ##> "/block for all #team bob" - alice <## "#team: you blocked bob" - cath <## "#team: alice blocked bob" + alice <## "#team: you blocked bob (signed)" + cath <## "#team: alice blocked bob (signed)" bob "/block for all #team bob" - alice <## "#team: you blocked bob" + alice <## "#team: you blocked bob (signed)" cath ##> "/block for all #team bob" - cath <## "#team: you blocked bob" + cath <## "#team: you blocked bob (signed)" bob #> "#team 2" alice <# "#team bob> 2 [blocked by admin] " cath <# "#team bob> 2 [blocked by admin] " cath ##> "/unblock for all #team bob" - cath <## "#team: you unblocked bob" - alice <## "#team: cath unblocked bob" + cath <## "#team: you unblocked bob (signed)" + alice <## "#team: cath unblocked bob (signed)" bob "/unblock for all #team bob" - alice <## "#team: you unblocked bob" + alice <## "#team: you unblocked bob (signed)" cath ##> "/unblock for all #team bob" - cath <## "#team: you unblocked bob" + cath <## "#team: you unblocked bob (signed)" bob #> "#team 3" [alice, cath] *<# "#team bob> 3" @@ -7238,17 +7238,17 @@ testBlockForAllMultipleMembers = -- lower roles to for batch block to be allowed (can't batch block if admins are selected) alice ##> "/mr team bob member" concurrentlyN_ - [ alice <## "#team: you changed the role of bob to member", - bob <## "#team: alice changed your role from admin to member", - cath <## "#team: alice changed the role of bob from admin to member", - dan <## "#team: alice changed the role of bob from admin to member" + [ alice <## "#team: you changed the role of bob to member (signed)", + bob <## "#team: alice changed your role from admin to member (signed)", + cath <## "#team: alice changed the role of bob from admin to member (signed)", + dan <## "#team: alice changed the role of bob from admin to member (signed)" ] alice ##> "/mr team cath member" concurrentlyN_ - [ alice <## "#team: you changed the role of cath to member", - bob <## "#team: alice changed the role of cath from admin to member", - cath <## "#team: alice changed your role from admin to member", - dan <## "#team: alice changed the role of cath from admin to member" + [ alice <## "#team: you changed the role of cath to member (signed)", + bob <## "#team: alice changed the role of cath from admin to member (signed)", + cath <## "#team: alice changed your role from admin to member (signed)", + dan <## "#team: alice changed the role of cath from admin to member (signed)" ] bob #> "#team 1" @@ -7258,9 +7258,9 @@ testBlockForAllMultipleMembers = [alice, bob, dan] *<# "#team cath> 2" alice ##> "/_block #1 2,3 blocked=on" - alice <## "#team: you blocked 2 members" - dan <## "#team: alice blocked bob" - dan <## "#team: alice blocked cath" + alice <## "#team: you blocked 2 members (signed)" + dan <## "#team: alice blocked bob (signed)" + dan <## "#team: alice blocked cath (signed)" bob 4" alice ##> "/_block #1 2,3 blocked=off" - alice <## "#team: you unblocked 2 members" - dan <## "#team: alice unblocked bob" - dan <## "#team: alice unblocked cath" + alice <## "#team: you unblocked 2 members (signed)" + dan <## "#team: alice unblocked bob (signed)" + dan <## "#team: alice unblocked cath (signed)" bob "/rm team dan" concurrentlyN_ - [ alice <## "#team: you removed dan from the group", + [ alice <## "#team: you removed dan from the group (signed)", do - dan <## "#team: alice removed you from the group" + dan <## "#team: alice removed you from the group (signed)" dan <## "use /d #team to delete the group", - bob <## "#team: alice removed dan from the group" + bob <## "#team: alice removed dan from the group (signed)" ] alice ##> "/block for all #team cath" - alice <## "#team: you blocked cath" - bob <## "#team: alice blocked cath" + alice <## "#team: you blocked cath (signed)" + bob <## "#team: alice blocked cath (signed)" alice ##> "/block for all #team dan" - alice <## "#team: you blocked dan" - bob <## "#team: alice blocked dan" + alice <## "#team: you blocked dan (signed)" + bob <## "#team: alice blocked dan (signed)" testGroupMemberInactive :: HasCallStack => TestParams -> IO () testGroupMemberInactive ps = do @@ -7396,15 +7396,15 @@ testGroupMemberReports = -- disableFullDeletion3 "jokes" alice bob cath alice ##> "/mr jokes bob moderator" concurrentlyN_ - [ alice <## "#jokes: you changed the role of bob to moderator", - bob <## "#jokes: alice changed your role from admin to moderator", - cath <## "#jokes: alice changed the role of bob from admin to moderator" + [ alice <## "#jokes: you changed the role of bob to moderator (signed)", + bob <## "#jokes: alice changed your role from admin to moderator (signed)", + cath <## "#jokes: alice changed the role of bob from admin to moderator (signed)" ] alice ##> "/mr jokes cath member" concurrentlyN_ - [ alice <## "#jokes: you changed the role of cath to member", - bob <## "#jokes: alice changed the role of cath from admin to member", - cath <## "#jokes: alice changed your role from admin to member" + [ alice <## "#jokes: you changed the role of cath to member (signed)", + bob <## "#jokes: alice changed the role of cath from admin to member (signed)", + cath <## "#jokes: alice changed your role from admin to member (signed)" ] alice ##> "/create link #jokes" gLink <- getGroupLink alice "jokes" GRMember True @@ -7841,13 +7841,13 @@ testScopedSupportForwardWhileReview = alice <## "changed member admission rules" concurrentlyN_ [ do - bob <## "alice updated group #team:" + bob <## "alice updated group #team: (signed)" bob <## "changed member admission rules", do - cath <## "alice updated group #team:" + cath <## "alice updated group #team: (signed)" cath <## "changed member admission rules", do - dan <## "alice updated group #team:" + dan <## "alice updated group #team: (signed)" dan <## "changed member admission rules" ] @@ -7924,13 +7924,13 @@ testScopedSupportForwardAll = alice <## "changed member admission rules" concurrentlyN_ [ do - bob <## "alice updated group #team:" + bob <## "alice updated group #team: (signed)" bob <## "changed member admission rules", do - cath <## "alice updated group #team:" + cath <## "alice updated group #team: (signed)" cath <## "changed member admission rules", do - dan <## "alice updated group #team:" + dan <## "alice updated group #team: (signed)" dan <## "changed member admission rules" ] @@ -7978,16 +7978,16 @@ testScopedSupportForwardAll = dan <## "changed to #my_team" concurrentlyN_ [ do - alice <## "dan updated group #team:" + alice <## "dan updated group #team: (signed)" alice <## "changed to #my_team", do - bob <## "dan updated group #team:" + bob <## "dan updated group #team: (signed)" bob <## "changed to #my_team", do - cath <## "dan updated group #team:" + cath <## "dan updated group #team: (signed)" cath <## "changed to #my_team", do - eve <## "dan updated group #team:" + eve <## "dan updated group #team: (signed)" eve <## "changed to #my_team" ] @@ -8061,11 +8061,11 @@ testScopedSupportForwardMemberRemoval = -- bob removes eve, eve and dan receive member removal message bob ##> "/_remove #1 5" concurrentlyN_ - [ bob <## "#team: you removed eve from the group", - alice <## "#team: bob removed eve from the group", - dan <## "#team: bob removed eve from the group", + [ bob <## "#team: you removed eve from the group (signed)", + alice <## "#team: bob removed eve from the group (signed)", + dan <## "#team: bob removed eve from the group (signed)", do - eve <## "#team: bob removed you from the group" + eve <## "#team: bob removed you from the group (signed)" eve <## "use /d #team to delete the group" ] @@ -8084,13 +8084,13 @@ setupReviewForward alice bob cath dan eve = do alice <## "changed member admission rules" concurrentlyN_ [ do - bob <## "alice updated group #team:" + bob <## "alice updated group #team: (signed)" bob <## "changed member admission rules", do - cath <## "alice updated group #team:" + cath <## "alice updated group #team: (signed)" cath <## "changed member admission rules", do - dan <## "alice updated group #team:" + dan <## "alice updated group #team: (signed)" dan <## "changed member admission rules" ] @@ -8144,13 +8144,13 @@ testScopedSupportForwardAdminRemoval = -- bob removes eve, eve and dan receive member removal message bob ##> "/rm team alice" concurrentlyN_ - [ bob <## "#team: you removed alice from the group", + [ bob <## "#team: you removed alice from the group (signed)", do - alice <## "#team: bob removed you from the group" + alice <## "#team: bob removed you from the group (signed)" alice <## "use /d #team to delete the group", - cath <## "#team: bob removed alice from the group", - dan <## "#team: bob removed alice from the group", - eve <## "#team: bob removed alice from the group" + cath <## "#team: bob removed alice from the group (signed)", + dan <## "#team: bob removed alice from the group (signed)", + eve <## "#team: bob removed alice from the group (signed)" ] -- there is no forwarding admin anymore between bob and cath, @@ -8191,9 +8191,9 @@ testScopedSupportForwardLeave = eve ##> "/leave #team" eve <## "#team: you left the group" eve <## "use /d #team to delete the group" - alice <## "#team: eve left the group" - bob <## "#team: eve left the group" - dan <## "#team: eve left the group" + alice <## "#team: eve left the group (signed)" + bob <## "#team: eve left the group (signed)" + dan <## "#team: eve left the group (signed)" alice ##> "#team (support: eve) hi" alice <## "bad chat command: support member not current or pending" @@ -8214,18 +8214,18 @@ testScopedSupportForwardGroupDeletion = -- if bob deletes the group, alice forwards it to eve and dan bob ##> "/d #team" concurrentlyN_ - [ bob <## "#team: you deleted the group", + [ bob <## "#team: you deleted the group (signed)", do - alice <## "#team: bob deleted the group" + alice <## "#team: bob deleted the group (signed)" alice <## "use /d #team to delete the local copy of the group", do - cath <## "#team: bob deleted the group" + cath <## "#team: bob deleted the group (signed)" cath <## "use /d #team to delete the local copy of the group", do - dan <## "#team: bob deleted the group" + dan <## "#team: bob deleted the group (signed)" dan <## "use /d #team to delete the local copy of the group", do - eve <## "#team: bob deleted the group" + eve <## "#team: bob deleted the group (signed)" eve <## "use /d #team to delete the local copy of the group" ] @@ -8431,7 +8431,7 @@ testScopedSupportUnreadStatsOnDelete = alice ##> "/set delete #team on" alice <## "updated group preferences:" alice <## "Full deletion: on" - bob <## "alice updated group #team:" + bob <## "alice updated group #team: (signed)" bob <## "updated group preferences:" bob <## "Full deletion: on" @@ -8586,11 +8586,11 @@ testScopedSupportMemberRemoved = cath ##> "/rm team bob" concurrentlyN_ - [ cath <## "#team: you removed bob from the group", + [ cath <## "#team: you removed bob from the group (signed)", do - bob <## "#team: cath removed you from the group" + bob <## "#team: cath removed you from the group (signed)" bob <## "use /d #team to delete the group", - alice <## "#team: cath removed bob from the group" + alice <## "#team: cath removed bob from the group (signed)" ] alice ##> "/member support chats #team" @@ -8618,9 +8618,9 @@ testScopedSupportUserRemovesMember = alice ##> "/rm team bob" concurrentlyN_ - [ alice <## "#team: you removed bob from the group", + [ alice <## "#team: you removed bob from the group (signed)", do - bob <## "#team: alice removed you from the group" + bob <## "#team: alice removed you from the group (signed)" bob <## "use /d #team to delete the group" ] @@ -8653,7 +8653,7 @@ testScopedSupportMemberLeaves = [ do bob <## "#team: you left the group" bob <## "use /d #team to delete the group", - alice <## "#team: bob left the group" + alice <## "#team: bob left the group (signed)" ] alice ##> "/member support chats #team" @@ -8685,11 +8685,11 @@ testSupportPreferenceGroup = alice <## "Chat with admins: off" concurrentlyN_ [ do - bob <## "alice updated group #team:" + bob <## "alice updated group #team: (signed)" bob <## "updated group preferences:" bob <## "Chat with admins: off", do - cath <## "alice updated group #team:" + cath <## "alice updated group #team: (signed)" cath <## "updated group preferences:" cath <## "Chat with admins: off" ] @@ -11499,7 +11499,7 @@ testRelayRejectRaceConcurrentInvitations ps = -- first rejection alice ##> "/rm #team bob" - alice .<##. ("#team: you removed bob from the group", "") + alice .<##. ("#team: you removed bob from the group (signed)", "") threadDelay 100000 alice ##> "/_add relays #1 1" alice <## "#team: group relays:" @@ -11514,7 +11514,7 @@ testRelayRejectRaceConcurrentInvitations ps = -- second rejection alice ##> "/rm #team bob" - alice .<##. ("#team: you removed bob from the group", "") + alice .<##. ("#team: you removed bob from the group (signed)", "") threadDelay 100000 alice ##> "/_add relays #1 1" alice <## "#team: group relays:" diff --git a/tests/ChatTests/Profiles.hs b/tests/ChatTests/Profiles.hs index c9f3864a82..b8b23c1a37 100644 --- a/tests/ChatTests/Profiles.hs +++ b/tests/ChatTests/Profiles.hs @@ -1218,7 +1218,7 @@ testBusinessUpdateProfiles = testChat4 businessProfile aliceProfile bobProfile c biz <# "#alisa alisa_1> hello again" -- customer can invite members too, if business allows biz ##> "/mr alisa alisa_1 admin" - biz <## "#alisa: you changed the role of alisa_1 to admin" + biz <## "#alisa: you changed the role of alisa_1 to admin (signed)" alice <## "#biz: biz_1 changed your role from member to admin" connectUsers alice bob alice ##> "/a #biz bob" @@ -2006,11 +2006,11 @@ testJoinGroupIncognito = -- remove member alice ##> ("/rm secret_club " <> cathIncognito) concurrentlyN_ - [ alice <## ("#secret_club: you removed " <> cathIncognito <> " from the group"), - bob <## ("#secret_club: alice removed " <> cathIncognito <> " from the group"), - dan <## ("#secret_club: alice removed " <> cathIncognito <> " from the group"), + [ alice <## ("#secret_club: you removed " <> cathIncognito <> " from the group (signed)"), + bob <## ("#secret_club: alice removed " <> cathIncognito <> " from the group (signed)"), + dan <## ("#secret_club: alice removed " <> cathIncognito <> " from the group (signed)"), do - cath <## "#secret_club: alice removed you from the group" + cath <## "#secret_club: alice removed you from the group (signed)" cath <## "use /d #secret_club to delete the group" ] bob #> "#secret_club hi" @@ -2149,10 +2149,10 @@ testDeleteContactThenGroupDeletesIncognitoProfile = testChat2 aliceProfile bobPr [ do bob <## "#team: you left the group" bob <## "use /d #team to delete the group", - alice <## ("#team: " <> bobIncognito <> " left the group") + alice <## ("#team: " <> bobIncognito <> " left the group (signed)") ] bob ##> "/d #team" - bob <## "#team: you deleted the group" + bob <## "#team: you deleted your local copy of the group" bob `hasContactProfiles` ["bob"] testDeleteGroupThenContactDeletesIncognitoProfile :: HasCallStack => TestParams -> IO () @@ -2194,10 +2194,10 @@ testDeleteGroupThenContactDeletesIncognitoProfile = testChat2 aliceProfile bobPr [ do bob <## "#team: you left the group" bob <## "use /d #team to delete the group", - alice <## ("#team: " <> bobIncognito <> " left the group") + alice <## ("#team: " <> bobIncognito <> " left the group (signed)") ] bob ##> "/d #team" - bob <## "#team: you deleted the group" + bob <## "#team: you deleted your local copy of the group" bob `hasContactProfiles` ["alice", "bob", T.pack bobIncognito] -- delete contact bob ##> "/d alice" @@ -2566,7 +2566,7 @@ testUpdateGroupPrefs = alice <## "updated group preferences:" alice <## "Full deletion: on" alice #$> ("/_get chat #1 count=100", chat, sndGroupFeatures <> [(0, "connected"), (1, "Full deletion: on")]) - bob <## "alice updated group #team:" + bob <## "alice updated group #team: (signed)" bob <## "updated group preferences:" bob <## "Full deletion: on" threadDelay 500000 @@ -2576,7 +2576,7 @@ testUpdateGroupPrefs = alice <## "Full deletion: off" alice <## "Voice messages: off" alice #$> ("/_get chat #1 count=100", chat, sndGroupFeatures <> [(0, "connected"), (1, "Full deletion: on"), (1, "Full deletion: off"), (1, "Voice messages: off")]) - bob <## "alice updated group #team:" + bob <## "alice updated group #team: (signed)" bob <## "updated group preferences:" bob <## "Full deletion: off" bob <## "Voice messages: off" @@ -2586,7 +2586,7 @@ testUpdateGroupPrefs = alice <## "updated group preferences:" alice <## "Voice messages: on" alice #$> ("/_get chat #1 count=100", chat, sndGroupFeatures <> [(0, "connected"), (1, "Full deletion: on"), (1, "Full deletion: off"), (1, "Voice messages: off"), (1, "Voice messages: on")]) - bob <## "alice updated group #team:" + bob <## "alice updated group #team: (signed)" bob <## "updated group preferences:" bob <## "Voice messages: on" threadDelay 500000 @@ -2639,7 +2639,7 @@ testAllowFullDeletionGroup = alice ##> "/set delete #team on" alice <## "updated group preferences:" alice <## "Full deletion: on" - bob <## "alice updated group #team:" + bob <## "alice updated group #team: (signed)" bob <## "updated group preferences:" bob <## "Full deletion: on" alice #$> ("/_get chat #1 count=100", chat, sndGroupFeatures <> [(0, "connected"), (1, "hi"), (0, "hey"), (1, "Full deletion: on")]) @@ -2703,7 +2703,7 @@ testProhibitDirectMessages = where directProhibited :: HasCallStack => TestCC -> IO () directProhibited cc = do - cc <## "alice updated group #team:" + cc <## "alice updated group #team: (signed)" cc <## "updated group preferences:" cc <## "Direct messages: off" @@ -2759,7 +2759,7 @@ testEnableTimedMessagesGroup = alice ##> "/_group_profile #1 {\"displayName\": \"team\", \"fullName\": \"\", \"groupPreferences\": {\"timedMessages\": {\"enable\": \"on\", \"ttl\": 1}, \"directMessages\": {\"enable\": \"on\"}, \"history\": {\"enable\": \"on\"}}}" alice <## "updated group preferences:" alice <## "Disappearing messages: on (1 sec)" - bob <## "alice updated group #team:" + bob <## "alice updated group #team: (signed)" bob <## "updated group preferences:" bob <## "Disappearing messages: on (1 sec)" threadDelay 1000000 @@ -2777,7 +2777,7 @@ testEnableTimedMessagesGroup = alice ##> "/set disappear #team off" alice <## "updated group preferences:" alice <## "Disappearing messages: off" - bob <## "alice updated group #team:" + bob <## "alice updated group #team: (signed)" bob <## "updated group preferences:" bob <## "Disappearing messages: off" threadDelay 1000000 @@ -2790,13 +2790,13 @@ testEnableTimedMessagesGroup = alice ##> "/set disappear #team on 30s" alice <## "updated group preferences:" alice <## "Disappearing messages: on (30 sec)" - bob <## "alice updated group #team:" + bob <## "alice updated group #team: (signed)" bob <## "updated group preferences:" bob <## "Disappearing messages: on (30 sec)" alice ##> "/set disappear #team week" -- "on" is optional alice <## "updated group preferences:" alice <## "Disappearing messages: on (1 week)" - bob <## "alice updated group #team:" + bob <## "alice updated group #team: (signed)" bob <## "updated group preferences:" bob <## "Disappearing messages: on (1 week)" @@ -2914,7 +2914,7 @@ testGroupPrefsDirectForRole = testChat4 aliceProfile bobProfile cathProfile danP where directForOwners :: HasCallStack => TestCC -> IO () directForOwners cc = do - cc <## "alice updated group #team:" + cc <## "alice updated group #team: (signed)" cc <## "updated group preferences:" cc <## "Direct messages: on for owners" @@ -2949,7 +2949,7 @@ testGroupPrefsFilesForRole = testChat3 aliceProfile bobProfile cathProfile $ where filesForOwners :: HasCallStack => TestCC -> IO () filesForOwners cc = do - cc <## "alice updated group #team:" + cc <## "alice updated group #team: (signed)" cc <## "updated group preferences:" cc <## "Files and media: on for owners" @@ -2991,7 +2991,7 @@ testGroupPrefsSimplexLinksForRole = testChat3 aliceProfile bobProfile cathProfil where linksForOwners :: HasCallStack => TestCC -> IO () linksForOwners cc = do - cc <## "alice updated group #team:" + cc <## "alice updated group #team: (signed)" cc <## "updated group preferences:" cc <## "SimpleX links: on for owners" @@ -3636,10 +3636,10 @@ testShortLinkAddressPrepareBusiness = testChat3 businessProfile aliceProfile {fu bob <## "business address: known business #biz" bob <## "use #biz to send messages" biz ##> "/d #bob" - biz <## "#bob: you deleted the group" - alice <## "#bob: biz deleted the group" + biz <## "#bob: you deleted the group (signed)" + alice <## "#bob: biz deleted the group (signed)" alice <## "use /d #bob to delete the local copy of the group" - bob <## "#biz: biz_1 deleted the group" + bob <## "#biz: biz_1 deleted the group (signed)" bob <## "use /d #biz to delete the local copy of the group" bob ##> ("/_connect plan 1 " <> shortLink) bob <## "business address: ok to connect" @@ -4395,7 +4395,7 @@ testShortLinkGroupChangeProfile = testChat3 aliceProfile bobProfile cathProfile alice ##> "/gp team club" alice <## "changed to #club" - cath <## "alice updated group #team:" + cath <## "alice updated group #team: (signed)" cath <## "changed to #club" bob ##> ("/_connect plan 1 " <> shortLink) @@ -4433,7 +4433,7 @@ testShortLinkGroupChangeProfileReceived = testChat3 aliceProfile bobProfile cath cath ##> "/gp team club" cath <## "changed to #club" - alice <## "cath updated group #team:" + alice <## "cath updated group #team: (signed)" alice <## "changed to #club" threadDelay 250000 diff --git a/tests/ProtocolTests.hs b/tests/ProtocolTests.hs index ca70ae094f..243a21a10b 100644 --- a/tests/ProtocolTests.hs +++ b/tests/ProtocolTests.hs @@ -256,13 +256,13 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do ==# XContact testProfile Nothing Nothing Nothing (Just (SharedMsgId "\1\2\3\4", MCText {text = "hello"})) it "x.grp.inv" $ "{\"v\":\"9\",\"event\":\"x.grp.inv\",\"params\":{\"groupInvitation\":{\"connRequest\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"invitedMember\":{\"memberRole\":\"member\",\"memberId\":\"BQYHCA==\"},\"groupProfile\":{\"fullName\":\"Team\",\"displayName\":\"team\",\"groupPreferences\":{\"reactions\":{\"enable\":\"on\"},\"voice\":{\"enable\":\"on\"}}},\"fromMember\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\"}}}}" - #==# XGrpInv GroupInvitation {fromMember = MemberIdRole (MemberId "\1\2\3\4") GRAdmin, invitedMember = MemberIdRole (MemberId "\5\6\7\8") GRMember, connRequest = testConnReq, groupProfile = testGroupProfile, business = Nothing, groupLinkId = Nothing, groupSize = Nothing} + #==# XGrpInv GroupInvitation {fromMember = MemberIdRole (MemberId "\1\2\3\4") GRAdmin, fromMemberKey = Nothing, invitedMember = MemberIdRole (MemberId "\5\6\7\8") GRMember, connRequest = testConnReq, groupProfile = testGroupProfile, business = Nothing, groupLinkId = Nothing, groupSize = Nothing} it "x.grp.inv with group link id" $ "{\"v\":\"9\",\"event\":\"x.grp.inv\",\"params\":{\"groupInvitation\":{\"connRequest\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"invitedMember\":{\"memberRole\":\"member\",\"memberId\":\"BQYHCA==\"},\"groupProfile\":{\"fullName\":\"Team\",\"displayName\":\"team\",\"groupPreferences\":{\"reactions\":{\"enable\":\"on\"},\"voice\":{\"enable\":\"on\"}}},\"fromMember\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\"}, \"groupLinkId\":\"AQIDBA==\"}}}" - #==# XGrpInv GroupInvitation {fromMember = MemberIdRole (MemberId "\1\2\3\4") GRAdmin, invitedMember = MemberIdRole (MemberId "\5\6\7\8") GRMember, connRequest = testConnReq, groupProfile = testGroupProfile, business = Nothing, groupLinkId = Just $ GroupLinkId "\1\2\3\4", groupSize = Nothing} + #==# XGrpInv GroupInvitation {fromMember = MemberIdRole (MemberId "\1\2\3\4") GRAdmin, fromMemberKey = Nothing, invitedMember = MemberIdRole (MemberId "\5\6\7\8") GRMember, connRequest = testConnReq, groupProfile = testGroupProfile, business = Nothing, groupLinkId = Just $ GroupLinkId "\1\2\3\4", groupSize = Nothing} it "x.grp.acpt without incognito profile" $ "{\"v\":\"9\",\"event\":\"x.grp.acpt\",\"params\":{\"memberId\":\"AQIDBA==\"}}" - #==# XGrpAcpt (MemberId "\1\2\3\4") + #==# XGrpAcpt (MemberId "\1\2\3\4") Nothing it "x.grp.mem.new" $ "{\"v\":\"9\",\"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, memberKey = Nothing} Nothing