core, ui: invite member of one group to another group (#7682)

* rfc: invite member of one group to another group

* update

* update

* implement

* version

* update

* do not show error if contact was removed when joining group

---------

Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
This commit is contained in:
Evgeny
2026-10-11 09:50:42 +01:00
committed by GitHub
co-authored by Evgeny @ SimpleX Chat
parent 1053e9fb5c
commit 07c363849b
41 changed files with 1571 additions and 52 deletions
+3
View File
@@ -465,6 +465,7 @@ data ChatCommand
| APIGetNtfConns {nonce :: C.CbNonce, encNtfInfo :: ByteString}
| APIGetConnNtfMessages (NonEmpty ConnMsgReq)
| APIAddMember {groupId :: GroupId, contactId :: ContactId, memberRole :: GroupMemberRole}
| APIAddMemberViaGroup {groupId :: GroupId, viaGroupId :: GroupId, groupMemberId :: GroupMemberId, memberRole :: GroupMemberRole}
| APIJoinGroup {groupId :: GroupId, enableNtfs :: MsgFilter}
| APIAcceptMember {groupId :: GroupId, groupMemberId :: GroupMemberId, memberRole :: GroupMemberRole}
| APIDeleteMemberSupportChat GroupId GroupMemberId
@@ -877,6 +878,7 @@ data ChatResponse
| CRUserDeletedMembers {user :: User, groupInfo :: GroupInfo, members :: [GroupMember], withMessages :: Bool, msgSigned :: Bool}
| CRGroupsList {user :: User, groups :: [GroupInfo]}
| CRSentGroupInvitation {user :: User, groupInfo :: GroupInfo, contact :: Contact, member :: GroupMember}
| CRSentGroupInvitationViaGroup {user :: User, groupInfo :: GroupInfo, member :: GroupMember, viaGroupInfo :: GroupInfo, viaMember :: GroupMember}
| CRFileTransferStatus User (FileTransfer, [Integer]) -- TODO refactor this type to FileTransferStatus
| CRFileTransferStatusXFTP User AChatItem
| CRUserProfile {user :: User, profile :: Profile}
@@ -1028,6 +1030,7 @@ data ChatEvent
| CEvtHostConnected {protocol :: AProtocolType, transportHost :: TransportHost}
| CEvtHostDisconnected {protocol :: AProtocolType, transportHost :: TransportHost}
| CEvtReceivedGroupInvitation {user :: User, groupInfo :: GroupInfo, contact :: Contact, fromMemberRole :: GroupMemberRole, memberRole :: GroupMemberRole}
| CEvtReceivedGroupInvitationViaGroup {user :: User, groupInfo :: GroupInfo, viaGroupInfo :: GroupInfo, viaMember :: GroupMember, fromMemberRole :: GroupMemberRole, memberRole :: GroupMemberRole}
| 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}
+74 -8
View File
@@ -2839,15 +2839,51 @@ processChatCommand cxt nm = \case
pure $ CRSentGroupInvitation user gInfo contact member {memberRole = memRole}
Nothing -> throwChatError $ CEGroupCantResendInvitation gInfo cName
| otherwise -> throwChatError $ CEGroupDuplicateMember cName
APIAddMemberViaGroup groupId viaGroupId gmId memRole -> withUser $ \user -> withGroupLock "addMember" groupId $ do
(g@(GIK gInfo _), viaGInfo, m) <- withFastStore $ \db ->
(,,) <$> getGroupInfoKeys db cxt user groupId <*> getGroupInfo db cxt user viaGroupId <*> getGroupMember db cxt user viaGroupId gmId
let GroupMember {localDisplayName = mName} = m
when (viaGroupId == groupId) $ throwChatError $ CEGroupDuplicateMember mName
when (useRelays' gInfo) $ throwCmdError "can't invite member to channel"
when (useRelays' viaGInfo) $ throwCmdError "can't invite member from channel"
assertUserGroupRole gInfo $ max GRAdmin memRole
assertUserGroupRole viaGInfo GRAuthor
unless (groupFeatureUserAllowed SGFDirectMessages viaGInfo) $ throwCmdError "direct messages not allowed"
when (incognitoMembership viaGInfo) $ throwChatError CEContactIncognitoCantInvite
when (incognitoMembership gInfo) $ throwChatError CEGroupIncognitoCantInvite
mConn <- case memberConn m of
Just conn | memberCurrent m -> pure conn
_ -> throwChatError CEGroupMemberNotActive
unless (m `supportsVersion` groupInvVersion) $ throwChatError CEPeerChatVRangeIncompatible
let sendInvitation = sendGrpInvitationViaGroup user viaGInfo m mConn g
withFastStore' (\db -> getMemberInvitedVia db cxt user gInfo m) >>= \case
Nothing -> do
gVar <- asks random
subMode <- chatReadVar subscriptionMode
(agentConnId, CCLink cReq _) <- withAgent $ \a -> createConnection a nm (aUserId user) True False SCMInvitation Nothing Nothing IKPQOff True subMode
member <- withFastStore $ \db -> createNewMemberViaGroup db gVar cxt user gInfo m mConn memRole agentConnId cReq subMode
sendInvitation member cReq
pure $ CRSentGroupInvitationViaGroup user gInfo member viaGInfo m
Just member@GroupMember {groupMemberId, memberStatus, memberRole = mRole}
| memberStatus == GSMemInvited -> do
unless (mRole == memRole) $ withFastStore' $ \db -> updateGroupMemberRole db user member memRole
withFastStore' (\db -> getMemberInvitation db user groupMemberId) >>= \case
Just cReq -> do
sendInvitation member {memberRole = memRole} cReq
pure $ CRSentGroupInvitationViaGroup user gInfo member {memberRole = memRole} viaGInfo m
Nothing -> throwChatError $ CEGroupCantResendInvitation gInfo mName
| otherwise -> throwChatError $ CEGroupDuplicateMember mName
APIJoinGroup groupId enableNtfs -> withUser $ \user@User {userId} -> do
withGroupLock "joinGroup" groupId $ do
(invitation, ct) <- withFastStore $ \db -> do
(invitation, ct_) <- withFastStore $ \db -> do
inv@ReceivedGroupInvitation {fromMember} <- getGroupInvitation db cxt user groupId
(inv,) <$> getContactViaMember db cxt user fromMember
(inv,) <$> forM (memberContactId fromMember) (\_ -> getContactViaMember db cxt user fromMember)
let ReceivedGroupInvitation {fromMember, connRequest, groupInfo = g@GroupInfo {membership, chatSettings}, groupKeys = gks} = invitation
Contact {activeConn} = ct
case activeConn of
Just Connection {peerChatVRange} -> do
peerChatVRange_ = case ct_ of
Nothing -> Right $ memberChatVRange' fromMember
Just ct@Contact {activeConn} -> maybe (Left ct) (\Connection {peerChatVRange} -> Right peerChatVRange) activeConn
case peerChatVRange_ of
Right peerChatVRange -> do
subMode <- chatReadVar subscriptionMode
dm <- encodeXGrpAcpt user (GIK g gks) peerChatVRange
agentConnId <- case memberConn fromMember of
@@ -2868,9 +2904,11 @@ processChatCommand cxt nm = \case
updateGroupMemberStatus db userId fromMember GSMemInvited
updateGroupMemberStatus db userId membership GSMemInvited
throwError e
updateCIGroupInvitationStatus user g CIGISAccepted `catchAllErrors` eToView
updateCIGroupInvitationStatus user g CIGISAccepted `catchAllErrors` \case
ChatErrorStore SEChatItemNotFoundByGroupId {} -> pure ()
e -> eToView e
pure $ CRUserAcceptedGroupSent user g {membership = membership {memberStatus = GSMemAccepted}} Nothing
Nothing -> throwChatError $ CEContactNotActive ct
Left ct -> throwChatError $ CEContactNotActive ct
APIAcceptMember groupId gmId role -> withUser $ \user@User {userId} -> do
(g@(GIK gInfo _), m) <- withFastStore $ \db -> (,) <$> getGroupInfoKeys db cxt user groupId <*> getGroupMemberById db cxt user gmId
-- same rule as role change (moderators grant up to member); pending member's role is a stand-in, so treat it as member
@@ -4318,6 +4356,25 @@ processChatCommand cxt nm = \case
toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDSnd (DirectChat ct) ci]
forM_ (timed_ >>= timedDeleteAt') $
startProximateTimedItemThread user (ChatRef CTDirect contactId Nothing, chatItemId' ci)
sendGrpInvitationViaGroup :: User -> GroupInfo -> GroupMember -> Connection -> GroupInfoKeys -> GroupMember -> ConnReqInvitation -> CM ()
sendGrpInvitationViaGroup user viaGInfo GroupMember {localDisplayName} mConn (GIK gInfo@GroupInfo {groupId, groupProfile, membership, businessChat} gks) GroupMember {groupMemberId, memberId, memberRole = memRole} cReq = do
let currentMemCount = fromIntegral $ currentMembers $ groupSummary gInfo
GroupMember {memberRole = userRole, memberId = userMemberId} = membership
groupInv =
GroupInvitation
{ fromMember = MemberIdRole userMemberId userRole,
fromMemberKey = Just $ groupMemberKey gks,
invitedMember = MemberIdRole memberId memRole,
connRequest = cReq,
groupProfile,
business = businessChat,
groupLinkId = Nothing,
groupSize = Just currentMemCount
}
(msg, _, _) <- sendDirectMemberMessage mConn (XGrpInv groupInv) (groupId' viaGInfo)
let content = CISndGroupInvitation (CIGroupInvitation {groupId, groupMemberId, localDisplayName, groupProfile, status = CIGISPending}) memRole
ci <- saveSndChatItem user (CDGroupSnd viaGInfo Nothing) msg content
toView $ CEvtNewChatItems user [AChatItem SCTGroup SMDSnd (GroupChat viaGInfo Nothing) ci]
addRelays :: User -> GroupInfoKeys -> ShortLinkContact -> [UserChatRelay] -> CM [(UserChatRelay, Either ChatError GroupRelay)]
addRelays user g@(GIK gInfo@GroupInfo {membership} gks) groupSLink relays =
mapConcurrently addRelay relays
@@ -4773,7 +4830,7 @@ processChatCommand cxt nm = \case
shortenShortLink' =<< withAgent (\a -> setConnShortLink a nm (aConnId' conn) SCMInvitation userLinkData Nothing False Nothing)
updateCIGroupInvitationStatus :: User -> GroupInfo -> CIGroupInvitationStatus -> CM ()
updateCIGroupInvitationStatus user GroupInfo {groupId} newStatus = do
AChatItem _ _ cInfo ChatItem {content, meta = CIMeta {itemId}} <- withFastStore $ \db -> getChatItemByGroupId db cxt user groupId
AChatItem _ _ cInfo ci@ChatItem {content, meta = meta@CIMeta {itemId}} <- withFastStore $ \db -> getChatItemByGroupId db cxt user groupId
case (cInfo, content) of
(DirectChat ct@Contact {contactId}, CIRcvGroupInvitation ciGroupInv@CIGroupInvitation {status} memRole)
| status == CIGISPending -> do
@@ -4782,6 +4839,14 @@ processChatCommand cxt nm = \case
updateDirectChatItemView user ct itemId aciContent False False timed_ Nothing
forM_ (timed_ >>= timedDeleteAt') $
startProximateTimedItemThread user (ChatRef CTDirect contactId Nothing, itemId)
(GroupChat g _, CIRcvGroupInvitation ciGroupInv@CIGroupInvitation {status} memRole)
| status == CIGISPending -> do
let content' = CIRcvGroupInvitation (ciGroupInv {status = newStatus} :: CIGroupInvitation) memRole
timed_ <- sndGroupCITimed False g Nothing
ci' <- withFastStore' $ \db -> updateGroupChatItem db user (groupId' g) ci {meta = meta {itemTimed = timed_}} content' False False Nothing
toView $ CEvtChatItemUpdated user (AChatItem SCTGroup SMDRcv cInfo ci')
forM_ (timed_ >>= timedDeleteAt') $
startProximateTimedItemThread user (ChatRef CTGroup (groupId' g) Nothing, itemId)
_ -> pure () -- prohibited
assertAllowedContent :: MsgContent -> CM ()
assertAllowedContent = \case
@@ -6168,6 +6233,7 @@ chatCommandP =
"/_ntf conns " *> (APIGetNtfConns <$> strP <* A.space <*> strP),
"/_ntf conn messages " *> (APIGetConnNtfMessages <$> connMsgsP),
"/_add #" *> (APIAddMember <$> A.decimal <* A.space <*> A.decimal <*> memberRole),
"/_add #" *> (APIAddMemberViaGroup <$> A.decimal <* " #" <*> A.decimal <* A.space <*> A.decimal <*> memberRole),
"/_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),
+50 -18
View File
@@ -1121,6 +1121,7 @@ processAgentMessageConn cxt user@User {userId} entity gks_ corrId agentConnId ag
XGrpRoster gr -> fmap ctx <$> xGrpRoster gInfo' m'' m'' gr verifiedMsg sharedMsgId_ brokerTs
XGrpRosterAck ackVer ackErr -> Nothing <$ xGrpRosterAck (GIK gInfo' gks) m'' ackVer ackErr
XGrpRosterRequest reqVer -> Nothing <$ xGrpRosterRequest (GIK gInfo' gks) m'' reqVer
XGrpInv gInv -> Nothing <$ xGrpInv gInfo' m'' conn' gInv msg brokerTs
-- TODO [knocking] why don't we forward these messages?
XGrpDirectInv connReq mContent_ msgScope -> memberCanSend (Just m'') msgScope $ Nothing <$ xGrpDirectInv gInfo' m'' conn' connReq mContent_ msg brokerTs
XGrpMsgForward fwd msg' -> Nothing <$ xGrpMsgForward (GIK gInfo' gks) Nothing m'' fwd (ParsedMsg Nothing Nothing msg') brokerTs
@@ -1654,7 +1655,7 @@ processAgentMessageConn cxt user@User {userId} entity gks_ corrId agentConnId ag
toViewTE $ TERejectingGroupJoinRequestMember user gInfo mem rjctReason
xGrpRelayInv :: InvitationId -> VersionRangeChat -> Maybe SignedMsg -> GroupRelayInvitation -> CM ()
xGrpRelayInv invId chatVRange signedMsg_ groupRelayInv@GroupRelayInvitation {groupLink}
| maxVersion chatVRange >= signedRelayInvVersion && isNothing presHeader_ =
| maxVersion chatVRange >= groupInvVersion && isNothing presHeader_ =
messageError "x.grp.relay.inv: invitation is not signed by the owner"
| otherwise = do
rejected <- withStore' $ \db -> isRelayGroupRejected db user groupLink
@@ -2703,27 +2704,14 @@ processAgentMessageConn cxt user@User {userId} entity gks_ corrId agentConnId ag
| otherwise = do
let Contact {localDisplayName = c, activeConn} = ct
GroupInvitation {fromMember = (MemberIdRole fromMemId fromRole), invitedMember = (MemberIdRole memId memRole), connRequest, groupLinkId} = inv
forM_ activeConn $ \Connection {connId, connChatVersion, peerChatVRange, customUserProfileId, groupLinkId = groupLinkId'} -> do
forM_ activeConn $ \conn@Connection {peerChatVRange, customUserProfileId, groupLinkId = groupLinkId'} -> do
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
memberKeys <- atomically . C.generateKeyPair =<< asks random
(GIK gInfo@GroupInfo {groupId, localDisplayName, groupProfile, membership} gks, hostId) <- withStore $ \db -> createGroupInvitation db cxt user ct inv customUserProfileId memberKeys
(g@(GIK gInfo@GroupInfo {groupId, localDisplayName, groupProfile, membership} _), hostId) <- withStore $ \db -> createGroupInvitation db cxt user (Left ct) peerChatVRange inv customUserProfileId memberKeys
void $ createChatItem user (CDGroupSnd gInfo Nothing) False CIChatBanner Nothing Nothing (Just epochStart)
let GroupMember {groupMemberId} = membership
-- hostContact is only reported for group links, where the client replaces
-- the transient host connection view with the group and removes its chat
joinGroupAsync hostContact_ sameLink = do
subMode <- chatReadVar subscriptionMode
dm <- encodeXGrpAcpt user (GIK gInfo gks) peerChatVRange
(connIds@(cmdId, acId), _) <- prepareAgentJoin user True connRequest
withStore' $ \db -> do
when sameLink $ setViaGroupLinkUri db groupId connId
createMemberConnectionAsync db user hostId connIds connChatVersion peerChatVRange subMode
updateGroupMemberStatusById db userId hostId GSMemAccepted
updateGroupMemberStatus db userId membership GSMemAccepted
joinAgentConnectionAsync cmdId False acId True connRequest dm subMode
toView $ CEvtUserAcceptedGroupSent user gInfo {membership = membership {memberStatus = GSMemAccepted}} hostContact_
createInvitationItem invStatus = do
let content = CIRcvGroupInvitation (CIGroupInvitation {groupId, groupMemberId, localDisplayName, groupProfile, status = invStatus}) memRole
(ci, cInfo) <- saveRcvChatItemNoParse user (CDDirectRcv ct) msg brokerTs content
@@ -2731,11 +2719,11 @@ processAgentMessageConn cxt user@User {userId} entity gks_ corrId agentConnId ag
toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDRcv cInfo ci]
if
| sameGroupLinkId groupLinkId groupLinkId' ->
joinGroupAsync (Just ct) True
joinGroupAsync g hostId conn connRequest (Just ct) True
| isTrue (autoAcceptGroupInvitations user) ->
-- a resent invitation returns the existing group, so only join while still invited
when (memberStatus membership == GSMemInvited) $ do
joinGroupAsync Nothing False
joinGroupAsync g hostId conn connRequest Nothing False
createInvitationItem CIGISAccepted
| otherwise -> do
createInvitationItem CIGISPending
@@ -2747,6 +2735,21 @@ processAgentMessageConn cxt user@User {userId} entity gks_ corrId agentConnId ag
sameGroupLinkId (Just gli) (Just gli') = gli == gli'
sameGroupLinkId _ _ = False
-- hostContact is only reported for group links, where the client replaces
-- the transient host connection view with the group and removes its chat
joinGroupAsync :: GroupInfoKeys -> GroupMemberId -> Connection -> ConnReqInvitation -> Maybe Contact -> Bool -> CM ()
joinGroupAsync (GIK gInfo@GroupInfo {groupId, membership} gks) hostId Connection {connId, connChatVersion, peerChatVRange} connRequest hostContact_ sameLink = do
subMode <- chatReadVar subscriptionMode
dm <- encodeXGrpAcpt user (GIK gInfo gks) peerChatVRange
(connIds@(cmdId, acId), _) <- prepareAgentJoin user True connRequest
withStore' $ \db -> do
when sameLink $ setViaGroupLinkUri db groupId connId
createMemberConnectionAsync db user hostId connIds connChatVersion peerChatVRange subMode
updateGroupMemberStatusById db userId hostId GSMemAccepted
updateGroupMemberStatus db userId membership GSMemAccepted
joinAgentConnectionAsync cmdId False acId True connRequest dm subMode
toView $ CEvtUserAcceptedGroupSent user gInfo {membership = membership {memberStatus = GSMemAccepted}} hostContact_
checkIntegrityCreateItem :: forall c. ChatTypeI c => ChatDirection c 'MDRcv -> MsgMeta -> CM ()
checkIntegrityCreateItem cd MsgMeta {integrity, broker = (_, brokerTs)} = case integrity of
MsgOk -> pure ()
@@ -3882,6 +3885,35 @@ processAgentMessageConn cxt user@User {userId} entity gks_ corrId agentConnId ag
unless (isUserGrpFwdRelay gInfo) $ connectToRelayAsync user gInfo rl
pure $ Just DJSGroup {jobSpec = DJDeliveryJob {includePending = False}}
xGrpInv :: GroupInfo -> GroupMember -> Connection -> GroupInvitation -> RcvMessage -> UTCTime -> CM ()
xGrpInv viaGInfo m mConn@Connection {peerChatVRange} inv msg brokerTs
| useRelays' viaGInfo = messageError "x.grp.inv: not allowed in channels"
| isJust publicGroup = messageError "x.grp.inv: can't invite to channel"
| not (groupFeatureMemberAllowed SGFDirectMessages m viaGInfo) = messageError "x.grp.inv: direct messages not allowed"
| memberBlocked m = messageWarning "x.grp.inv: member is blocked (ignoring)"
| fromRole < GRAdmin || fromRole < memRole = messageError "x.grp.inv: inviting member role"
| fromMemId == memId = messageError "x.grp.inv: duplicate member ID"
| otherwise = do
memberKeys <- atomically . C.generateKeyPair =<< asks random
let incognitoProfileId = localProfileId <$> incognitoMembershipProfile viaGInfo
(g@(GIK gInfo@GroupInfo {groupId, localDisplayName, groupProfile, membership} _), hostId) <- withStore $ \db -> createGroupInvitation db cxt user (Right m) peerChatVRange inv incognitoProfileId memberKeys
void $ createChatItem user (CDGroupSnd gInfo Nothing) False CIChatBanner Nothing Nothing (Just epochStart)
let GroupMember {groupMemberId} = membership
createInvitationItem invStatus = do
let content = CIRcvGroupInvitation (CIGroupInvitation {groupId, groupMemberId, localDisplayName, groupProfile, status = invStatus}) memRole
(ci, cInfo) <- saveRcvChatItemNoParse user (CDGroupRcv viaGInfo Nothing m) msg brokerTs content
withStore' $ \db -> setGroupInvitationChatItemId db user groupId (chatItemId' ci)
groupMsgToView cInfo ci
if isTrue (autoAcceptGroupInvitations user)
then when (memberStatus membership == GSMemInvited) $ do
joinGroupAsync g hostId mConn connRequest Nothing False
createInvitationItem CIGISAccepted
else do
createInvitationItem CIGISPending
toView CEvtReceivedGroupInvitationViaGroup {user, groupInfo = gInfo, viaGroupInfo = viaGInfo, viaMember = m, fromMemberRole = fromRole, memberRole = memRole}
where
GroupInvitation {fromMember = MemberIdRole fromMemId fromRole, invitedMember = MemberIdRole memId memRole, connRequest, groupProfile = GroupProfile {publicGroup}} = inv
xGrpDirectInv :: GroupInfo -> GroupMember -> Connection -> ConnReqInvitation -> Maybe MsgContent -> RcvMessage -> UTCTime -> CM ()
xGrpDirectInv g@GroupInfo {groupId, groupProfile = gp} m mConn@Connection {connId = mConnId} connReq mContent_ msg brokerTs
| not (groupFeatureMemberAllowed SGFDirectMessages m g) = messageError "x.grp.direct.inv: direct messages not allowed"
+4 -2
View File
@@ -87,6 +87,8 @@ import Simplex.Messaging.Version hiding (version)
-- 18 - relay web capabilities (2026-05-31)
-- 19 - group roster (2026-06-18)
-- 20 - p2p group member keys for signing (2026-07-26)
-- 21 - command keywords with any characters (2026-09-27)
-- 22 - owner-signed relay invitations, group invitations via member connections (2026-10-10)
-- This should not be used directly in code, instead use `maxVersion chatVRange` from ChatConfig.
-- This indirection is needed for backward/forward compatibility testing.
@@ -143,8 +145,8 @@ groupMemberKeyVersion = VersionChat 20
anyTextCommandsVersion :: VersionChat
anyTextCommandsVersion = VersionChat 21
signedRelayInvVersion :: VersionChat
signedRelayInvVersion = VersionChat 22
groupInvVersion :: VersionChat
groupInvVersion = VersionChat 22
data ConnectionEntity
= RcvDirectMsgConnection {entityConnection :: Connection, contact :: Maybe Contact}
+65 -9
View File
@@ -86,6 +86,7 @@ module Simplex.Chat.Store.Groups
getContactGroupPreferences,
getGroupInvitation,
createNewContactMember,
createNewMemberViaGroup,
createGroupRelayRecord,
getGroupRelayById,
getGroupRelayByGMId,
@@ -140,6 +141,7 @@ module Simplex.Chat.Store.Groups
createBusinessRequestGroup,
getContactViaMember,
getMemberInvitation,
getMemberInvitedVia,
createMemberConnection,
createMemberConnectionAsync,
updatePreparedRelayedGroup,
@@ -455,9 +457,8 @@ createNewGroup db cxt user@User {userId} groupProfile incognitoProfile memberId
}
-- | 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 -> C.KeyPairEd25519 -> ExceptT StoreError IO (GroupInfoKeys, 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
createGroupInvitation :: DB.Connection -> StoreCxt -> User -> Either Contact GroupMember -> VersionRangeChat -> GroupInvitation -> Maybe ProfileId -> C.KeyPairEd25519 -> ExceptT StoreError IO (GroupInfoKeys, GroupMemberId)
createGroupInvitation db cxt user@User {userId} inviter peerChatVRange GroupInvitation {fromMember, fromMemberKey, invitedMember, connRequest, groupProfile, business} incognitoProfileId memberKeys = do
liftIO getInvitationGroupId_ >>= \case
Nothing -> createGroupInvitation_
Just gId -> do
@@ -501,8 +502,28 @@ createGroupInvitation db cxt user@User {userId} contact@Contact {contactId, acti
((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 ((\(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)
(groupMemberId, invitedBy) <- case inviter of
Left contact@Contact {contactId} -> do
GroupMember {groupMemberId} <- createContactMemberInv_ db user groupId Nothing contact fromMember GCHostMember GSMemInvited IBUnknown Nothing ((\(MemberKey k) -> k) <$> fromMemberKey) currentTs hostVRange
pure (groupMemberId, IBContact contactId)
Right GroupMember {memberProfile} -> do
(hostName, memProfileId, hostProfile, badgeVerified) <- createNewMemberProfile_ db cxt user (fromLocalProfile memberProfile) Nothing currentTs
let MemberIdRole {memberId, memberRole} = fromMember
host =
NewGroupMember
{ memInfo = MemberInfo memberId memberRole (Just $ ChatVersionRange hostVRange) hostProfile fromMemberKey,
memCategory = GCHostMember,
memStatus = GSMemInvited,
memRestriction = Nothing,
memInvitedBy = IBUnknown,
memInvitedByGroupMemberId = Nothing,
localDisplayName = hostName,
memContactId = Nothing,
memProfileId
}
GroupMember {groupMemberId} <- createNewMember_ db user groupId host badgeVerified currentTs
pure (groupMemberId, IBUnknown)
membership <- createContactMemberInv_ db user groupId (Just groupMemberId) user invitedMember GCUserMember GSMemInvited invitedBy incognitoProfileId (Just $ fst memberKeys) currentTs (vr cxt)
let chatSettings = ChatSettings {enableNtfs = MFAll, sendRcpts = Nothing, favorite = False}
pure
( GroupInfo
@@ -1436,6 +1457,32 @@ createNewContactMember db gVar User {userId, userContactId} GroupInfo {groupId,
)
pure indexInGroup
createNewMemberViaGroup :: DB.Connection -> TVar ChaChaDRG -> StoreCxt -> User -> GroupInfo -> GroupMember -> Connection -> GroupMemberRole -> ConnId -> ConnReqInvitation -> SubscriptionMode -> ExceptT StoreError IO GroupMember
createNewMemberViaGroup db gVar cxt user@User {userId} GroupInfo {groupId, membership} viaMember@GroupMember {memberProfile} Connection {connChatVersion, peerChatVRange} memberRole agentConnId connRequest subMode =
createWithRandomId' db gVar $ \memId -> runExceptT $ do
createdAt <- liftIO getCurrentTime
(localDisplayName, memProfileId, profile, badgeVerified) <- createNewMemberProfile_ db cxt user (fromLocalProfile memberProfile) Nothing createdAt
let newMember =
NewGroupMember
{ memInfo = MemberInfo (MemberId memId) memberRole (Just $ ChatVersionRange peerChatVRange) profile Nothing,
memCategory = GCInviteeMember,
memStatus = GSMemInvited,
memRestriction = Nothing,
memInvitedBy = IBUser,
memInvitedByGroupMemberId = Just $ groupMemberId' membership,
localDisplayName,
memContactId = Nothing,
memProfileId
}
member@GroupMember {groupMemberId} <- createNewMember_ db user groupId newMember badgeVerified createdAt
liftIO $ do
DB.execute
db
"UPDATE group_members SET sent_inv_queue_info = ?, invited_via_group_member_id = ? WHERE group_member_id = ?"
(connRequest, groupMemberId' viaMember, groupMemberId)
void $ createMemberConnection_ db userId groupMemberId agentConnId connChatVersion peerChatVRange Nothing 0 createdAt subMode
pure member
createGroupRelayRecord :: DB.Connection -> GroupInfo -> GroupMember -> UserChatRelay -> ExceptT StoreError IO GroupRelay
createGroupRelayRecord db GroupInfo {groupId} GroupMember {groupMemberId} UserChatRelay {chatRelayId} = do
currentTs <- liftIO getCurrentTime
@@ -2239,6 +2286,15 @@ getMemberInvitation db User {userId} groupMemberId =
fmap join . maybeFirstRow fromOnly $
DB.query db "SELECT sent_inv_queue_info FROM group_members WHERE group_member_id = ? AND user_id = ?" (groupMemberId, userId)
getMemberInvitedVia :: DB.Connection -> StoreCxt -> User -> GroupInfo -> GroupMember -> IO (Maybe GroupMember)
getMemberInvitedVia db cxt user@User {userId} GroupInfo {groupId} viaMember = do
currentTs <- getCurrentTime
maybeFirstRow (toContactMember currentTs cxt user) $
DB.query
db
(groupMemberQuery <> " WHERE m.user_id = ? AND m.group_id = ? AND m.invited_via_group_member_id = ? AND m.member_status NOT IN (?,?,?)")
(userId, groupId, groupMemberId' viaMember, GSMemRejected, GSMemRemoved, GSMemLeft)
createMemberConnection :: DB.Connection -> UserId -> GroupMember -> ConnId -> VersionChat -> VersionRangeChat -> SubscriptionMode -> IO Connection
createMemberConnection db userId GroupMember {groupMemberId} agentConnId chatV peerChatVRange subMode = do
currentTs <- getCurrentTime
@@ -2441,7 +2497,7 @@ createNewGroupMember db cxt user gInfo invitingMember memInfo@MemberInfo {profil
memContactId = Nothing,
memProfileId
}
createNewMember_ db user gInfo newMember badgeVerified currentTs
createNewMember_ db user (groupId' gInfo) newMember badgeVerified currentTs
createNewMemberProfile_ :: DB.Connection -> StoreCxt -> User -> Profile -> Maybe ProofPresHeader -> UTCTime -> ExceptT StoreError IO (Text, ProfileId, Profile, Maybe Bool)
createNewMemberProfile_ db cxt User {userId} p@Profile {displayName, fullName, shortDescr, description, image, contactLink, preferences} presHeader_ createdAt =
@@ -2454,11 +2510,11 @@ createNewMemberProfile_ db cxt User {userId} p@Profile {displayName, fullName, s
profileId <- insertedRowId db
pure $ Right (ldn, profileId, p', badgeVerified)
createNewMember_ :: DB.Connection -> User -> GroupInfo -> NewGroupMember -> Maybe Bool -> UTCTime -> ExceptT StoreError IO GroupMember
createNewMember_ :: DB.Connection -> User -> GroupId -> NewGroupMember -> Maybe Bool -> UTCTime -> ExceptT StoreError IO GroupMember
createNewMember_
db
User {userId, userContactId}
GroupInfo {groupId}
groupId
NewGroupMember
{ memInfo = MemberInfo memberId memberRole memChatVRange memberProfile@Profile {badge} memKey,
memCategory = memberCategory,
@@ -2639,7 +2695,7 @@ createIntroReMember
(localDisplayName, memProfileId, memberProfile', badgeVerified) <- createNewMemberProfile_ db cxt user memberProfile presHeader_ currentTs
let memRestriction = restriction <$> memRestrictions_
newMember = NewGroupMember {memInfo = (memInfo :: MemberInfo) {profile = memberProfile'}, memCategory = GCPreMember, memStatus = GSMemIntroduced, memRestriction, memInvitedBy = IBUnknown, memInvitedByGroupMemberId = Nothing, localDisplayName, memContactId = Nothing, memProfileId}
createNewMember_ db user gInfo newMember badgeVerified currentTs
createNewMember_ db user (groupId' gInfo) newMember badgeVerified currentTs
createIntroReMemberConn :: DB.Connection -> User -> GroupMember -> GroupMember -> VersionChat -> MemberInfo -> (CommandId, ConnId) -> SubscriptionMode -> ExceptT StoreError IO GroupMember
createIntroReMemberConn
@@ -54,6 +54,7 @@ import Simplex.Chat.Store.Postgres.Migrations.M20260915_user_badges
import Simplex.Chat.Store.Postgres.Migrations.M20260918_badge_issue_errors
import Simplex.Chat.Store.Postgres.Migrations.M20260923_preferences_json
import Simplex.Chat.Store.Postgres.Migrations.M20260925_badge_bindings
import Simplex.Chat.Store.Postgres.Migrations.M20261010_invite_member_via_group
import Simplex.Messaging.Agent.Store.Shared (Migration (..))
schemaMigrations :: [(String, Text, Maybe Text)]
@@ -107,7 +108,8 @@ schemaMigrations =
("20260915_user_badges", m20260915_user_badges, Just down_m20260915_user_badges),
("20260918_badge_issue_errors", m20260918_badge_issue_errors, Just down_m20260918_badge_issue_errors),
("20260923_preferences_json", m20260923_preferences_json, Just down_m20260923_preferences_json),
("20260925_badge_bindings", m20260925_badge_bindings, Just down_m20260925_badge_bindings)
("20260925_badge_bindings", m20260925_badge_bindings, Just down_m20260925_badge_bindings),
("20261010_invite_member_via_group", m20261010_invite_member_via_group, Just down_m20261010_invite_member_via_group)
]
-- | The list of migrations in ascending order by date
@@ -0,0 +1,21 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Chat.Store.Postgres.Migrations.M20261010_invite_member_via_group where
import Data.Text (Text)
import Text.RawString.QQ (r)
m20261010_invite_member_via_group :: Text
m20261010_invite_member_via_group =
[r|
ALTER TABLE group_members ADD COLUMN invited_via_group_member_id BIGINT REFERENCES group_members(group_member_id) ON DELETE SET NULL;
CREATE INDEX idx_group_members_invited_via_group_member_id ON group_members(invited_via_group_member_id);
|]
down_m20261010_invite_member_via_group :: Text
down_m20261010_invite_member_via_group =
[r|
DROP INDEX idx_group_members_invited_via_group_member_id;
ALTER TABLE group_members DROP COLUMN invited_via_group_member_id;
|]
@@ -1022,7 +1022,8 @@ CREATE TABLE test_chat_schema.group_members (
removed_at timestamp with time zone,
roster_served_version bigint,
member_security_code text,
member_security_code_verified_at timestamp with time zone
member_security_code_verified_at timestamp with time zone,
invited_via_group_member_id bigint
);
@@ -2709,6 +2710,10 @@ CREATE INDEX idx_group_members_invited_by_group_member_id ON test_chat_schema.gr
CREATE INDEX idx_group_members_invited_via_group_member_id ON test_chat_schema.group_members USING btree (invited_via_group_member_id);
CREATE INDEX idx_group_members_member_profile_id ON test_chat_schema.group_members USING btree (member_profile_id);
@@ -3465,6 +3470,11 @@ ALTER TABLE ONLY test_chat_schema.group_members
ALTER TABLE ONLY test_chat_schema.group_members
ADD CONSTRAINT group_members_invited_via_group_member_id_fkey FOREIGN KEY (invited_via_group_member_id) REFERENCES test_chat_schema.group_members(group_member_id) ON DELETE SET NULL;
ALTER TABLE ONLY test_chat_schema.group_members
ADD CONSTRAINT group_members_member_profile_id_fkey FOREIGN KEY (member_profile_id) REFERENCES test_chat_schema.contact_profiles(contact_profile_id) ON DELETE SET NULL;
+3 -1
View File
@@ -177,6 +177,7 @@ import Simplex.Chat.Store.SQLite.Migrations.M20260915_user_badges
import Simplex.Chat.Store.SQLite.Migrations.M20260918_badge_issue_errors
import Simplex.Chat.Store.SQLite.Migrations.M20260923_preferences_json
import Simplex.Chat.Store.SQLite.Migrations.M20260925_badge_bindings
import Simplex.Chat.Store.SQLite.Migrations.M20261010_invite_member_via_group
import Simplex.Messaging.Agent.Store.Shared (Migration (..))
schemaMigrations :: [(String, Query, Maybe Query)]
@@ -353,7 +354,8 @@ schemaMigrations =
("20260915_user_badges", m20260915_user_badges, Just down_m20260915_user_badges),
("20260918_badge_issue_errors", m20260918_badge_issue_errors, Just down_m20260918_badge_issue_errors),
("20260923_preferences_json", m20260923_preferences_json, Just down_m20260923_preferences_json),
("20260925_badge_bindings", m20260925_badge_bindings, Just down_m20260925_badge_bindings)
("20260925_badge_bindings", m20260925_badge_bindings, Just down_m20260925_badge_bindings),
("20261010_invite_member_via_group", m20261010_invite_member_via_group, Just down_m20261010_invite_member_via_group)
]
-- | The list of migrations in ascending order by date
@@ -0,0 +1,20 @@
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Chat.Store.SQLite.Migrations.M20261010_invite_member_via_group where
import Database.SQLite.Simple (Query)
import Database.SQLite.Simple.QQ (sql)
m20261010_invite_member_via_group :: Query
m20261010_invite_member_via_group =
[sql|
ALTER TABLE group_members ADD COLUMN invited_via_group_member_id INTEGER REFERENCES group_members(group_member_id) ON DELETE SET NULL;
CREATE INDEX idx_group_members_invited_via_group_member_id ON group_members(invited_via_group_member_id);
|]
down_m20261010_invite_member_via_group :: Query
down_m20261010_invite_member_via_group =
[sql|
DROP INDEX idx_group_members_invited_via_group_member_id;
ALTER TABLE group_members DROP COLUMN invited_via_group_member_id;
|]
@@ -55,6 +55,7 @@ SEARCH rcv_files USING COVERING INDEX idx_rcv_files_group_member_id (group_membe
SEARCH snd_files USING COVERING INDEX idx_snd_files_group_member_id (group_member_id=?)
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_to_group_member_id (to_group_member_id=?)
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_re_group_member_id (re_group_member_id=?)
SEARCH group_members USING COVERING INDEX idx_group_members_invited_via_group_member_id (invited_via_group_member_id=?)
SEARCH group_members USING COVERING INDEX idx_group_members_invited_by_group_member_id (invited_by_group_member_id=?)
SEARCH contacts USING COVERING INDEX idx_contacts_grp_direct_inv_from_group_member_id (grp_direct_inv_from_group_member_id=?)
SEARCH contacts USING COVERING INDEX idx_contacts_contact_group_member_id (contact_group_member_id=?)
@@ -110,6 +111,7 @@ SEARCH rcv_files USING COVERING INDEX idx_rcv_files_group_member_id (group_membe
SEARCH snd_files USING COVERING INDEX idx_snd_files_group_member_id (group_member_id=?)
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_to_group_member_id (to_group_member_id=?)
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_re_group_member_id (re_group_member_id=?)
SEARCH group_members USING COVERING INDEX idx_group_members_invited_via_group_member_id (invited_via_group_member_id=?)
SEARCH group_members USING COVERING INDEX idx_group_members_invited_by_group_member_id (invited_by_group_member_id=?)
SEARCH contacts USING COVERING INDEX idx_contacts_grp_direct_inv_from_group_member_id (grp_direct_inv_from_group_member_id=?)
SEARCH contacts USING COVERING INDEX idx_contacts_contact_group_member_id (contact_group_member_id=?)
@@ -322,6 +324,7 @@ SEARCH rcv_files USING COVERING INDEX idx_rcv_files_group_member_id (group_membe
SEARCH snd_files USING COVERING INDEX idx_snd_files_group_member_id (group_member_id=?)
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_to_group_member_id (to_group_member_id=?)
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_re_group_member_id (re_group_member_id=?)
SEARCH group_members USING COVERING INDEX idx_group_members_invited_via_group_member_id (invited_via_group_member_id=?)
SEARCH group_members USING COVERING INDEX idx_group_members_invited_by_group_member_id (invited_by_group_member_id=?)
SEARCH contacts USING COVERING INDEX idx_contacts_grp_direct_inv_from_group_member_id (grp_direct_inv_from_group_member_id=?)
SEARCH contacts USING COVERING INDEX idx_contacts_contact_group_member_id (contact_group_member_id=?)
@@ -359,6 +362,7 @@ SEARCH rcv_files USING COVERING INDEX idx_rcv_files_group_member_id (group_membe
SEARCH snd_files USING COVERING INDEX idx_snd_files_group_member_id (group_member_id=?)
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_to_group_member_id (to_group_member_id=?)
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_re_group_member_id (re_group_member_id=?)
SEARCH group_members USING COVERING INDEX idx_group_members_invited_via_group_member_id (invited_via_group_member_id=?)
SEARCH group_members USING COVERING INDEX idx_group_members_invited_by_group_member_id (invited_by_group_member_id=?)
SEARCH contacts USING COVERING INDEX idx_contacts_grp_direct_inv_from_group_member_id (grp_direct_inv_from_group_member_id=?)
SEARCH contacts USING COVERING INDEX idx_contacts_contact_group_member_id (contact_group_member_id=?)
@@ -396,6 +400,7 @@ SEARCH rcv_files USING COVERING INDEX idx_rcv_files_group_member_id (group_membe
SEARCH snd_files USING COVERING INDEX idx_snd_files_group_member_id (group_member_id=?)
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_to_group_member_id (to_group_member_id=?)
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_re_group_member_id (re_group_member_id=?)
SEARCH group_members USING COVERING INDEX idx_group_members_invited_via_group_member_id (invited_via_group_member_id=?)
SEARCH group_members USING COVERING INDEX idx_group_members_invited_by_group_member_id (invited_by_group_member_id=?)
SEARCH contacts USING COVERING INDEX idx_contacts_grp_direct_inv_from_group_member_id (grp_direct_inv_from_group_member_id=?)
SEARCH contacts USING COVERING INDEX idx_contacts_contact_group_member_id (contact_group_member_id=?)
@@ -621,6 +626,7 @@ SEARCH rcv_files USING COVERING INDEX idx_rcv_files_group_member_id (group_membe
SEARCH snd_files USING COVERING INDEX idx_snd_files_group_member_id (group_member_id=?)
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_to_group_member_id (to_group_member_id=?)
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_re_group_member_id (re_group_member_id=?)
SEARCH group_members USING COVERING INDEX idx_group_members_invited_via_group_member_id (invited_via_group_member_id=?)
SEARCH group_members USING COVERING INDEX idx_group_members_invited_by_group_member_id (invited_by_group_member_id=?)
SEARCH contacts USING COVERING INDEX idx_contacts_grp_direct_inv_from_group_member_id (grp_direct_inv_from_group_member_id=?)
SEARCH contacts USING COVERING INDEX idx_contacts_contact_group_member_id (contact_group_member_id=?)
@@ -657,6 +663,7 @@ SEARCH rcv_files USING COVERING INDEX idx_rcv_files_group_member_id (group_membe
SEARCH snd_files USING COVERING INDEX idx_snd_files_group_member_id (group_member_id=?)
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_to_group_member_id (to_group_member_id=?)
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_re_group_member_id (re_group_member_id=?)
SEARCH group_members USING COVERING INDEX idx_group_members_invited_via_group_member_id (invited_via_group_member_id=?)
SEARCH group_members USING COVERING INDEX idx_group_members_invited_by_group_member_id (invited_by_group_member_id=?)
SEARCH contacts USING COVERING INDEX idx_contacts_grp_direct_inv_from_group_member_id (grp_direct_inv_from_group_member_id=?)
SEARCH contacts USING COVERING INDEX idx_contacts_contact_group_member_id (contact_group_member_id=?)
@@ -694,6 +701,7 @@ SEARCH rcv_files USING COVERING INDEX idx_rcv_files_group_member_id (group_membe
SEARCH snd_files USING COVERING INDEX idx_snd_files_group_member_id (group_member_id=?)
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_to_group_member_id (to_group_member_id=?)
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_re_group_member_id (re_group_member_id=?)
SEARCH group_members USING COVERING INDEX idx_group_members_invited_via_group_member_id (invited_via_group_member_id=?)
SEARCH group_members USING COVERING INDEX idx_group_members_invited_by_group_member_id (invited_by_group_member_id=?)
SEARCH contacts USING COVERING INDEX idx_contacts_grp_direct_inv_from_group_member_id (grp_direct_inv_from_group_member_id=?)
SEARCH contacts USING COVERING INDEX idx_contacts_contact_group_member_id (contact_group_member_id=?)
@@ -731,6 +739,7 @@ SEARCH rcv_files USING COVERING INDEX idx_rcv_files_group_member_id (group_membe
SEARCH snd_files USING COVERING INDEX idx_snd_files_group_member_id (group_member_id=?)
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_to_group_member_id (to_group_member_id=?)
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_re_group_member_id (re_group_member_id=?)
SEARCH group_members USING COVERING INDEX idx_group_members_invited_via_group_member_id (invited_via_group_member_id=?)
SEARCH group_members USING COVERING INDEX idx_group_members_invited_by_group_member_id (invited_by_group_member_id=?)
SEARCH contacts USING COVERING INDEX idx_contacts_grp_direct_inv_from_group_member_id (grp_direct_inv_from_group_member_id=?)
SEARCH contacts USING COVERING INDEX idx_contacts_contact_group_member_id (contact_group_member_id=?)
@@ -1304,6 +1313,7 @@ SEARCH rcv_files USING COVERING INDEX idx_rcv_files_group_member_id (group_membe
SEARCH snd_files USING COVERING INDEX idx_snd_files_group_member_id (group_member_id=?)
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_to_group_member_id (to_group_member_id=?)
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_re_group_member_id (re_group_member_id=?)
SEARCH group_members USING COVERING INDEX idx_group_members_invited_via_group_member_id (invited_via_group_member_id=?)
SEARCH group_members USING COVERING INDEX idx_group_members_invited_by_group_member_id (invited_by_group_member_id=?)
SEARCH contacts USING COVERING INDEX idx_contacts_grp_direct_inv_from_group_member_id (grp_direct_inv_from_group_member_id=?)
SEARCH contacts USING COVERING INDEX idx_contacts_contact_group_member_id (contact_group_member_id=?)
@@ -1342,6 +1352,7 @@ SEARCH rcv_files USING COVERING INDEX idx_rcv_files_group_member_id (group_membe
SEARCH snd_files USING COVERING INDEX idx_snd_files_group_member_id (group_member_id=?)
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_to_group_member_id (to_group_member_id=?)
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_re_group_member_id (re_group_member_id=?)
SEARCH group_members USING COVERING INDEX idx_group_members_invited_via_group_member_id (invited_via_group_member_id=?)
SEARCH group_members USING COVERING INDEX idx_group_members_invited_by_group_member_id (invited_by_group_member_id=?)
SEARCH contacts USING COVERING INDEX idx_contacts_grp_direct_inv_from_group_member_id (grp_direct_inv_from_group_member_id=?)
SEARCH contacts USING COVERING INDEX idx_contacts_contact_group_member_id (contact_group_member_id=?)
@@ -1997,6 +2008,7 @@ SEARCH rcv_files USING COVERING INDEX idx_rcv_files_group_member_id (group_membe
SEARCH snd_files USING COVERING INDEX idx_snd_files_group_member_id (group_member_id=?)
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_to_group_member_id (to_group_member_id=?)
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_re_group_member_id (re_group_member_id=?)
SEARCH group_members USING COVERING INDEX idx_group_members_invited_via_group_member_id (invited_via_group_member_id=?)
SEARCH group_members USING COVERING INDEX idx_group_members_invited_by_group_member_id (invited_by_group_member_id=?)
SEARCH contacts USING COVERING INDEX idx_contacts_grp_direct_inv_from_group_member_id (grp_direct_inv_from_group_member_id=?)
SEARCH contacts USING COVERING INDEX idx_contacts_contact_group_member_id (contact_group_member_id=?)
@@ -2034,6 +2046,7 @@ SEARCH rcv_files USING COVERING INDEX idx_rcv_files_group_member_id (group_membe
SEARCH snd_files USING COVERING INDEX idx_snd_files_group_member_id (group_member_id=?)
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_to_group_member_id (to_group_member_id=?)
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_re_group_member_id (re_group_member_id=?)
SEARCH group_members USING COVERING INDEX idx_group_members_invited_via_group_member_id (invited_via_group_member_id=?)
SEARCH group_members USING COVERING INDEX idx_group_members_invited_by_group_member_id (invited_by_group_member_id=?)
SEARCH contacts USING COVERING INDEX idx_contacts_grp_direct_inv_from_group_member_id (grp_direct_inv_from_group_member_id=?)
SEARCH contacts USING COVERING INDEX idx_contacts_contact_group_member_id (contact_group_member_id=?)
@@ -6235,6 +6248,29 @@ SEARCH p USING INTEGER PRIMARY KEY (rowid=?)
SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JOIN
SEARCH bp USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN
Query:
SELECT
m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction,
m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, p.preferences_json,
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified,
m.created_at, m.updated_at,
m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, m.member_security_code, m.member_security_code_verified_at,
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id,
c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.user_contact_link_id,
c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter,
c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version,
bp.badge_proof, bp.badge_pres_header, bp.badge_key_idx, bp.badge_type, bp.badge_expiry, bp.badge_extra
FROM group_members m
JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id)
LEFT JOIN connections c ON c.group_member_id = m.group_member_id
LEFT JOIN group_member_badge_proofs bp ON bp.group_member_id = m.group_member_id
WHERE m.user_id = ? AND m.group_id = ? AND m.invited_via_group_member_id = ? AND m.member_status NOT IN (?,?,?)
Plan:
SEARCH m USING INDEX idx_group_members_invited_via_group_member_id (invited_via_group_member_id=?)
SEARCH p USING INTEGER PRIMARY KEY (rowid=?)
SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JOIN
SEARCH bp USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN
Query:
SELECT
m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction,
@@ -7006,6 +7042,7 @@ SEARCH rcv_files USING COVERING INDEX idx_rcv_files_group_member_id (group_membe
SEARCH snd_files USING COVERING INDEX idx_snd_files_group_member_id (group_member_id=?)
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_to_group_member_id (to_group_member_id=?)
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_re_group_member_id (re_group_member_id=?)
SEARCH group_members USING COVERING INDEX idx_group_members_invited_via_group_member_id (invited_via_group_member_id=?)
SEARCH group_members USING COVERING INDEX idx_group_members_invited_by_group_member_id (invited_by_group_member_id=?)
SEARCH contacts USING COVERING INDEX idx_contacts_grp_direct_inv_from_group_member_id (grp_direct_inv_from_group_member_id=?)
SEARCH contacts USING COVERING INDEX idx_contacts_contact_group_member_id (contact_group_member_id=?)
@@ -7038,6 +7075,7 @@ SEARCH rcv_files USING COVERING INDEX idx_rcv_files_group_member_id (group_membe
SEARCH snd_files USING COVERING INDEX idx_snd_files_group_member_id (group_member_id=?)
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_to_group_member_id (to_group_member_id=?)
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_re_group_member_id (re_group_member_id=?)
SEARCH group_members USING COVERING INDEX idx_group_members_invited_via_group_member_id (invited_via_group_member_id=?)
SEARCH group_members USING COVERING INDEX idx_group_members_invited_by_group_member_id (invited_by_group_member_id=?)
SEARCH contacts USING COVERING INDEX idx_contacts_grp_direct_inv_from_group_member_id (grp_direct_inv_from_group_member_id=?)
SEARCH contacts USING COVERING INDEX idx_contacts_contact_group_member_id (contact_group_member_id=?)
@@ -7070,6 +7108,7 @@ SEARCH rcv_files USING COVERING INDEX idx_rcv_files_group_member_id (group_membe
SEARCH snd_files USING COVERING INDEX idx_snd_files_group_member_id (group_member_id=?)
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_to_group_member_id (to_group_member_id=?)
SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_re_group_member_id (re_group_member_id=?)
SEARCH group_members USING COVERING INDEX idx_group_members_invited_via_group_member_id (invited_via_group_member_id=?)
SEARCH group_members USING COVERING INDEX idx_group_members_invited_by_group_member_id (invited_by_group_member_id=?)
SEARCH contacts USING COVERING INDEX idx_contacts_grp_direct_inv_from_group_member_id (grp_direct_inv_from_group_member_id=?)
SEARCH contacts USING COVERING INDEX idx_contacts_contact_group_member_id (contact_group_member_id=?)
@@ -7603,7 +7642,7 @@ SCAN files
Query: SELECT count(1) FROM group_members
Plan:
SCAN group_members USING COVERING INDEX idx_group_members_invited_by_group_member_id
SCAN group_members USING COVERING INDEX idx_group_members_invited_via_group_member_id
Query: SELECT count(1) FROM pending_group_messages
Plan:
@@ -7703,6 +7742,10 @@ Query: SELECT issue_failed_since, issue_error FROM badge_purchases
Plan:
SCAN badge_purchases
Query: SELECT item_content FROM chat_items WHERE group_id = 1 ORDER BY chat_item_id DESC LIMIT 1
Plan:
SEARCH chat_items USING INDEX idx_chat_items_group_id (group_id=?)
Query: SELECT last_insert_rowid()
Plan:
SCAN CONSTANT ROW
@@ -8183,6 +8226,10 @@ Query: UPDATE group_members SET roster_served_version = ?, updated_at = ? WHERE
Plan:
SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?)
Query: UPDATE group_members SET sent_inv_queue_info = ?, invited_via_group_member_id = ? WHERE group_member_id = ?
Plan:
SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?)
Query: UPDATE group_members SET support_chat_items_member_attention = ?, updated_at = ? WHERE group_member_id = ?
Plan:
SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?)
@@ -260,6 +260,7 @@ CREATE TABLE group_members(
roster_served_version INTEGER,
member_security_code TEXT,
member_security_code_verified_at TEXT,
invited_via_group_member_id INTEGER REFERENCES group_members(group_member_id) ON DELETE SET NULL,
FOREIGN KEY(user_id, local_display_name)
REFERENCES display_names(user_id, local_display_name)
ON DELETE CASCADE
@@ -1557,6 +1558,9 @@ CREATE INDEX idx_badge_code_redemptions_user ON badge_code_redemptions(
CREATE UNIQUE INDEX idx_badge_purchases_code_redemption ON badge_purchases(
badge_code_redemption_id
);
CREATE INDEX idx_group_members_invited_via_group_member_id ON group_members(
invited_via_group_member_id
);
CREATE TRIGGER on_group_members_insert_update_summary
AFTER INSERT ON group_members
FOR EACH ROW
+1
View File
@@ -82,6 +82,7 @@ runInputLoop ct@ChatTerminal {termState, liveMessageState} cc = forever $ do
CRContactDeleted u c -> whenCurrUser cc u $ unsetActiveContact ct c
CRGroupDeletedUser u g _ _ -> whenCurrUser cc u $ unsetActiveGroup ct g
CRSentGroupInvitation u g _ _ -> whenCurrUser cc u $ setActiveGroup ct g
CRSentGroupInvitationViaGroup u g _ _ _ -> whenCurrUser cc u $ setActiveGroup ct g
CRCmdOk _ -> case cmd of
Right APIDeleteUser {} -> setActive ct ""
_ -> pure ()
+3
View File
@@ -198,6 +198,9 @@ chatEventNotification t@ChatTerminal {sendNotification} cc = \case
CEvtReceivedGroupInvitation u g ct _ _ ->
when (contactNtf u ct False) $
sendNtf ("#" <> viewGroupName g <> " " <> viewContactName ct <> "> ", "invited you to join the group")
CEvtReceivedGroupInvitationViaGroup u g viaG m _ _ ->
when (groupNtf u viaG False) $
sendNtf ("#" <> viewGroupName g <> " " <> viewMemberName m <> "> ", "invited you to join the group")
CEvtUserJoinedGroup u g _ -> when (groupNtf u g False) $ do
whenCurrUser cc u $ setActiveGroup t g
sendNtf ("#" <> viewGroupName g, "you are connected to group")
+6 -6
View File
@@ -208,6 +208,7 @@ chatResponseToView hu cfg@ChatConfig {logLevel, showReactions, showFullLinks, te
-- CRGroupConversationsDeleted u _g _conversations -> ttyUser u []
CRGroupsList u gs -> ttyUser u $ viewGroupsList gs
CRSentGroupInvitation u g c _ -> ttyUser u $ viewSentGroupInvitation g c
CRSentGroupInvitationViaGroup u g _ viaG viaM -> ttyUser u ["invitation to join the group " <> ttyGroup' g <> " sent to " <> ttyMember viaM <> " in " <> ttyGroup' viaG]
CRFileTransferStatus u ftStatus -> ttyUser u $ viewFileTransferStatus ftStatus
CRFileTransferStatusXFTP u ci -> ttyUser u $ viewFileTransferStatusXFTP ci
CRUserProfile u@User {profile = LocalProfile {localBadge}} p -> ttyUser u $ viewUserProfile localBadge p
@@ -514,7 +515,8 @@ chatEventToView hu ChatConfig {logLevel, showReactions, showReceipts, testView}
in ttyUser u [sShow connId <> ": END"]
CEvtSubscriptionStatus srv status conns -> [plain $ subStatusStr status <> " " <> tshow (length conns) <> " connections on server " <> showSMPServer srv]
CEvtServiceSubStatus srv event -> [plain $ serviceSubEventStr srv event]
CEvtReceivedGroupInvitation {user = u, groupInfo = g, contact = c, memberRole = r} -> ttyUser u $ viewReceivedGroupInvitation g c r
CEvtReceivedGroupInvitation {user = u, groupInfo = g, contact = c, memberRole = r} -> ttyUser u $ viewReceivedGroupInvitation g (ttyContact' c) r
CEvtReceivedGroupInvitationViaGroup {user = u, groupInfo = g, viaGroupInfo = viaG, viaMember = m, memberRole = r} -> ttyUser u $ viewReceivedGroupInvitation g (ttyMember m <> " in " <> ttyGroup' viaG) r
CEvtUserJoinedGroup u g m -> ttyUser u $ viewUserJoinedGroup g m
CEvtGroupLinkDataUpdated u g groupLink relays relaysChanged
| relaysChanged -> ttyUser u $ viewGroupLinkRelaysUpdated g groupLink relays
@@ -732,7 +734,6 @@ viewChatItem chat ci@ChatItem {chatDir, meta = meta@CIMeta {itemForwarded, forwa
GroupChat g scopeInfo -> case chatDir of
CIGroupSnd -> case content of
CISndMsgContent mc -> hideLive meta $ withSndFile to $ sndMsg to context mc
CISndGroupInvitation {} -> showSndItemProhibited to
_ -> showSndItem to
where
to = ttyToGroup g scopeInfo
@@ -743,7 +744,6 @@ viewChatItem chat ci@ChatItem {chatDir, meta = meta@CIMeta {itemForwarded, forwa
CIRcvMsgContent mc -> withRcvFile from $ rcvMsg from context mc
CIRcvIntegrityError err -> viewRcvIntegrityError from err ts tz meta
CIRcvMsgError err -> viewRcvMsgError from err ts tz meta
CIRcvGroupInvitation {} | isJust m_ -> showRcvItemProhibited from
CIRcvModerated {} -> receivedWithTime_ ts tz (ttyFromGroup g scopeInfo m_) context meta [plainContent content] False
CIRcvBlocked {} -> receivedWithTime_ ts tz (ttyFromGroup g scopeInfo m_) context meta [plainContent content] False
_ -> showRcvItem from
@@ -1402,9 +1402,9 @@ viewConnectedToGroupMember g@GroupInfo {groupId} m@GroupMember {groupMemberId, m
]
_ -> [ttyGroup' g <> ": " <> connectedMember m <> " is connected"]
viewReceivedGroupInvitation :: GroupInfo -> Contact -> GroupMemberRole -> [StyledString]
viewReceivedGroupInvitation g c role =
ttyFullGroup g <> ": " <> ttyContact' c <> " invites you to join the group as " <> showRole role
viewReceivedGroupInvitation :: GroupInfo -> StyledString -> GroupMemberRole -> [StyledString]
viewReceivedGroupInvitation g inviter role =
ttyFullGroup g <> ": " <> inviter <> " invites you to join the group as " <> showRole role
: case incognitoMembershipProfile g of
Just mp -> ["use " <> highlight ("/j " <> viewGroupName g) <> " to join incognito as " <> incognitoProfile' (fromLocalProfile mp)]
Nothing -> ["use " <> highlight ("/j " <> viewGroupName g) <> " to accept"]