add key distribution steps, and fix some tests

This commit is contained in:
Evgeny @ SimpleX Chat
2026-08-01 19:13:33 +00:00
parent 6c74a917a4
commit c4675bf89f
12 changed files with 361 additions and 340 deletions
+1 -1
View File
@@ -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}
+10 -9
View File
@@ -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,
+2
View File
@@ -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,
+20 -7
View File
@@ -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)
+4 -4
View File
@@ -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]
+35 -32
View File
@@ -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
+1 -1
View File
@@ -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 ""
+2
View File
@@ -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,
+1 -1
View File
@@ -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
+256 -256
View File
File diff suppressed because it is too large Load Diff
+26 -26
View File
@@ -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 <message> 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
+3 -3
View File
@@ -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