diff --git a/changelog.d/2-features/WPB-19713 b/changelog.d/2-features/WPB-19713 new file mode 100644 index 0000000000..cdbf1fd8a2 --- /dev/null +++ b/changelog.d/2-features/WPB-19713 @@ -0,0 +1 @@ +Implement `channels` and `channelsCount` in `user-groups` endpoints. diff --git a/integration/test/API/Brig.hs b/integration/test/API/Brig.hs index bce2531ada..1cb9d53308 100644 --- a/integration/test/API/Brig.hs +++ b/integration/test/API/Brig.hs @@ -1061,6 +1061,11 @@ getUserGroup user gid = do req <- baseRequest user Brig Versioned $ joinHttpPath ["user-groups", gid] submit "GET" req +getUserGroupWithChannels :: (MakesValue user) => user -> String -> App Response +getUserGroupWithChannels user gid = do + req <- baseRequest user Brig Versioned $ joinHttpPath ["user-groups", gid] + submit "GET" $ req & addQueryParams [("include_channels", "true")] + updateUserGroupChannels :: (MakesValue user) => user -> String -> [String] -> App Response updateUserGroupChannels user gid convIds = do req <- baseRequest user Brig Versioned $ joinHttpPath ["user-groups", gid, "channels"] @@ -1074,11 +1079,23 @@ data GetUserGroupsArgs = GetUserGroupsArgs lastName :: Maybe String, lastCreatedAt :: Maybe String, lastId :: Maybe String, - includeMemberCount :: Bool + includeMemberCount :: Bool, + includeChannels :: Bool } instance Default GetUserGroupsArgs where - def = GetUserGroupsArgs Nothing Nothing Nothing Nothing Nothing Nothing Nothing False + def = + GetUserGroupsArgs + { q = Nothing, + sortByKeys = Nothing, + sortOrder = Nothing, + pSize = Nothing, + lastName = Nothing, + lastCreatedAt = Nothing, + lastId = Nothing, + includeMemberCount = False, + includeChannels = False + } getUserGroups :: (MakesValue user) => user -> GetUserGroupsArgs -> App Response getUserGroups user GetUserGroupsArgs {..} = do @@ -1094,7 +1111,8 @@ getUserGroups user GetUserGroupsArgs {..} = do ("last_seen_name",) <$> lastName, ("last_seen_created_at",) <$> lastCreatedAt, ("last_seen_id",) <$> lastId, - (if includeMemberCount then Just ("include_member_count", "true") else Nothing) + (if includeMemberCount then Just ("include_member_count", "true") else Nothing), + (if includeChannels then Just ("include_channels", "true") else Nothing) ] ) diff --git a/integration/test/Test/UserGroup.hs b/integration/test/Test/UserGroup.hs index 6b3fae9674..4e34207c1c 100644 --- a/integration/test/Test/UserGroup.hs +++ b/integration/test/Test/UserGroup.hs @@ -339,7 +339,8 @@ testUserGroupGetGroupsAllInputs = do lastName = lastName', lastCreatedAt = lastCreatedAt', lastId = lastId', - includeMemberCount = includeMemberCount' + includeMemberCount = includeMemberCount', + includeChannels = includeChannels' } | q' <- qs, sortBy' <- sortByKeysList, @@ -348,7 +349,8 @@ testUserGroupGetGroupsAllInputs = do lastName' <- lastNames, lastCreatedAt' <- lastCreatedAts, lastId' <- lastIds, - includeMemberCount' <- [False, True] + includeMemberCount' <- [False, True], + includeChannels' <- [False, True] ] where qs = [Nothing, Just "A"] @@ -392,8 +394,8 @@ testUserGroupRemovalOnDelete = do resp.status `shouldMatchInt` 200 resp.json %. "members" `shouldMatch` [charlieId] -testUserGroupUpdateChannels :: (HasCallStack) => App () -testUserGroupUpdateChannels = do +testUserGroupUpdateChannelsSucceeds :: (HasCallStack) => App () +testUserGroupUpdateChannelsSucceeds = do (alice, tid, [_bob]) <- createTeam OwnDomain 2 setTeamFeatureLockStatus alice tid "channels" "unlocked" let config = @@ -412,22 +414,41 @@ testUserGroupUpdateChannels = do >>= getJSON 200 gid <- ug %. "id" & asString - convId <- - postConversation alice (defMLS {team = Just tid, groupConvType = Just "channel"}) - >>= getJSON 201 - >>= objConvId + convs <- replicateM 5 $ postConversation alice (defMLS {team = Just tid, groupConvType = Just "channel"}) >>= getJSON 201 >>= objConvId + withWebSocket alice $ \wsAlice -> do - updateUserGroupChannels alice gid [convId.id_] >>= assertSuccess + updateUserGroupChannels alice gid ((.id_) <$> take 2 convs) >>= assertSuccess notif <- awaitMatch isUserGroupUpdatedNotif wsAlice notif %. "payload.0.user_group.id" `shouldMatch` gid - -- bobId <- asString $ bob %. "id" - bindResponse (getUserGroup alice gid) $ \resp -> do + bindResponse (getUserGroupWithChannels alice gid) $ \resp -> do + resp.status `shouldMatchInt` 200 + (resp.json %. "channels" >>= asList >>= traverse objQid) `shouldMatchSet` for (take 2 convs) objQid + + bindResponse (getUserGroups alice (def {includeChannels = True})) $ \resp -> do + resp.status `shouldMatchInt` 200 + (resp.json %. "page.0.channels" >>= asList >>= traverse objQid) `shouldMatchSet` for (take 2 convs) objQid + + updateUserGroupChannels alice gid ((.id_) <$> tail convs) >>= assertSuccess + + bindResponse (getUserGroupWithChannels alice gid) $ \resp -> do + resp.status `shouldMatchInt` 200 + (resp.json %. "channels" >>= asList >>= traverse objQid) `shouldMatchSet` for (tail convs) objQid + + bindResponse (getUserGroups alice (def {includeChannels = True})) $ \resp -> do resp.status `shouldMatchInt` 200 + (resp.json %. "page.0.channels" >>= asList >>= traverse objQid) `shouldMatchSet` for (tail convs) objQid --- FUTUREWORK: check the actual associated channels --- resp.json %. "members" `shouldMatch` [bobId] + updateUserGroupChannels alice gid [] >>= assertSuccess + + bindResponse (getUserGroupWithChannels alice gid) $ \resp -> do + resp.status `shouldMatchInt` 200 + (resp.json %. "channels" >>= fmap length . asList) `shouldMatchInt` 0 + + bindResponse (getUserGroups alice (def {includeChannels = True})) $ \resp -> do + resp.status `shouldMatchInt` 200 + (resp.json %. "page.0.channels" >>= fmap length . asList) `shouldMatchInt` 0 testUserGroupUpdateChannelsNonAdmin :: (HasCallStack) => App () testUserGroupUpdateChannelsNonAdmin = do diff --git a/libs/types-common/default.nix b/libs/types-common/default.nix index 0cafcabbfd..63392d3321 100644 --- a/libs/types-common/default.nix +++ b/libs/types-common/default.nix @@ -35,6 +35,7 @@ , openapi3 , optparse-applicative , pem +, polysemy , protobuf , QuickCheck , quickcheck-instances @@ -93,6 +94,7 @@ mkDerivation { openapi3 optparse-applicative pem + polysemy protobuf QuickCheck quickcheck-instances diff --git a/libs/types-common/src/Data/Qualified.hs b/libs/types-common/src/Data/Qualified.hs index 18407f557f..fc59fd841c 100644 --- a/libs/types-common/src/Data/Qualified.hs +++ b/libs/types-common/src/Data/Qualified.hs @@ -47,6 +47,7 @@ module Data.Qualified deprecatedSchema, qualifiedSchema, qualifiedObjectSchema, + inputQualifyLocal, ) where @@ -61,6 +62,8 @@ import Data.OpenApi (deprecated) import Data.OpenApi qualified as S import Data.Schema import Imports hiding (local) +import Polysemy +import Polysemy.Input import Test.QuickCheck (Arbitrary (arbitrary)) ---------------------------------------------------------------------- @@ -234,3 +237,11 @@ instance S.ToSchema (Qualified Handle) where instance (Arbitrary a) => Arbitrary (Qualified a) where arbitrary = Qualified <$> arbitrary <*> arbitrary + +---------------------------------------------------------------------- +-- Polysemy + +inputQualifyLocal :: (Member (Input (Local ())) r) => a -> Sem r (Local a) +inputQualifyLocal a = do + l <- input @(Local ()) + pure $ qualifyAs l a diff --git a/libs/types-common/types-common.cabal b/libs/types-common/types-common.cabal index 393ee5b74b..096b81173b 100644 --- a/libs/types-common/types-common.cabal +++ b/libs/types-common/types-common.cabal @@ -125,6 +125,7 @@ library , openapi3 , optparse-applicative >=0.10 , pem + , polysemy , protobuf >=0.2 , QuickCheck >=2.9 , quickcheck-instances >=0.3.16 diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs index fc8e1ea258..a81fcd5496 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs @@ -314,7 +314,7 @@ type UserGroupAPI = ) :<|> Named "get-user-group" - ( Summary "[STUB] (channels in response not implemented)" + ( Summary "Fetch a group accessible from the logged-in user" :> From 'V10 :> ZLocalUser :> CanThrow 'UserGroupNotFound @@ -331,7 +331,7 @@ type UserGroupAPI = ) :<|> Named "get-user-groups" - ( Summary "[STUB] (channelsCount not implemented)" + ( Summary "Fetch groups accessible from the logged-in user" :> From 'V10 :> ZLocalUser :> "user-groups" @@ -342,6 +342,7 @@ type UserGroupAPI = :> QueryParam' '[Optional, Strict, LastSeenNameDesc] "last_seen_name" UserGroupName :> QueryParam' '[Optional, Strict, LastSeenCreatedAtDesc] "last_seen_created_at" UTCTimeMillis :> QueryParam' '[Optional, Strict, LastSeenIdDesc] "last_seen_id" UserGroupId + :> QueryFlag "include_channels" :> QueryFlag "include_member_count" :> Get '[JSON] UserGroupPage ) diff --git a/libs/wire-api/src/Wire/API/UserGroup.hs b/libs/wire-api/src/Wire/API/UserGroup.hs index 7b36a366e8..f4dfa28d94 100644 --- a/libs/wire-api/src/Wire/API/UserGroup.hs +++ b/libs/wire-api/src/Wire/API/UserGroup.hs @@ -110,7 +110,7 @@ userGroupToMeta ug = { id_ = ug.id_, name = ug.name, members = Const (), - channels = Const (), + channels = ug.channels, membersCount = ug.membersCount, channelsCount = ug.channelsCount, managedBy = ug.managedBy, @@ -121,8 +121,8 @@ data UserGroup_ (f :: Type -> Type) = UserGroup_ { id_ :: UserGroupId, name :: UserGroupName, members :: f (Vector UserId), - channels :: f (Maybe (Vector (Qualified ConvId))), membersCount :: Maybe Int, + channels :: Maybe (Vector (Qualified ConvId)), channelsCount :: Maybe Int, managedBy :: ManagedBy, createdAt :: UTCTimeMillis @@ -150,8 +150,8 @@ instance ToSchema (UserGroup_ (Const ())) where <$> (.id_) .= field "id" schema <*> (.name) .= field "name" schema <*> (.members) .= pure mempty - <*> (.channels) .= pure mempty <*> (.membersCount) .= maybe_ (optField "membersCount" schema) + <*> (.channels) .= maybe_ (optField "channels" (vector schema)) <*> (.channelsCount) .= maybe_ (optField "channelsCount" schema) <*> (.managedBy) .= field "managedBy" schema <*> (.createdAt) .= field "createdAt" schema @@ -177,8 +177,8 @@ instance ToSchema (UserGroup_ Identity) where <$> (.id_) .= field "id" schema <*> (.name) .= field "name" schema <*> (runIdentity . (.members)) .= field "members" (Identity <$> vector schema) - <*> (runIdentity . (.channels)) .= (Identity <$> maybe_ (optField "channels" (vector schema))) <*> (.membersCount) .= maybe_ (optField "membersCount" schema) + <*> (.channels) .= maybe_ (optField "channels" (vector schema)) <*> (.channelsCount) .= maybe_ (optField "channelsCount" schema) <*> (.managedBy) .= field "managedBy" schema <*> (.createdAt) .= field "createdAt" schema diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/UserGroup.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/UserGroup.hs index ae40851ffc..4c2f9313df 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/UserGroup.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/UserGroup.hs @@ -42,9 +42,9 @@ testObject_UserGroup_1 :: UserGroupMeta testObject_UserGroup_1 = UserGroup_ { id_ = userGroupId1, - name = (unsafeToUserGroupName "name"), - members = (Const ()), - channels = (Const ()), + name = unsafeToUserGroupName "name", + members = Const (), + channels = Nothing, membersCount = Nothing, channelsCount = Just 0, managedBy = ManagedByWire, @@ -55,10 +55,10 @@ testObject_UserGroup_2 :: UserGroup testObject_UserGroup_2 = UserGroup_ { id_ = userGroupId2, - name = (unsafeToUserGroupName "yet another one"), - members = (Identity $ fromList [userId1, userId2]), + name = unsafeToUserGroupName "yet another one", + members = Identity $ fromList [userId1, userId2], channels = - Identity . Just . fromList $ + Just . fromList $ [ Qualified (Id (fromJust (UUID.fromString "445c08d2-a16b-49ea-a274-4208bb2efe8f"))) (Domain "example.com") ], membersCount = Nothing, diff --git a/libs/wire-subsystems/src/Wire/UserGroupStore.hs b/libs/wire-subsystems/src/Wire/UserGroupStore.hs index 2136b2a60a..ad973bd7a2 100644 --- a/libs/wire-subsystems/src/Wire/UserGroupStore.hs +++ b/libs/wire-subsystems/src/Wire/UserGroupStore.hs @@ -18,7 +18,8 @@ data UserGroupPageRequest = UserGroupPageRequest paginationState :: PaginationState, sortOrder :: SortOrder, pageSize :: PageSize, - includeMemberCount :: Bool + includeMemberCount :: Bool, + includeChannels :: Bool } data PaginationState = PaginationSortByName (Maybe (UserGroupName, UserGroupId)) | PaginationSortByCreatedAt (Maybe (UTCTimeMillis, UserGroupId)) @@ -33,7 +34,7 @@ toSortBy = \case data UserGroupStore m a where CreateUserGroup :: TeamId -> NewUserGroup -> ManagedBy -> UserGroupStore m UserGroup - GetUserGroup :: TeamId -> UserGroupId -> UserGroupStore m (Maybe UserGroup) + GetUserGroup :: TeamId -> UserGroupId -> Bool -> UserGroupStore m (Maybe UserGroup) GetUserGroups :: UserGroupPageRequest -> UserGroupStore m UserGroupPage UpdateUserGroup :: TeamId -> UserGroupId -> UserGroupUpdate -> UserGroupStore m (Maybe ()) DeleteUserGroup :: TeamId -> UserGroupId -> UserGroupStore m (Maybe ()) diff --git a/libs/wire-subsystems/src/Wire/UserGroupStore/Postgres.hs b/libs/wire-subsystems/src/Wire/UserGroupStore/Postgres.hs index 3249df272e..079e9cc810 100644 --- a/libs/wire-subsystems/src/Wire/UserGroupStore/Postgres.hs +++ b/libs/wire-subsystems/src/Wire/UserGroupStore/Postgres.hs @@ -9,12 +9,14 @@ import Data.Functor.Contravariant.Divisible import Data.Id import Data.Json.Util import Data.Profunctor +import Data.Qualified (Local, QualifiedWithTag (tUntagged), inputQualifyLocal, qualifyAs) import Data.Range import Data.Text qualified as T import Data.Text.Encoding qualified as TE import Data.Time import Data.UUID as UUID import Data.Vector (Vector) +import Data.Vector qualified as V import Hasql.Decoders qualified as HD import Hasql.Encoders qualified as HE import Hasql.Pool @@ -41,12 +43,12 @@ type UserGroupStorePostgresEffectConstraints r = interpretUserGroupStoreToPostgres :: forall r. - (UserGroupStorePostgresEffectConstraints r) => + (UserGroupStorePostgresEffectConstraints r, Member (Input (Local ())) r) => InterpreterFor UserGroupStore r interpretUserGroupStoreToPostgres = interpret $ \case CreateUserGroup team newUserGroup managedBy -> createUserGroup team newUserGroup managedBy - GetUserGroup team userGroupId -> getUserGroup team userGroupId + GetUserGroup team userGroupId includeChannels -> getUserGroup team userGroupId includeChannels GetUserGroups req -> getUserGroups req UpdateUserGroup tid gid gup -> updateGroup tid gid gup DeleteUserGroup tid gid -> deleteGroup tid gid @@ -75,30 +77,47 @@ updateUsers gid uids = do getUserGroup :: forall r. - (UserGroupStorePostgresEffectConstraints r) => + (UserGroupStorePostgresEffectConstraints r, Member (Input (Local ())) r) => TeamId -> UserGroupId -> + Bool -> Sem r (Maybe UserGroup) -getUserGroup team id_ = do +getUserGroup team id_ includeChannels = do pool <- input - eitherUserGroup <- liftIO $ use pool session + loc <- inputQualifyLocal () + eitherUserGroup <- liftIO $ use pool (if includeChannels then sessionWithChannels loc else session) either throw pure eitherUserGroup where session :: Session (Maybe UserGroup) session = runMaybeT do (name, managedBy, createdAt) <- MaybeT $ statement (id_, team) getGroupMetadataStatement members <- lift $ Identity <$> statement id_ getGroupMembersStatement - let membersCount = Nothing + let membersCount = Just . V.length $ runIdentity members channelsCount = Nothing channels = mempty pure $ UserGroup_ {..} + sessionWithChannels :: Local a -> Session (Maybe UserGroup) + sessionWithChannels loc = runMaybeT do + (name, managedBy, createdAt, memberIds, channelIds) <- MaybeT $ statement (id_, team) getGroupWithMembersAndChannelsStatement + let members = Identity (fmap Id memberIds) + membersCount = Just $ V.length memberIds + channels = Just (fmap (tUntagged . qualifyAs loc . Id) channelIds) + channelsCount = Just $ V.length channelIds + pure $ UserGroup_ {..} + decodeMetadataRow :: (Text, Int32, UTCTime) -> Either Text (UserGroupName, ManagedBy, UTCTimeMillis) decodeMetadataRow (name, managedByInt, utcTime) = (,,toUTCTimeMillis utcTime) <$> userGroupNameFromText name <*> managedByFromInt32 managedByInt + decodeWithArrays :: (Text, Int32, UTCTime, Vector UUID, Vector UUID) -> Either Text (UserGroupName, ManagedBy, UTCTimeMillis, Vector UUID, Vector UUID) + decodeWithArrays (name, managedByInt, utcTime, membs, chans) = do + n <- userGroupNameFromText name + m <- managedByFromInt32 managedByInt + pure (n, m, toUTCTimeMillis utcTime, membs, chans) + getGroupMetadataStatement :: Statement (UserGroupId, TeamId) (Maybe (UserGroupName, ManagedBy, UTCTimeMillis)) getGroupMetadataStatement = lmap (\(gid, tid) -> (gid.toUUID, tid.toUUID)) @@ -115,6 +134,21 @@ getUserGroup team id_ = do select (user_id :: uuid) from user_group_member where user_group_id = ($1 :: uuid) |] + getGroupWithMembersAndChannelsStatement :: Statement (UserGroupId, TeamId) (Maybe (UserGroupName, ManagedBy, UTCTimeMillis, Vector UUID, Vector UUID)) + getGroupWithMembersAndChannelsStatement = + lmap (\(gid, tid) -> (gid.toUUID, tid.toUUID)) + . refineResult (mapM decodeWithArrays) + $ [maybeStatement| + select + (name :: text), + (managed_by :: int), + (created_at :: timestamptz), + coalesce((select array_agg(ugm.user_id) from user_group_member ugm where ugm.user_group_id = ug.id), array[]::uuid[]) :: uuid[], + coalesce((select array_agg(ugc.conv_id) from user_group_channel ugc where ugc.user_group_id = ug.id), array[]::uuid[]) :: uuid[] + from user_group ug + where ug.id = ($1 :: uuid) and ug.team_id = ($2 :: uuid) + |] + divide3 :: (Divisible f) => (p -> (a, b, c)) -> f a -> f b -> f c -> f p divide3 f a b c = divide (\p -> let (x, y, z) = f p in (x, (y, z))) a (divide id b c) @@ -126,49 +160,52 @@ divide5 f a b c d e = divide (\p -> let (v, w, x, y, z) = f p in (v, (w, x, y, z getUserGroups :: forall r. - (UserGroupStorePostgresEffectConstraints r) => + ( UserGroupStorePostgresEffectConstraints r, + Member (Input (Local ())) r + ) => UserGroupPageRequest -> Sem r UserGroupPage getUserGroups req@(UserGroupPageRequest {..}) = do pool <- input + loc <- inputQualifyLocal () eitherResult <- liftIO $ use pool do TxSessions.transaction TxSessions.ReadCommitted TxSessions.Read do - UserGroupPage <$> getUserGroupsSession <*> getCountSession + UserGroupPage <$> getUserGroupsSession loc <*> getCountSession either throw pure eitherResult where - getUserGroupsSession :: Tx.Transaction [UserGroupMeta] - getUserGroupsSession = case (req.searchString, req.paginationState) of + getUserGroupsSession :: Local a -> Tx.Transaction [UserGroupMeta] + getUserGroupsSession loc = case (req.searchString, req.paginationState) of (Nothing, PaginationSortByName Nothing) -> do let encoder = divide id encodeId encodeInt - stmt = refineResult (mapM parseRow) $ Statement queryBS encoder decodeRow True + stmt = refineResult (mapM $ parseRow loc) $ Statement queryBS encoder decodeRow True Tx.statement (req.team, pageSizeInt) stmt (Nothing, PaginationSortByCreatedAt Nothing) -> do let encoder = divide id encodeId encodeInt - stmt = refineResult (mapM parseRow) $ Statement queryBS encoder decodeRow True + stmt = refineResult (mapM $ parseRow loc) $ Statement queryBS encoder decodeRow True Tx.statement (req.team, pageSizeInt) stmt (Nothing, PaginationSortByName (Just (name, gid))) -> do let encoder = divide4 id encodeId encodeGroupName encodeId encodeInt - stmt = refineResult (mapM parseRow) $ Statement queryBS encoder decodeRow True + stmt = refineResult (mapM $ parseRow loc) $ Statement queryBS encoder decodeRow True Tx.statement (req.team, name, gid, pageSizeInt) stmt (Nothing, PaginationSortByCreatedAt (Just (timestamp, gid))) -> do let encoder = divide4 id encodeId encodeTime encodeId encodeInt - stmt = refineResult (mapM parseRow) $ Statement queryBS encoder decodeRow True + stmt = refineResult (mapM $ parseRow loc) $ Statement queryBS encoder decodeRow True Tx.statement (req.team, timestamp, gid, pageSizeInt) stmt (Just st, PaginationSortByName Nothing) -> do let encoder = divide3 id encodeId encodeText encodeInt - stmt = refineResult (mapM parseRow) $ Statement queryBS encoder decodeRow True + stmt = refineResult (mapM $ parseRow loc) $ Statement queryBS encoder decodeRow True Tx.statement (req.team, fuzzy st, pageSizeInt) stmt (Just st, PaginationSortByCreatedAt Nothing) -> do let encoder = divide3 id encodeId encodeText encodeInt - stmt = refineResult (mapM parseRow) $ Statement queryBS encoder decodeRow True + stmt = refineResult (mapM $ parseRow loc) $ Statement queryBS encoder decodeRow True Tx.statement (req.team, fuzzy st, pageSizeInt) stmt (Just st, PaginationSortByName (Just (name, gid))) -> do let encoder = divide5 id encodeId encodeGroupName encodeId encodeText encodeInt - stmt = refineResult (mapM parseRow) $ Statement queryBS encoder decodeRow True + stmt = refineResult (mapM $ parseRow loc) $ Statement queryBS encoder decodeRow True Tx.statement (req.team, name, gid, fuzzy st, pageSizeInt) stmt (Just st, PaginationSortByCreatedAt (Just (timestamp, gid))) -> do let encoder = divide5 id encodeId encodeTime encodeId encodeText encodeInt - stmt = refineResult (mapM parseRow) $ Statement queryBS encoder decodeRow True + stmt = refineResult (mapM $ parseRow loc) $ Statement queryBS encoder decodeRow True Tx.statement (req.team, timestamp, gid, fuzzy st, pageSizeInt) stmt getCountSession :: Tx.Transaction Int @@ -218,19 +255,34 @@ getUserGroups req@(UserGroupPageRequest {..}) = do encodeTime :: HE.Params UTCTimeMillis encodeTime = contramap fromUTCTimeMillis $ HE.param $ HE.nonNullable HE.timestamptz - decodeRow :: HD.Result [(UUID, Text, Int32, UTCTime, Maybe Int32)] + decodeRow :: HD.Result [(UUID, Text, Int32, UTCTime, Maybe Int32, Int32, Maybe (Vector UUID))] decodeRow = HD.rowList - ( (,,,,) + ( (,,,,,,) <$> HD.column (HD.nonNullable HD.uuid) <*> HD.column (HD.nonNullable HD.text) <*> HD.column (HD.nonNullable HD.int4) <*> HD.column (HD.nonNullable HD.timestamptz) <*> (if req.includeMemberCount then Just <$> HD.column (HD.nonNullable HD.int4) else pure Nothing) + <*> HD.column (HD.nonNullable HD.int4) + <*> ( if req.includeChannels + then + Just + <$> HD.column + ( HD.nonNullable + ( HD.array + ( HD.dimension + V.replicateM + (HD.element (HD.nonNullable HD.uuid)) + ) + ) + ) + else pure Nothing + ) ) - parseRow :: (UUID, Text, Int32, UTCTime, Maybe Int32) -> Either Text UserGroupMeta - parseRow (Id -> id_, namePre, managedByPre, toUTCTimeMillis -> createdAt, membersCountRaw) = do + parseRow :: Local a -> (UUID, Text, Int32, UTCTime, Maybe Int32, Int32, Maybe (Vector UUID)) -> Either Text UserGroupMeta + parseRow loc (Id -> id_, namePre, managedByPre, toUTCTimeMillis -> createdAt, membersCountRaw, channelsCountRaw, maybeChannels) = do managedBy <- case managedByPre of 0 -> pure ManagedByWire 1 -> pure ManagedByScim @@ -238,8 +290,8 @@ getUserGroups req@(UserGroupPageRequest {..}) = do name <- userGroupNameFromText namePre let members = Const () membersCount = fromIntegral <$> membersCountRaw - channelsCount = Nothing - channels = mempty + channelsCount = Just (fromIntegral channelsCountRaw) + channels = fmap (fmap (tUntagged . qualifyAs loc . Id)) maybeChannels pure $ UserGroup_ {..} -- \| Compile a pagination state into select query to return the next page. Result is the @@ -254,6 +306,8 @@ getUserGroups req@(UserGroupPageRequest {..}) = do filter (not . T.null) $ ["id", "name", "managed_by", "created_at"] <> ["(select count(*) from user_group_member as ugm where ugm.user_group_id = ug.id) as members" | includeMemberCount] + <> ["(select count(*) from user_group_channel as ugc where ugc.user_group_id = ug.id) as channels"] + <> ["coalesce((select array_agg(ugc.conv_id) from user_group_channel as ugc where ugc.user_group_id = ug.id), array[]::uuid[]) as channel_ids" | includeChannels] whr = "where team_id = ($1 :: uuid)" sortColumn = toSortBy paginationState orderBy = T.unwords ["order by", sortColumnName sortColumn, sortOrderClause sortOrder <> ", id", sortOrderClause sortOrder] diff --git a/libs/wire-subsystems/src/Wire/UserGroupSubsystem.hs b/libs/wire-subsystems/src/Wire/UserGroupSubsystem.hs index 16df18d5b5..f50ab4046a 100644 --- a/libs/wire-subsystems/src/Wire/UserGroupSubsystem.hs +++ b/libs/wire-subsystems/src/Wire/UserGroupSubsystem.hs @@ -14,7 +14,7 @@ import Wire.API.UserGroup.Pagination data UserGroupSubsystem m a where CreateGroup :: UserId -> NewUserGroup -> UserGroupSubsystem m UserGroup - GetGroup :: UserId -> UserGroupId -> UserGroupSubsystem m (Maybe UserGroup) + GetGroup :: UserId -> UserGroupId -> Bool -> UserGroupSubsystem m (Maybe UserGroup) GetGroups :: UserId -> Maybe Text -> @@ -25,6 +25,7 @@ data UserGroupSubsystem m a where Maybe UTCTimeMillis -> Maybe UserGroupId -> Bool -> + Bool -> UserGroupSubsystem m UserGroupPage UpdateGroup :: UserId -> UserGroupId -> UserGroupUpdate -> UserGroupSubsystem m () DeleteGroup :: UserId -> UserGroupId -> UserGroupSubsystem m () diff --git a/libs/wire-subsystems/src/Wire/UserGroupSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/UserGroupSubsystem/Interpreter.hs index 5eb9936add..1a522145fb 100644 --- a/libs/wire-subsystems/src/Wire/UserGroupSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/UserGroupSubsystem/Interpreter.hs @@ -44,9 +44,9 @@ interpretUserGroupSubsystem :: InterpreterFor UserGroupSubsystem r interpretUserGroupSubsystem = interpret $ \case CreateGroup creator newGroup -> createUserGroup creator newGroup - GetGroup getter gid -> getUserGroup getter gid - GetGroups getter q sortByKeys sortOrder pSize mLastGroupName mLastCreatedAt mLastGroupId includeMemberCount -> - getUserGroups getter q sortByKeys sortOrder pSize mLastGroupName mLastCreatedAt mLastGroupId includeMemberCount + GetGroup getter gid includeChannels -> getUserGroup getter gid includeChannels + GetGroups getter q sortByKeys sortOrder pSize mLastGroupName mLastCreatedAt mLastGroupId includeMemberCount includeChannels -> + getUserGroups getter q sortByKeys sortOrder pSize mLastGroupName mLastCreatedAt mLastGroupId includeMemberCount includeChannels UpdateGroup updater groupId groupUpdate -> updateGroup updater groupId groupUpdate DeleteGroup deleter groupId -> deleteGroup deleter groupId AddUser adder groupId addeeId -> addUser adder groupId addeeId @@ -143,11 +143,12 @@ getUserGroup :: ) => UserId -> UserGroupId -> + Bool -> Sem r (Maybe UserGroup) -getUserGroup getter gid = runMaybeT $ do +getUserGroup getter gid includeChannels = runMaybeT $ do team <- MaybeT $ getUserTeam getter getterCanSeeAll <- mkGetterCanSeeAll getter team - userGroup <- MaybeT $ Store.getUserGroup team gid + userGroup <- MaybeT $ Store.getUserGroup team gid includeChannels if getterCanSeeAll || getter `elem` (toList (runIdentity userGroup.members)) then pure userGroup else MaybeT $ pure Nothing @@ -178,8 +179,9 @@ getUserGroups :: Maybe UTCTimeMillis -> Maybe UserGroupId -> Bool -> + Bool -> Sem r UserGroupPage -getUserGroups getter searchString sortBy' sortOrder' mPageSize mLastGroupName mLastCreatedAt mLastGroupId includeMemberCount' = do +getUserGroups getter searchString sortBy' sortOrder' mPageSize mLastGroupName mLastCreatedAt mLastGroupId includeMemberCount' includeChannels' = do team :: TeamId <- getUserTeam getter >>= ifNothing UserGroupNotATeamAdmin getterCanSeeAll :: Bool <- fromMaybe False <$> runMaybeT (mkGetterCanSeeAll getter team) unless getterCanSeeAll (throw UserGroupNotATeamAdmin) @@ -192,7 +194,8 @@ getUserGroups getter searchString sortBy' sortOrder' mPageSize mLastGroupName mL SortByCreatedAt -> PaginationSortByCreatedAt $ (,) <$> mLastCreatedAt <*> mLastGroupId, team = team, searchString = searchString, - includeMemberCount = includeMemberCount' + includeMemberCount = includeMemberCount', + includeChannels = includeChannels' } Store.getUserGroups pageReq where @@ -257,7 +260,7 @@ addUser :: UserId -> Sem r () addUser adder groupId addeeId = do - ug <- getUserGroup adder groupId >>= note UserGroupNotFound + ug <- getUserGroup adder groupId False >>= note UserGroupNotFound team <- getTeamAsAdmin adder >>= note UserGroupNotATeamAdmin void $ internalGetTeamMember addeeId team >>= note UserGroupMemberIsNotInTheSameTeam unless (addeeId `elem` runIdentity ug.members) $ do @@ -279,7 +282,7 @@ addUsers :: Vector UserId -> Sem r () addUsers adder groupId addeeIds = do - ug <- getUserGroup adder groupId >>= note UserGroupNotFound + ug <- getUserGroup adder groupId False >>= note UserGroupNotFound team <- getTeamAsAdmin adder >>= note UserGroupNotATeamAdmin forM_ addeeIds $ \addeeId -> internalGetTeamMember addeeId team >>= note UserGroupMemberIsNotInTheSameTeam @@ -304,7 +307,7 @@ updateUsers :: Vector UserId -> Sem r () updateUsers updater groupId uids = do - void $ getUserGroup updater groupId >>= note UserGroupNotFound + void $ getUserGroup updater groupId False >>= note UserGroupNotFound team <- getTeamAsAdmin updater >>= note UserGroupNotATeamAdmin forM_ uids $ \uid -> internalGetTeamMember uid team >>= note UserGroupMemberIsNotInTheSameTeam @@ -326,7 +329,7 @@ removeUser :: UserId -> Sem r () removeUser remover groupId removeeId = do - ug <- getUserGroup remover groupId >>= note UserGroupNotFound + ug <- getUserGroup remover groupId False >>= note UserGroupNotFound team <- getTeamAsAdmin remover >>= note UserGroupNotATeamAdmin void $ internalGetTeamMember removeeId team >>= note UserGroupMemberIsNotInTheSameTeam when (removeeId `elem` runIdentity ug.members) $ do @@ -368,7 +371,8 @@ removeUserFromAllGroups uid tid = do fmap Store.userGroupCreatedAtPaginationState mug, team = tid, searchString = Nothing, - includeMemberCount = False + includeMemberCount = False, + includeChannels = False } updateChannels :: @@ -384,7 +388,7 @@ updateChannels :: Vector ConvId -> Sem r () updateChannels performer groupId channelIds = do - void $ getUserGroup performer groupId >>= note UserGroupNotFound + void $ getUserGroup performer groupId False >>= note UserGroupNotFound teamId <- getTeamAsAdmin performer >>= note UserGroupNotATeamAdmin for_ channelIds $ \channelId -> do conv <- internalGetConversation channelId >>= note UserGroupChannelNotFound diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserGroupStore.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserGroupStore.hs index 935197ffe2..ecf966865a 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserGroupStore.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserGroupStore.hs @@ -11,13 +11,14 @@ import Data.Domain (Domain (Domain)) import Data.Id import Data.Json.Util import Data.Map qualified as Map -import Data.Qualified (Qualified (Qualified)) +import Data.Qualified import Data.Text qualified as T import Data.Time.Clock import Data.Vector (Vector, fromList) import GHC.Stack import Imports import Polysemy +import Polysemy.Input import Polysemy.Internal (Append) import Polysemy.State import System.Random (StdGen, mkStdGen) @@ -42,6 +43,7 @@ type UserGroupStoreInMemEffectConstraints r = type UserGroupStoreInMemEffectStack = '[ UserGroupStore, State UserGroupInMemState, + Input (Local ()), Rnd.Random, State StdGen ] @@ -50,14 +52,15 @@ runInMemoryUserGroupStore :: (Member MockNow r) => UserGroupInMemState -> Sem (U runInMemoryUserGroupStore state = evalState (mkStdGen 3) . randomToStatefulStdGen + . runInputConst (toLocalUnsafe (Domain "my-domain") ()) . evalState state . userGroupStoreTestInterpreter -userGroupStoreTestInterpreter :: (UserGroupStoreInMemEffectConstraints r) => InterpreterFor UserGroupStore r +userGroupStoreTestInterpreter :: (UserGroupStoreInMemEffectConstraints r, Member (Input (Local ())) r) => InterpreterFor UserGroupStore r userGroupStoreTestInterpreter = interpret $ \case CreateUserGroup tid ng mb -> createUserGroupImpl tid ng mb - GetUserGroup tid gid -> getUserGroupImpl tid gid + GetUserGroup tid gid includeChannels -> getUserGroupImpl tid gid includeChannels GetUserGroups req -> getUserGroupsImpl req UpdateUserGroup tid gid gup -> updateUserGroupImpl tid gid gup DeleteUserGroup tid gid -> deleteUserGroupImpl tid gid @@ -93,12 +96,18 @@ createUserGroupImpl tid nug managedBy = do modify (Map.insert (tid, gid) ug) pure ug -getUserGroupImpl :: (UserGroupStoreInMemEffectConstraints r) => TeamId -> UserGroupId -> Sem r (Maybe UserGroup) -getUserGroupImpl tid gid = (Map.lookup (tid, gid)) <$> get @UserGroupInMemState +getUserGroupImpl :: (UserGroupStoreInMemEffectConstraints r) => TeamId -> UserGroupId -> Bool -> Sem r (Maybe UserGroup) +getUserGroupImpl tid gid includeChannels = fmap (filterChannels includeChannels) . Map.lookup (tid, gid) <$> get @UserGroupInMemState + +filterChannels :: Bool -> UserGroup -> UserGroup +filterChannels includeChannels ug = + if includeChannels + then (ug :: UserGroup) {channelsCount = Just $ maybe 0 length ug.channels} + else (ug :: UserGroup) {channels = mempty} getUserGroupsImpl :: (UserGroupStoreInMemEffectConstraints r) => UserGroupPageRequest -> Sem r UserGroupPage getUserGroupsImpl UserGroupPageRequest {..} = do - meta <- ((snd <$>) . sieve . fmap (_2 %~ userGroupToMeta) . Map.toList) <$> get @UserGroupInMemState + meta <- ((snd <$>) . sieve . fmap (_2 %~ userGroupToMeta . (filterChannels includeChannels)) . Map.toList) <$> get @UserGroupInMemState pure $ UserGroupPage meta (length meta) where sieve, @@ -152,7 +161,7 @@ getUserGroupsImpl UserGroupPageRequest {..} = do updateUserGroupImpl :: (UserGroupStoreInMemEffectConstraints r) => TeamId -> UserGroupId -> UserGroupUpdate -> Sem r (Maybe ()) updateUserGroupImpl tid gid (UserGroupUpdate newName) = do - exists <- getUserGroupImpl tid gid + exists <- getUserGroupImpl tid gid False let f :: Maybe UserGroup -> Maybe UserGroup f Nothing = Nothing f (Just g) = Just (g {name = newName} :: UserGroup) @@ -162,7 +171,7 @@ updateUserGroupImpl tid gid (UserGroupUpdate newName) = do deleteUserGroupImpl :: (UserGroupStoreInMemEffectConstraints r) => TeamId -> UserGroupId -> Sem r (Maybe ()) deleteUserGroupImpl tid gid = do - exists <- getUserGroupImpl tid gid + exists <- getUserGroupImpl tid gid False modify (Map.delete (tid, gid)) pure $ exists $> () @@ -183,24 +192,33 @@ removeUserImpl gid uid = do modifyUserGroupsGidOnly gid (Map.alter f) updateUserGroupChannelsImpl :: - (UserGroupStoreInMemEffectConstraints r) => + (UserGroupStoreInMemEffectConstraints r, Member (Input (Local ())) r) => UserGroupId -> Vector ConvId -> Sem r () updateUserGroupChannelsImpl gid convIds = do + qualifyLocal <- qualifyAs <$> input let f :: Maybe UserGroup -> Maybe UserGroup f Nothing = Nothing f (Just g) = Just ( g - { channels = Identity $ Just $ flip Qualified (Domain "") <$> convIds, - channelsCount = Just $ length convIds + { channels = Just $ tUntagged . qualifyLocal <$> convIds, + channelsCount = Nothing } :: UserGroup ) modifyUserGroupsGidOnly gid (Map.alter f) +listUserGroupChannelsImpl :: + (UserGroupStoreInMemEffectConstraints r) => + UserGroupId -> + Sem r (Vector ConvId) +listUserGroupChannelsImpl gid = + foldMap (fmap qUnqualified) . ((.channels) . snd <=< find ((== gid) . snd . fst) . Map.toList) + <$> get @(Map (TeamId, UserGroupId) UserGroup) + ---------------------------------------------------------------------- modifyUserGroupsGidOnly :: diff --git a/libs/wire-subsystems/test/unit/Wire/UserGroupSubsystem/InterpreterSpec.hs b/libs/wire-subsystems/test/unit/Wire/UserGroupSubsystem/InterpreterSpec.hs index 77fd7e3031..e01f2500e8 100644 --- a/libs/wire-subsystems/test/unit/Wire/UserGroupSubsystem/InterpreterSpec.hs +++ b/libs/wire-subsystems/test/unit/Wire/UserGroupSubsystem/InterpreterSpec.hs @@ -138,7 +138,7 @@ spec = timeoutHook $ describe "UserGroupSubsystem.Interpreter" do members = User.userId <$> V.fromList members } createdGroup <- createGroup (ownerId team) newUserGroup - retrievedGroup <- getGroup (ownerId team) createdGroup.id_ + retrievedGroup <- getGroup (ownerId team) createdGroup.id_ False now <- toUTCTimeMillis <$> get let assert = createdGroup.name === newUserGroupName @@ -238,7 +238,7 @@ spec = timeoutHook $ describe "UserGroupSubsystem.Interpreter" do . runDependencies (allUsers team) (galleyTeam team) . interpretUserGroupSubsystem $ do - mGroup <- getGroup (ownerId team) groupId + mGroup <- getGroup (ownerId team) groupId False pure $ mGroup === Nothing prop "team admins can get all groups in their team; outsiders can see nothing" $ \team otherTeam userGroupName -> @@ -253,11 +253,11 @@ spec = timeoutHook $ describe "UserGroupSubsystem.Interpreter" do } group1 <- createGroup (ownerId team) newUserGroup - getGroupAdmin <- getGroup (ownerId team) group1.id_ - getGroupOutsider <- getGroup (ownerId otherTeam) group1.id_ + getGroupAdmin <- getGroup (ownerId team) group1.id_ False + getGroupOutsider <- getGroup (ownerId otherTeam) group1.id_ False - getGroupsAdmin <- getGroups (ownerId team) (Just (userGroupNameToText userGroupName)) Nothing Nothing Nothing Nothing Nothing Nothing False - getGroupsOutsider <- try $ getGroups (ownerId otherTeam) (Just (userGroupNameToText userGroupName)) Nothing Nothing Nothing Nothing Nothing Nothing False + getGroupsAdmin <- getGroups (ownerId team) (Just (userGroupNameToText userGroupName)) Nothing Nothing Nothing Nothing Nothing Nothing False False + getGroupsOutsider <- try $ getGroups (ownerId otherTeam) (Just (userGroupNameToText userGroupName)) Nothing Nothing Nothing Nothing Nothing Nothing False False pure $ getGroupAdmin === Just group1 @@ -288,10 +288,10 @@ spec = timeoutHook $ describe "UserGroupSubsystem.Interpreter" do group1 <- createGroup (ownerId team1) newUserGroup1 group2 <- createGroup (ownerId team2) newUserGroup2 - getOwnGroup <- getGroup (ownerId team1) group1.id_ - getOtherGroup <- getGroup (ownerId team1) group2.id_ - getOwnGroups <- getGroups (ownerId team1) (Just (userGroupNameToText userGroupName1)) Nothing Nothing Nothing Nothing Nothing Nothing False - getOtherGroups <- getGroups (ownerId team1) (Just (userGroupNameToText userGroupName2)) Nothing Nothing Nothing Nothing Nothing Nothing False + getOwnGroup <- getGroup (ownerId team1) group1.id_ False + getOtherGroup <- getGroup (ownerId team1) group2.id_ False + getOwnGroups <- getGroups (ownerId team1) (Just (userGroupNameToText userGroupName1)) Nothing Nothing Nothing Nothing Nothing Nothing False False + getOtherGroups <- getGroups (ownerId team1) (Just (userGroupNameToText userGroupName2)) Nothing Nothing Nothing Nothing Nothing Nothing False False pure $ getOwnGroup === Just group1 @@ -305,10 +305,10 @@ spec = timeoutHook $ describe "UserGroupSubsystem.Interpreter" do let newGroups = [NewUserGroup (either undefined id $ userGroupNameFromText name) mempty | name <- ["1", "2", "2", "33"]] groups <- (\ng -> passTime 1 >> createGroup (ownerId team1) ng) `mapM` newGroups - get0 <- getGroups (ownerId team1) (Just "nope") Nothing Nothing Nothing Nothing Nothing Nothing False - get1 <- getGroups (ownerId team1) (Just "1") Nothing Nothing Nothing Nothing Nothing Nothing False - get2 <- getGroups (ownerId team1) (Just "2") Nothing Nothing Nothing Nothing Nothing Nothing False - get3 <- getGroups (ownerId team1) (Just "3") Nothing Nothing Nothing Nothing Nothing Nothing False + get0 <- getGroups (ownerId team1) (Just "nope") Nothing Nothing Nothing Nothing Nothing Nothing False False + get1 <- getGroups (ownerId team1) (Just "1") Nothing Nothing Nothing Nothing Nothing Nothing False False + get2 <- getGroups (ownerId team1) (Just "2") Nothing Nothing Nothing Nothing Nothing Nothing False False + get3 <- getGroups (ownerId team1) (Just "3") Nothing Nothing Nothing Nothing Nothing Nothing False False pure do get0.page `shouldBe` [] @@ -336,7 +336,7 @@ spec = timeoutHook $ describe "UserGroupSubsystem.Interpreter" do results :: [UserGroupPage] <- do let fetch mLastName mLastCreatedAt mLastGroupId = do - p <- getGroups (ownerId team1) Nothing (Just SortByCreatedAt) Nothing (Just pageSize) mLastName mLastCreatedAt mLastGroupId False + p <- getGroups (ownerId team1) Nothing (Just SortByCreatedAt) Nothing (Just pageSize) mLastName mLastCreatedAt mLastGroupId False False if length p.page < pageSizeToInt pageSize then pure [p] else do @@ -377,9 +377,9 @@ spec = timeoutHook $ describe "UserGroupSubsystem.Interpreter" do group1b <- mkGroup "1" group3b <- mkGroup "3" - sortByDefaults <- getGroups (ownerId team1) Nothing Nothing Nothing Nothing Nothing Nothing Nothing False - sortByNameDesc <- getGroups (ownerId team1) Nothing (Just SortByName) (Just Desc) Nothing Nothing Nothing Nothing False - sortByCreatedAtAsc <- getGroups (ownerId team1) Nothing (Just SortByCreatedAt) (Just Asc) Nothing Nothing Nothing Nothing False + sortByDefaults <- getGroups (ownerId team1) Nothing Nothing Nothing Nothing Nothing Nothing Nothing False False + sortByNameDesc <- getGroups (ownerId team1) Nothing (Just SortByName) (Just Desc) Nothing Nothing Nothing Nothing False False + sortByCreatedAtAsc <- getGroups (ownerId team1) Nothing (Just SortByCreatedAt) (Just Asc) Nothing Nothing Nothing Nothing False False let expectSortByDefaults = [[group1b, group2b, group3b], [group1a, group2a, group3a]] expectSortByNameDesc = [[group3a, group3b], [group2a, group2b], [group1a, group1b]] @@ -409,9 +409,9 @@ spec = timeoutHook $ describe "UserGroupSubsystem.Interpreter" do . interpretUserGroupSubsystem $ do ug0 :: UserGroup <- createGroup (ownerId team) (NewUserGroup originalName mempty) - ug1 :: Maybe UserGroup <- getGroup (ownerId team) ug0.id_ + ug1 :: Maybe UserGroup <- getGroup (ownerId team) ug0.id_ False updateGroup (ownerId team) ug0.id_ userGroupUpdate - ug2 :: Maybe UserGroup <- getGroup (ownerId team) ug0.id_ + ug2 :: Maybe UserGroup <- getGroup (ownerId team) ug0.id_ False pure $ (ug1 === Just ug0) .&&. (ug2 === Just (ug0 {name = userGroupUpdate.name} :: UserGroup)) @@ -475,9 +475,9 @@ spec = timeoutHook $ describe "UserGroupSubsystem.Interpreter" do ug <- createGroup (ownerId team) (NewUserGroup name mempty) ug2 <- createGroup (ownerId team) (NewUserGroup name2 mempty) - mUg <- getGroup (ownerId team) ug.id_ - isDeleted <- isNothing <$> (deleteGroup (ownerId team) ug.id_ >> getGroup (ownerId team) ug.id_) - mUg2 <- getGroup (ownerId team) ug2.id_ + mUg <- getGroup (ownerId team) ug.id_ False + isDeleted <- isNothing <$> (deleteGroup (ownerId team) ug.id_ >> getGroup (ownerId team) ug.id_ False) + mUg2 <- getGroup (ownerId team) ug2.id_ False e1 <- catchExpectedError $ deleteGroup (ownerId team2) ug.id_ e2 <- catchExpectedError $ deleteGroup (ownerId team) (Id UUID.nil) @@ -544,16 +544,16 @@ spec = timeoutHook $ describe "UserGroupSubsystem.Interpreter" do ug :: UserGroup <- createGroup (ownerId team) (NewUserGroup newGroupName mempty) addUser (ownerId team) ug.id_ (User.userId mbr1) - ugWithFirst <- getGroup (ownerId team) ug.id_ + ugWithFirst <- getGroup (ownerId team) ug.id_ False addUser (ownerId team) ug.id_ (User.userId mbr1) - ugWithIdemP <- getGroup (ownerId team) ug.id_ + ugWithIdemP <- getGroup (ownerId team) ug.id_ False addUser (ownerId team) ug.id_ (User.userId mbr2) - ugWithSecond <- getGroup (ownerId team) ug.id_ + ugWithSecond <- getGroup (ownerId team) ug.id_ False removeUser (ownerId team) ug.id_ (User.userId mbr1) - ugWithoutFirst <- getGroup (ownerId team) ug.id_ + ugWithoutFirst <- getGroup (ownerId team) ug.id_ False removeUser (ownerId team) ug.id_ (User.userId mbr1) -- idemp let propertyCheck = ((.members) <$> ugWithFirst) === Just (Identity $ V.fromList [User.userId mbr1]) diff --git a/postgres-schema.sql b/postgres-schema.sql index 9075fe312f..5c3fea345a 100644 --- a/postgres-schema.sql +++ b/postgres-schema.sql @@ -104,6 +104,7 @@ CREATE TABLE public.user_group_channel ( ); + ALTER TABLE public.user_group_channel OWNER TO "wire-server"; -- diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index 7ca8f6b1a0..96f7a8a637 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -1678,7 +1678,8 @@ createUserGroup :: (_) => Local UserId -> NewUserGroup -> Handler r UserGroup createUserGroup lusr newUserGroup = lift . liftSem $ UserGroup.createGroup (tUnqualified lusr) newUserGroup getUserGroup :: (_) => Local UserId -> UserGroupId -> Bool -> Handler r (Maybe UserGroup) -getUserGroup lusr ugid _ = lift . liftSem $ UserGroup.getGroup (tUnqualified lusr) ugid +getUserGroup lusr ugid includeChannels = + lift . liftSem $ UserGroup.getGroup (tUnqualified lusr) ugid includeChannels getUserGroups :: (_) => @@ -1691,9 +1692,10 @@ getUserGroups :: Maybe UTCTimeMillis -> Maybe UserGroupId -> Bool -> + Bool -> Handler r UserGroupPage -getUserGroups lusr q sortByKeys sortOrder pSize mLastName mLastCreatedAt mLastId includeMemberCount = - lift . liftSem $ UserGroup.getGroups (tUnqualified lusr) q sortByKeys sortOrder pSize mLastName mLastCreatedAt mLastId includeMemberCount +getUserGroups lusr q sortByKeys sortOrder pSize mLastName mLastCreatedAt mLastId includeChannels includeMemberCount = + lift . liftSem $ UserGroup.getGroups (tUnqualified lusr) q sortByKeys sortOrder pSize mLastName mLastCreatedAt mLastId includeMemberCount includeChannels updateUserGroup :: (_) => Local UserId -> UserGroupId -> UserGroupUpdate -> (Handler r) () updateUserGroup lusr gid gupd = lift . liftSem $ UserGroup.updateGroup (tUnqualified lusr) gid gupd