invitation to public group in group

This commit is contained in:
Evgeny @ SimpleX Chat committed 2026-04-13 17:54:56 +00:00
1 parent d070a75f11
commit 1df5b548a1
5 files changed
+61 -28

No files matched your search

+2 -1
View File
@@ -519,7 +519,7 @@ data ChatCommand
| APIGetGroupRelays {groupId :: GroupId}
| NewPublicGroup IncognitoEnabled (NonEmpty Int64) GroupProfile
| AddMember GroupName ContactName GroupMemberRole
| SharePublicGroup GroupName ContactName
| SharePublicGroup GroupName ChatName
| JoinGroup {groupName :: GroupName, enableNtfs :: MsgFilter}
| AcceptMember GroupName ContactName GroupMemberRole
| MemberRole GroupName ContactName GroupMemberRole
@@ -880,6 +880,7 @@ data ChatEvent
| CEvtHostConnected {protocol :: AProtocolType, transportHost :: TransportHost}
| CEvtHostDisconnected {protocol :: AProtocolType, transportHost :: TransportHost}
| CEvtReceivedGroupInvitation {user :: User, groupInfo :: GroupInfo, contact :: Contact, fromMemberRole :: GroupMemberRole, memberRole :: GroupMemberRole}
| CEvtReceivedPublicGroupInvitation {user :: User, sharedGroupInfo :: GroupInfo, contact_ :: Maybe Contact, fromGroupInfo_ :: Maybe GroupInfo, fromMember_ :: Maybe GroupMember}
| CEvtUserJoinedGroup {user :: User, groupInfo :: GroupInfo, hostMember :: GroupMember}
| CEvtGroupLinkDataUpdated {user :: User, groupInfo :: GroupInfo, groupLink :: GroupLink, groupRelays :: [GroupRelay], relaysChanged :: Bool}
| CEvtGroupRelayUpdated {user :: User, groupInfo :: GroupInfo, member :: GroupMember, groupRelay :: GroupRelay}
+19 -15
View File
@@ -2472,11 +2472,16 @@ processChatCommand vr nm = \case
pure (gInfo, relays)
pure $ CRGroupRelays user gInfo relays
APISharePublicGroup groupId toChatRef -> withUser $ \user -> do
gInfo@GroupInfo {groupProfile, membership, groupSummary, localDisplayName = gName} <- withFastStore $ \db -> getGroupInfo db vr user groupId
gInfo@GroupInfo {groupProfile, membership = GroupMember {memberId, groupMemberId}, groupSummary, localDisplayName} <- withFastStore $ \db -> getGroupInfo db vr user groupId
unless (useRelays' gInfo) $ throwCmdError "not a public group"
let groupSize = fromIntegral $ currentMembers groupSummary
GroupMember {memberId = mId, groupMemberId = mGroupMemberId} = membership
ciGroupInv oss = CIGroupInvitation {groupId, groupMemberId = mGroupMemberId, localDisplayName = gName, groupProfile, status = CIGISPending, ownerSigStatus = oss}
sendInv :: forall c. ChatTypeI c => Maybe GroupOwnerSig -> SndMessage -> ChatDirection c 'MDSnd -> ChatInfo c -> CM ChatResponse
sendInv groupOwner msg cd cInfo = do
let oss = if isJust groupOwner then Just OSSVerified else Nothing
ciContent = CISndGroupInvitation (CIGroupInvitation {groupId, groupMemberId, localDisplayName, groupProfile, status = CIGISPending, ownerSigStatus = oss}) GRObserver
ci <- saveSndChatItem' user cd msg ciContent Nothing Nothing Nothing Nothing False
toView $ CEvtNewChatItems user [AChatItem chatTypeI SMDSnd cInfo ci]
pure $ CRSentPublicGroupInvitation user gInfo
case toChatRef of
ChatRef CTDirect contactId _ -> do
ct@Contact {activeConn = Just ctConn} <- withFastStore $ \db -> getContact db vr user contactId
@@ -2484,13 +2489,10 @@ processChatCommand vr nm = \case
adHash <- withAgent $ \a -> getConnectionRatchetAdHash a (aConnId ctConn)
let invBody = encodeUtf8 $ encodeJSON groupProfile
sig = C.sign' memberPrivKey $ smpEncode CBDirect <> adHash <> invBody
pure GroupOwnerSig {memberId = mId, binding = B64UrlByteString adHash, ownerSig = B64UrlByteString $ C.signatureBytes sig}
pure GroupOwnerSig {memberId, binding = B64UrlByteString adHash, ownerSig = B64UrlByteString $ C.signatureBytes sig}
let inv = PublicGroupInvitation {groupProfile, groupOwner, groupSize}
oss = if isJust groupOwner then Just OSSVerified else Nothing
(msg, _) <- sendDirectContactMessage user ct $ XGrpInvPub inv
ci <- saveSndChatItem' user (CDDirectSnd ct) msg (CISndGroupInvitation (ciGroupInv oss) GRObserver) Nothing Nothing Nothing Nothing False
toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDSnd (DirectChat ct) ci]
pure $ CRSentPublicGroupInvitation user gInfo
sendInv groupOwner msg (CDDirectSnd ct) (DirectChat ct)
ChatRef CTGroup toGroupId scope -> do
(toGInfo, members) <- withFastStore $ \db -> do
g <- getGroupInfo db vr user toGroupId
@@ -2498,9 +2500,7 @@ processChatCommand vr nm = \case
pure (g, ms)
let inv = PublicGroupInvitation {groupProfile, groupOwner = Nothing, groupSize}
msg <- sendGroupMessage user toGInfo scope members $ XGrpInvPub inv
ci <- saveSndChatItem' user (CDGroupSnd toGInfo Nothing) msg (CISndGroupInvitation (ciGroupInv Nothing) GRObserver) Nothing Nothing Nothing Nothing False
toView $ CEvtNewChatItems user [AChatItem SCTGroup SMDSnd (GroupChat toGInfo Nothing) ci]
pure $ CRSentPublicGroupInvitation user gInfo
sendInv Nothing msg (CDGroupSnd toGInfo Nothing) (GroupChat toGInfo Nothing)
_ -> throwCmdError "unsupported chat type"
APIAddMember groupId contactId memRole -> withUser $ \user -> withGroupLock "addMember" groupId $ do
-- TODO for large groups: no need to load all members to determine if contact is a member
@@ -2889,9 +2889,13 @@ processChatCommand vr nm = \case
AddMember gName cName memRole -> withUser $ \user -> do
(groupId, contactId) <- withFastStore $ \db -> (,) <$> getGroupIdByName db user gName <*> getContactIdByName db user cName
processChatCommand vr nm $ APIAddMember groupId contactId memRole
SharePublicGroup gName cName -> withUser $ \user -> do
(groupId, contactId) <- withFastStore $ \db -> (,) <$> getGroupIdByName db user gName <*> getContactIdByName db user cName
processChatCommand vr nm $ APISharePublicGroup groupId (ChatRef CTDirect contactId Nothing)
SharePublicGroup gName (ChatName cType toName) -> withUser $ \user -> do
groupId <- withFastStore $ \db -> getGroupIdByName db user gName
chatRef <- case cType of
CTDirect -> ChatRef CTDirect <$> withFastStore (\db -> getContactIdByName db user toName) <*> pure Nothing
CTGroup -> ChatRef CTGroup <$> withFastStore (\db -> getGroupIdByName db user toName) <*> pure Nothing
_ -> throwCmdError "unsupported chat type"
processChatCommand vr nm $ APISharePublicGroup groupId chatRef
JoinGroup gName enableNtfs -> withUser $ \user -> do
groupId <- withFastStore $ \db -> getGroupIdByName db user gName
processChatCommand vr nm $ APIJoinGroup groupId enableNtfs
@@ -4845,7 +4849,7 @@ chatCommandP =
"/_ntf conn messages " *> (APIGetConnNtfMessages <$> connMsgsP),
"/_add #" *> (APIAddMember <$> A.decimal <* A.space <*> A.decimal <*> memberRole),
"/_share #" *> (APISharePublicGroup <$> A.decimal <* A.space <*> chatRefP),
"/share " *> char_ '#' *> (SharePublicGroup <$> displayNameP <* A.space <* char_ '@' <*> displayNameP),
"/share " *> char_ '#' *> (SharePublicGroup <$> displayNameP <* A.space <*> chatNameP),
"/_join #" *> (APIJoinGroup <$> A.decimal <*> pure MFAll), -- needs to be changed to support in UI
"/_accept member #" *> (APIAcceptMember <$> A.decimal <* A.space <*> A.decimal <*> memberRole),
"/_delete member chat #" *> (APIDeleteMemberSupportChat <$> A.decimal <* A.space <*> A.decimal),
+17 -10
View File
@@ -1029,6 +1029,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
XInfoProbe probe -> Nothing <$ xInfoProbe (COMGroupMember m'') probe
XInfoProbeCheck probeHash -> Nothing <$ xInfoProbeCheck (COMGroupMember m'') probeHash
XInfoProbeOk probe -> Nothing <$ xInfoProbeOk (COMGroupMember m'') probe
XGrpInvPub pubGrpInv -> Nothing <$ processPublicGroupInvitationGroup gInfo' m'' conn' pubGrpInv msg brokerTs
BFileChunk sharedMsgId chunk -> Nothing <$ bFileChunkGroup gInfo' sharedMsgId chunk msgMeta
_ -> Nothing <$ messageError ("unsupported message: " <> tshow event)
forM deliveryTaskContext_ $ \taskContext ->
@@ -2317,6 +2318,11 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
ft <- withStore $ \db -> getDirectFileIdBySharedMsgId db user ct sharedMsgId >>= getRcvFileTransfer db user
receiveInlineChunk ft chunk meta
processPublicGroupInvitationGroup :: GroupInfo -> GroupMember -> Connection -> PublicGroupInvitation -> RcvMessage -> UTCTime -> CM ()
processPublicGroupInvitationGroup g m conn inv msg brokerTs = do
(gInfo, _) <- rcvPublicGroupInvitation conn inv msg brokerTs (CDGroupRcv g Nothing m) (GroupChat g Nothing)
toView $ CEvtReceivedPublicGroupInvitation {user, sharedGroupInfo = gInfo, contact_ = Nothing, fromGroupInfo_ = Just g, fromMember_ = Just m}
bFileChunkGroup :: GroupInfo -> SharedMsgId -> FileChunk -> MsgMeta -> CM ()
bFileChunkGroup GroupInfo {groupId} sharedMsgId chunk meta = do
ft <- withStore $ \db -> getGroupFileIdBySharedMsgId db userId groupId sharedMsgId >>= getRcvFileTransfer db user
@@ -2415,7 +2421,12 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
sameGroupLinkId _ _ = False
processPublicGroupInvitation :: Contact -> Connection -> PublicGroupInvitation -> RcvMessage -> MsgMeta -> CM ()
processPublicGroupInvitation ct conn PublicGroupInvitation {groupProfile, groupOwner, groupSize} msg msgMeta = do
processPublicGroupInvitation ct conn inv msg msgMeta = do
(gInfo, _) <- rcvPublicGroupInvitation conn inv msg (metaBrokerTs msgMeta) (CDDirectRcv ct) (DirectChat ct)
toView $ CEvtReceivedPublicGroupInvitation {user, sharedGroupInfo = gInfo, contact_ = Just ct, fromGroupInfo_ = Nothing, fromMember_ = Nothing}
rcvPublicGroupInvitation :: forall c. (ChatTypeI c, ChatTypeQuotable c) => Connection -> PublicGroupInvitation -> RcvMessage -> UTCTime -> ChatDirection c 'MDRcv -> ChatInfo c -> CM (GroupInfo, ChatItem c 'MDRcv)
rcvPublicGroupInvitation conn PublicGroupInvitation {groupProfile, groupOwner, groupSize} msg brokerTs cd cInfo = do
unless (isJust $ publicGroup groupProfile) $ messageError "x.grp.inv.pub: not a public group"
let oss = case groupOwner of
Nothing -> Nothing
@@ -2424,21 +2435,17 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
prepLink = PreparedConnLink {connFullLink = Nothing, connShortLink = sLnk_}
gVar <- asks random
subRole <- asks $ channelSubscriberRole . config
(gInfo@GroupInfo {groupId, localDisplayName = gName, membership}, _) <- withStore $ \db ->
(gInfo@GroupInfo {groupId, localDisplayName, membership = GroupMember {groupMemberId}}, _) <- withStore $ \db ->
createPreparedGroup db gVar vr user groupProfile False prepLink Nothing True subRole (Just $ fromIntegral groupSize)
withStore' $ \db -> setGroupChatHidden db user groupId True
let GroupMember {groupMemberId} = membership
content = CIRcvGroupInvitation (CIGroupInvitation {groupId, groupMemberId, localDisplayName = gName, groupProfile, status = CIGISPending, ownerSigStatus = oss}) GRObserver
(ci, cInfo) <- saveRcvChatItemNoParse user (CDDirectRcv ct) msg brokerTs content
let content = CIRcvGroupInvitation (CIGroupInvitation {groupId, groupMemberId, localDisplayName, groupProfile, status = CIGISPending, ownerSigStatus = oss}) GRObserver
(ci, _) <- saveRcvChatItemNoParse user cd msg brokerTs content
withStore' $ \db -> setGroupInvitationChatItemId db user groupId (chatItemId' ci)
toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDRcv cInfo ci]
toView $ CEvtReceivedGroupInvitation {user, groupInfo = gInfo, contact = ct, fromMemberRole = GROwner, memberRole = GRObserver}
-- start async link resolution for signature verification
toView $ CEvtNewChatItems user [AChatItem chatTypeI SMDRcv cInfo ci]
forM_ groupOwner $ \_ ->
forM_ sLnk_ $ \sLnk ->
void $ getAgentConnShortLinkAsync user CFGetGroupDataInv (Just conn) sLnk
where
brokerTs = metaBrokerTs msgMeta
pure (gInfo, ci)
processPublicGroupLinkData :: User -> Contact -> Connection -> FixedLinkData 'CMContact -> ConnLinkData 'CMContact -> CM ()
processPublicGroupLinkData _user _ct _conn _fixedLinkData _cData = do
+5
View File
@@ -472,6 +472,11 @@ chatEventToView hu ChatConfig {logLevel, showReactions, showReceipts, testView}
in ttyUser u [sShow connId <> ": END"]
CEvtSubscriptionStatus srv status conns -> [plain $ subStatusStr status <> " " <> show (length conns) <> " connections on server " <> showSMPServer srv]
CEvtReceivedGroupInvitation {user = u, groupInfo = g, contact = c, memberRole = r} -> ttyUser u $ viewReceivedGroupInvitation g c r
CEvtReceivedPublicGroupInvitation {user = u, sharedGroupInfo = g, contact_ = ct_, fromGroupInfo_ = fromG_, fromMember_ = m_} ->
ttyUser u $ case (ct_, fromG_, m_) of
(Just ct, _, _) -> [ttyFullContact ct <> " shared public group " <> ttyGroup' g]
(_, Just fromG, Just m) -> [ttyGroup' fromG <> ": " <> ttyMember m <> " shared public group " <> ttyGroup' g]
_ -> ["received public group invitation " <> ttyGroup' g]
CEvtUserJoinedGroup u g m -> ttyUser u $ viewUserJoinedGroup g m
CEvtGroupLinkDataUpdated u g groupLink relays relaysChanged
| relaysChanged -> ttyUser u $ viewGroupLinkRelaysUpdated g groupLink relays
+18 -2
View File
@@ -16,6 +16,7 @@ chatRelayTests = do
it "relay profile updated in address" testRelayProfileUpdateInAddress
describe "public group invitations" $ do
it "share public group in direct chat" testSharePublicGroupDirect
it "share public group in group chat" testSharePublicGroupInGroup
testGetSetChatRelays :: HasCallStack => TestParams -> IO ()
testGetSetChatRelays ps =
@@ -180,8 +181,23 @@ testSharePublicGroupDirect ps =
connectUsers alice cath
alice ##> "/share #team @cath"
alice <## "shared public group #team"
cath <## "#team: alice invites you to join the group as observer"
cath <## "use /j team to accept"
cath <## "alice (Alice) shared public group #team"
testSharePublicGroupInGroup :: HasCallStack => TestParams -> IO ()
testSharePublicGroupInGroup ps =
withNewTestChat ps "alice" aliceProfile $ \alice ->
withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob ->
withNewTestChat ps "cath" cathProfile $ \cath -> do
bob ##> "/ad"
(bobSLink, _cLink) <- getContactLinks bob True
alice ##> ("/relays name=bob_relay " <> bobSLink)
alice <## "ok"
createChannelWithRelay "team" alice bob
-- create a regular group with alice and cath
createGroup2 "friends" alice cath
alice ##> "/share #team #friends"
alice <## "shared public group #team"
cath <## "#friends: alice shared public group #team"
-- Create a public group with relay=1, wait for relay to join
createChannelWithRelay :: HasCallStack => String -> TestCC -> TestCC -> IO ()