diff --git a/src/Simplex/Chat/Library/Internal.hs b/src/Simplex/Chat/Library/Internal.hs index 6e119ee6c4..ff79cfa01e 100644 --- a/src/Simplex/Chat/Library/Internal.hs +++ b/src/Simplex/Chat/Library/Internal.hs @@ -1180,18 +1180,21 @@ forwardCachedRoster user gInfo subscriber = do introduceInChannel :: VersionRangeChat -> User -> GroupInfo -> GroupMember -> CM () introduceInChannel _ _ _ GroupMember {activeConn = Nothing} = throwChatError $ CEInternalError "member connection not active" introduceInChannel vr user gInfo subscriber@GroupMember {activeConn = Just conn, indexInGroup = subscriberIdx} = do - -- roster first, so the joiner trusts mod/admin keys before relay intros - forwardCachedRoster user gInfo subscriber - -- filter to current members so left/removed mods/admins aren't re-introduced to the joiner - modMs <- filter memberCurrent <$> withStore' (\db -> getGroupModerators db vr user gInfo) + (owners, rosterMems) <- withStore' $ \db -> + (,) <$> getGroupOwners db vr user gInfo <*> getGroupRosterMembers db vr user gInfo + let modMs = owners <> rosterMems void $ sendGroupMessage' user gInfo modMs $ XGrpMemNew (memberInfo gInfo subscriber) Nothing withStore' $ \db -> setMemberVectorNewRelations db subscriber [(indexInGroup m, (IDSubjectIntroduced, MRIntroduced)) | m <- modMs] - let introEvts = map (memberIntroEvt gInfo) modMs - forM_ (L.nonEmpty introEvts) $ \introEvts' -> - sendGroupMemberMessages user gInfo conn introEvts' + -- owner intros first so the joiner has the owner profile loaded before applying the cached roster (signed by the owner) + sendIntros owners + forwardCachedRoster user gInfo subscriber + sendIntros rosterMems withStore' $ \db -> setMembersVectorsNewRelation db modMs subscriberIdx IDSubjectIntroduced MRIntroduced + where + sendIntros ms = forM_ (L.nonEmpty $ map (memberIntroEvt gInfo) ms) $ \evts -> + sendGroupMemberMessages user gInfo conn evts userProfileInGroup :: User -> GroupInfo -> Maybe Profile -> Profile userProfileInGroup user = userProfileInGroup' user . groupFeatureUserAllowed SGFSimplexLinks @@ -1228,8 +1231,8 @@ isRosterRole r = r == GRModerator || r == GRAdmin -- Drop non-privileged-role entries and de-duplicate by memberId, keeping the first. validateGroupRoster :: GroupRoster -> GroupRoster -validateGroupRoster GroupRoster {version, roster = entries} = - GroupRoster {version, roster = dedup [] $ filter (\RosterMember {role} -> isRosterRole role) entries} +validateGroupRoster GroupRoster {version = ver, roster = entries} = + GroupRoster {version = ver, roster = dedup [] $ filter (\RosterMember {role} -> isRosterRole role) entries} where dedup _ [] = [] dedup seen (rm@RosterMember {memberId} : rms) diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs index decf4126b8..f697014143 100644 --- a/src/Simplex/Chat/Store/Groups.hs +++ b/src/Simplex/Chat/Store/Groups.hs @@ -68,6 +68,7 @@ module Simplex.Chat.Store.Groups getSupportScopeMembersByIndexes, getGroupModerators, getGroupRosterMembers, + getGroupOwners, getGroupRelayMembers, getGroupMembersForExpiration, getRemovedMembersToCleanup, @@ -1215,6 +1216,14 @@ getGroupRosterMembers db vr user@User {userId, userContactId} GroupInfo {groupId (groupMemberQuery <> " WHERE m.user_id = ? AND m.group_id = ? AND (m.contact_id IS NULL OR m.contact_id != ?) AND m.member_role IN (?,?)") (userId, groupId, userContactId, GRModerator, GRAdmin) +getGroupOwners :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> IO [GroupMember] +getGroupOwners db vr user@User {userId, userContactId} GroupInfo {groupId} = do + filter memberCurrent . map (toContactMember vr user) + <$> DB.query + db + (groupMemberQuery <> " WHERE m.user_id = ? AND m.group_id = ? AND (m.contact_id IS NULL OR m.contact_id != ?) AND m.member_role = ?") + (userId, groupId, userContactId, GROwner) + getGroupRelayMembers :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> IO [GroupMember] getGroupRelayMembers db vr user@User {userId, userContactId} GroupInfo {groupId} = do map (toContactMember vr user) diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs index 37e9e5b677..aa9bc4b193 100644 --- a/tests/ChatTests/Groups.hs +++ b/tests/ChatTests/Groups.hs @@ -9646,30 +9646,38 @@ testChannelRoleTransitionsUpdateRoster ps = withNewTestChat ps "eve" eveProfile $ \eve -> withNewTestChat ps "frank" frankProfile $ \frank -> do (shortLink, fullLink) <- prepareChannel1Relay "team" alice bob - forM_ [cath, dan, eve] $ \member -> - memberJoinChannel "team" [bob] [alice] shortLink fullLink member - -- member -> moderator: dan/eve XGrpMemRole skipped; roster apply creates cath and emits chat item - threadDelay 1000000 + memberJoinChannel "team" [bob] [alice] shortLink fullLink cath + -- member -> moderator + threadDelay 100000 alice ##> "/mr #team cath moderator" alice <## "#team: you changed the role of cath to moderator (signed)" concurrentlyN_ [ bob <## "#team: alice changed the role of cath from member to moderator (signed)", - cath <## "#team: alice changed your role from member to moderator (signed)", - dan <## "#team: alice changed the role of cath from member to moderator (signed)", - eve <## "#team: alice changed the role of cath from member to moderator (signed)" + cath <## "#team: alice changed your role from member to moderator (signed)" ] - -- moderator -> admin (within roster): dan/eve now know cath, so role event lands cleanly - threadDelay 1000000 + -- dan joins; cached roster has cath as moderator + threadDelay 100000 + memberJoinChannel "team" [bob] [alice, cath] shortLink fullLink dan + dan <## "#team: alice changed the role of cath from member to moderator (signed)" + threadDelay 100000 + checkMemberRow dan "cath" (Just "moderator") + -- moderator -> admin: dan now knows cath, role event lands cleanly + threadDelay 100000 alice ##> "/mr #team cath admin" alice <## "#team: you changed the role of cath to admin (signed)" concurrentlyN_ [ bob <## "#team: alice changed the role of cath from moderator to admin (signed)", cath <## "#team: alice changed your role from moderator to admin (signed)", - dan <## "#team: alice changed the role of cath from moderator to admin (signed)", - eve <## "#team: alice changed the role of cath from moderator to admin (signed)" + dan <## "#team: alice changed the role of cath from moderator to admin (signed)" ] + -- eve joins; cached roster has cath as admin + threadDelay 100000 + memberJoinChannel "team" [bob] [alice, cath] shortLink fullLink eve + eve <## "#team: alice changed the role of cath from member to admin (signed)" + threadDelay 100000 + checkMemberRow eve "cath" (Just "admin") -- admin -> member (crossing out of roster): roster drops cath - threadDelay 1000000 + threadDelay 100000 alice ##> "/mr #team cath member" alice <## "#team: you changed the role of cath to member (signed)" concurrentlyN_ @@ -9678,8 +9686,8 @@ testChannelRoleTransitionsUpdateRoster ps = dan <## "#team: alice changed the role of cath from admin to member (signed)", eve <## "#team: alice changed the role of cath from admin to member (signed)" ] - -- frank joins; cath never posted, and isn't in the roster, so frank has no record of her - threadDelay 1000000 + -- frank joins; cath isn't in the roster, so frank has no record of her + threadDelay 100000 memberJoinChannel "team" [bob] [alice] shortLink fullLink frank threadDelay 100000 checkMemberRow frank "cath" Nothing