From 220e70ca918ec9677dc90c1b0a169ed99c518f94 Mon Sep 17 00:00:00 2001 From: Gautier DI FOLCO Date: Thu, 31 Jul 2025 17:43:56 +0200 Subject: [PATCH 1/9] WPB-18191: Add route to collaborator permissions from team --- changelog.d/2-features/WPB-18191 | 1 + integration/test/API/Brig.hs | 8 ++++ integration/test/Test/TeamCollaborators.hs | 30 ++++++++++++++ libs/wire-api/src/Wire/API/Event/Team.hs | 14 +++++++ .../src/Wire/API/Routes/Public/Brig.hs | 11 +++++ libs/wire-api/src/Wire/API/Team/Member.hs | 2 + .../src/Wire/TeamCollaboratorsStore.hs | 1 + .../Wire/TeamCollaboratorsStore/Postgres.hs | 28 +++++++++++++ .../src/Wire/TeamCollaboratorsSubsystem.hs | 1 + .../TeamCollaboratorsSubsystem/Interpreter.hs | 41 +++++++++++++++++++ .../TeamCollaboratorsStore.hs | 6 +++ services/brig/src/Brig/Team/API.hs | 1 + 12 files changed, 144 insertions(+) create mode 100644 changelog.d/2-features/WPB-18191 diff --git a/changelog.d/2-features/WPB-18191 b/changelog.d/2-features/WPB-18191 new file mode 100644 index 0000000000..af814fe8c3 --- /dev/null +++ b/changelog.d/2-features/WPB-18191 @@ -0,0 +1 @@ +Allow member permissions to be updated in a team. diff --git a/integration/test/API/Brig.hs b/integration/test/API/Brig.hs index b7ef7bc465..64222269b5 100644 --- a/integration/test/API/Brig.hs +++ b/integration/test/API/Brig.hs @@ -1184,6 +1184,14 @@ refreshAppCookie u tid appId = do req <- baseRequest u Brig Versioned $ joinHttpPath ["teams", tid, "apps", appId, "cookies"] submit "POST" req +updateTeamCollaborator :: (MakesValue owner, MakesValue collaborator, HasCallStack) => owner -> String -> collaborator -> [String] -> App Response +updateTeamCollaborator owner tid collaborator permissions = do + (_, collabId) <- objQid collaborator + req <- baseRequest owner Brig Versioned $ joinHttpPath ["teams", tid, "collaborators", collabId] + submit "PUT" $ + req + & addJSON permissions + removeTeamCollaborator :: (MakesValue owner, MakesValue collaborator, HasCallStack) => owner -> String -> collaborator -> App Response removeTeamCollaborator owner tid collaborator = do (_, collabId) <- objQid collaborator diff --git a/integration/test/Test/TeamCollaborators.hs b/integration/test/Test/TeamCollaborators.hs index 9e8050d177..00d4e259bd 100644 --- a/integration/test/Test/TeamCollaborators.hs +++ b/integration/test/Test/TeamCollaborators.hs @@ -271,3 +271,33 @@ testRemoveCollaboratorInTeamConversation = do resp.status `shouldMatchInt` 200 otherMembers <- asList (resp.json %. "members.others") traverse (%. "qualified_id") otherMembers `shouldMatchSet` traverse (%. "qualified_id") [owner, alice] + +testUpdateMember :: (HasCallStack) => App () +testUpdateMember = do + (owner, team, [alice]) <- createTeam OwnDomain 2 + + -- At the time of writing, it wasn't clear if this should be a bot instead. + bob <- randomUser OwnDomain def + addTeamCollaborator + owner + team + bob + ["implicit_connection"] + >>= assertSuccess + postOne2OneConversation bob alice team "chit-chat" >>= assertSuccess + + updateTeamCollaborator + owner + team + bob + ["create_team_conversation", "implicit_connection"] + >>= assertSuccess + postOne2OneConversation bob alice team "chit-chat" >>= assertSuccess + + updateTeamCollaborator + owner + team + bob + [] + >>= assertSuccess + postOne2OneConversation bob alice team "chit-chat" >>= assertLabel 403 "operation-denied" diff --git a/libs/wire-api/src/Wire/API/Event/Team.hs b/libs/wire-api/src/Wire/API/Event/Team.hs index 70a52ea822..1ea37463a4 100644 --- a/libs/wire-api/src/Wire/API/Event/Team.hs +++ b/libs/wire-api/src/Wire/API/Event/Team.hs @@ -125,6 +125,7 @@ data EventType | ConvDelete | CollaboratorAdd | AppCreate + | CollaboratorUpdate | CollaboratorRemove deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform EventType) @@ -144,6 +145,7 @@ instance ToSchema EventType where element "team.conversation-delete" ConvDelete, element "team.collaborator-add" CollaboratorAdd, element "team.app-create" AppCreate, + element "team.collaborator-update" CollaboratorUpdate, element "team.collaborator-remove" CollaboratorRemove ] @@ -161,6 +163,7 @@ data EventData | EdConvDelete ConvId | EdCollaboratorAdd UserId [CollaboratorPermission] | EdAppCreate UserId + | EdCollaboratorUpdate UserId [CollaboratorPermission] | EdCollaboratorRemove UserId deriving stock (Eq, Show, Generic) @@ -192,6 +195,11 @@ instance ToJSON EventData where "permissions" A..= perms ] toJSON (EdAppCreate usr) = A.object ["user" A..= usr] + toJSON (EdCollaboratorUpdate usr perms) = + A.object + [ "user" A..= usr, + "permissions" A..= perms + ] toJSON (EdCollaboratorRemove usr) = A.object ["user" A..= usr] eventDataType :: EventData -> EventType @@ -205,6 +213,7 @@ eventDataType (EdConvCreate _) = ConvCreate eventDataType (EdConvDelete _) = ConvDelete eventDataType (EdCollaboratorAdd _ _) = CollaboratorAdd eventDataType (EdAppCreate _) = AppCreate +eventDataType (EdCollaboratorUpdate _ _) = CollaboratorUpdate eventDataType (EdCollaboratorRemove _) = CollaboratorRemove parseEventData :: EventType -> Maybe Value -> Parser EventData @@ -240,6 +249,10 @@ parseEventData AppCreate Nothing = fail "missing event data for type 'team.app-c parseEventData AppCreate (Just j) = do let f o = EdAppCreate <$> o .: "user" withObject "app create data" f j +parseEventData CollaboratorUpdate Nothing = fail "missing event data for type 'team.collaborator-update" +parseEventData CollaboratorUpdate (Just j) = do + let f o = EdCollaboratorUpdate <$> o .: "user" <*> o .: "permissions" + withObject "collaborator update data" f j parseEventData CollaboratorRemove Nothing = fail "missing event data for type 'team.collaborator-remove" parseEventData CollaboratorRemove (Just j) = do let f o = EdCollaboratorRemove <$> o .: "user" @@ -259,6 +272,7 @@ genEventData = \case ConvDelete -> EdConvDelete <$> arbitrary CollaboratorAdd -> EdCollaboratorAdd <$> arbitrary <*> arbitrary AppCreate -> EdAppCreate <$> arbitrary + CollaboratorUpdate -> EdCollaboratorUpdate <$> arbitrary <*> arbitrary CollaboratorRemove -> EdCollaboratorRemove <$> arbitrary makeLenses ''Event 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 b54f9db2a8..f30adb2190 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs @@ -2062,6 +2062,17 @@ type TeamsAPI = :> MultiVerb1 'POST '[JSON] (RespondEmpty 200 "") ) :<|> Named + "update-team-collaborator" + ( Summary "Update a collaborator permissions from the team." + :> From 'V11 + :> ZLocalUser + :> "teams" + :> Capture "tid" TeamId + :> "collaborators" + :> Capture "uid" UserId + :> ReqBody '[JSON] (Set CollaboratorPermission) + :> MultiVerb1 'PUT '[JSON] (RespondEmpty 200 "") + ) "get-team-collaborators" ( Summary "Get all collaborators of the team." :> From 'V10 diff --git a/libs/wire-api/src/Wire/API/Team/Member.hs b/libs/wire-api/src/Wire/API/Team/Member.hs index f6a75b1ebc..8fd62ad342 100644 --- a/libs/wire-api/src/Wire/API/Team/Member.hs +++ b/libs/wire-api/src/Wire/API/Team/Member.hs @@ -482,6 +482,7 @@ data HiddenPerm | JoinRegularConversations | CreateApp | ManageApps + | UpdateTeamCollaborator | RemoveTeamCollaborator deriving (Eq, Ord, Show) @@ -570,6 +571,7 @@ roleHiddenPermissions role = HiddenPermissions p p NewTeamCollaborator, CreateApp, ManageApps, + UpdateTeamCollaborator, RemoveTeamCollaborator ] roleHiddenPerms RoleMember = diff --git a/libs/wire-subsystems/src/Wire/TeamCollaboratorsStore.hs b/libs/wire-subsystems/src/Wire/TeamCollaboratorsStore.hs index 55a0e4658d..0e0b16ec7f 100644 --- a/libs/wire-subsystems/src/Wire/TeamCollaboratorsStore.hs +++ b/libs/wire-subsystems/src/Wire/TeamCollaboratorsStore.hs @@ -13,6 +13,7 @@ data TeamCollaboratorsStore m a where GetTeamCollaborator :: TeamId -> UserId -> TeamCollaboratorsStore m (Maybe TeamCollaborator) GetTeamCollaborations :: UserId -> TeamCollaboratorsStore m ([TeamCollaborator]) GetTeamCollaboratorsWithIds :: Set TeamId -> Set UserId -> TeamCollaboratorsStore m [TeamCollaborator] + UpdateTeamCollaborator :: UserId -> TeamId -> Set CollaboratorPermission -> TeamCollaboratorsStore m () RemoveTeamCollaborator :: UserId -> TeamId -> TeamCollaboratorsStore m () makeSem ''TeamCollaboratorsStore diff --git a/libs/wire-subsystems/src/Wire/TeamCollaboratorsStore/Postgres.hs b/libs/wire-subsystems/src/Wire/TeamCollaboratorsStore/Postgres.hs index cb9b3b4180..95d3b6d25f 100644 --- a/libs/wire-subsystems/src/Wire/TeamCollaboratorsStore/Postgres.hs +++ b/libs/wire-subsystems/src/Wire/TeamCollaboratorsStore/Postgres.hs @@ -38,6 +38,7 @@ interpretTeamCollaboratorsStoreToPostgres = GetTeamCollaborator teamId userId -> getTeamCollaboratorImpl teamId userId GetTeamCollaborations userId -> getTeamCollaborationsImpl userId GetTeamCollaboratorsWithIds teamIds userIds -> getTeamCollaboratorsWithIdsImpl teamIds userIds + UpdateTeamCollaborator userId teamId permissions -> updateTeamCollaboratorImpl userId teamId permissions RemoveTeamCollaborator userId teamId -> removeTeamCollaboratorImpl userId teamId getTeamCollaboratorImpl :: @@ -125,6 +126,33 @@ getAllTeamCollaboratorsImpl teamId = do select user_id :: uuid, team_id :: uuid, permissions :: int2[] from collaborators where team_id = ($1 :: uuid) |] +updateTeamCollaboratorImpl :: + ( Member (Input Pool) r, + Member (Embed IO) r, + Member (Error UsageError) r + ) => + UserId -> + TeamId -> + Set CollaboratorPermission -> + Sem r () +updateTeamCollaboratorImpl userId teamId permissions = do + pool <- input + eitherErrorOrUnit <- liftIO $ use pool session + either throw pure eitherErrorOrUnit + where + session :: Session () + session = statement (userId, teamId, permissions) updateStatement + + updateStatement :: Statement (UserId, TeamId, Set CollaboratorPermission) () + updateStatement = + lmap + ( \(uid, tid, pms) -> + (toUUID uid, toUUID tid, collaboratorPermissionToPostgreslRep <$> (Data.Vector.fromList . toAscList) pms) + ) + $ [resultlessStatement| + update collaborators set permissions = ($3 :: smallint[]) where user_id = ($1 :: uuid) and team_id = ($2 :: uuid) + |] + removeTeamCollaboratorImpl :: ( Member (Input Pool) r, Member (Embed IO) r, diff --git a/libs/wire-subsystems/src/Wire/TeamCollaboratorsSubsystem.hs b/libs/wire-subsystems/src/Wire/TeamCollaboratorsSubsystem.hs index 2e57f9729f..77e06f051e 100644 --- a/libs/wire-subsystems/src/Wire/TeamCollaboratorsSubsystem.hs +++ b/libs/wire-subsystems/src/Wire/TeamCollaboratorsSubsystem.hs @@ -14,6 +14,7 @@ data TeamCollaboratorsSubsystem m a where InternalGetTeamCollaborator :: TeamId -> UserId -> TeamCollaboratorsSubsystem m (Maybe TeamCollaborator) InternalGetTeamCollaborations :: UserId -> TeamCollaboratorsSubsystem m [TeamCollaborator] InternalGetTeamCollaboratorsWithIds :: Set TeamId -> Set UserId -> TeamCollaboratorsSubsystem m [TeamCollaborator] + UpdateTeamCollaborator :: Local UserId -> UserId -> TeamId -> Set CollaboratorPermission -> TeamCollaboratorsSubsystem m () InternalRemoveTeamCollaborator :: UserId -> TeamId -> TeamCollaboratorsSubsystem m () makeSem ''TeamCollaboratorsSubsystem diff --git a/libs/wire-subsystems/src/Wire/TeamCollaboratorsSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/TeamCollaboratorsSubsystem/Interpreter.hs index 4430d129ad..033fd1044c 100644 --- a/libs/wire-subsystems/src/Wire/TeamCollaboratorsSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/TeamCollaboratorsSubsystem/Interpreter.hs @@ -34,6 +34,7 @@ interpretTeamCollaboratorsSubsystem = interpret $ \case InternalGetTeamCollaborator team user -> internalGetTeamCollaboratorImpl team user InternalGetTeamCollaborations userId -> internalGetTeamCollaborationsImpl userId InternalGetTeamCollaboratorsWithIds teams userIds -> internalGetTeamCollaboratorsWithIdsImpl teams userIds + UpdateTeamCollaborator zUser user team perms -> updateTeamCollaboratorImpl zUser user team perms InternalRemoveTeamCollaborator user team -> internalRemoveTeamCollaboratorImpl user team internalGetTeamCollaboratorImpl :: @@ -91,6 +92,46 @@ internalGetTeamCollaboratorsWithIdsImpl :: internalGetTeamCollaboratorsWithIdsImpl = do Store.getTeamCollaboratorsWithIds +updateTeamCollaboratorImpl :: + ( Member TeamSubsystem r, + Member (Error TeamCollaboratorsError) r, + Member Store.TeamCollaboratorsStore r, + Member Now r, + Member NotificationSubsystem r + ) => + Local UserId -> + UserId -> + TeamId -> + Set CollaboratorPermission -> + Sem r () +updateTeamCollaboratorImpl zUser user team perms = do + guardPermission (tUnqualified zUser) team TeamMember.UpdateTeamCollaborator InsufficientRights + Store.updateTeamCollaborator user team perms + unless (Set.member ImplicitConnection perms) $ + -- TODO gdf remove O2O conversations + pure () + + now <- get + let event = newEvent team now (EdCollaboratorUpdate user $ Set.toList perms) + teamMembersList <- internalGetTeamAdmins team + let teamMembers :: [UserId] = view TeamMember.userId <$> (teamMembersList ^. TeamMember.teamMembers) + -- TODO: Review the event's values + pushNotifications + [ def + { origin = Just (tUnqualified zUser), + json = toJSONObject $ event, + recipients = + ( \uid -> + Recipient + { recipientUserId = uid, + recipientClients = Push.RecipientClientsAll + } + ) + <$> teamMembers, + transient = False + } + ] + internalRemoveTeamCollaboratorImpl :: ( Member Store.TeamCollaboratorsStore r ) => diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/TeamCollaboratorsStore.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/TeamCollaboratorsStore.hs index fab46b025d..c8855eb3ab 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/TeamCollaboratorsStore.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/TeamCollaboratorsStore.hs @@ -27,5 +27,11 @@ inMemoryTeamCollaboratorsStoreInterpreter = GetTeamCollaboratorsWithIds teamIds userIds -> gets $ \(s :: Map TeamId [TeamCollaborator]) -> concatMap (concatMap (filter (\tc -> tc.gUser `elem` userIds)) . (\(tid :: TeamId) -> Map.lookup tid s)) teamIds + UpdateTeamCollaborator userId teamId permissions -> + let updatePermissions teamCollaborator = + if teamCollaborator.gUser == userId + then teamCollaborator {gPermissions = permissions} + else teamCollaborator + in modify $ Map.adjust (fmap updatePermissions) teamId RemoveTeamCollaborator userId teamId -> modify $ Map.alter (fmap $ filter $ (/= userId) . gUser) teamId diff --git a/services/brig/src/Brig/Team/API.hs b/services/brig/src/Brig/Team/API.hs index d9d375f157..115cb05622 100644 --- a/services/brig/src/Brig/Team/API.hs +++ b/services/brig/src/Brig/Team/API.hs @@ -114,6 +114,7 @@ servantAPI = :<|> Named @"get-team-size" (\uid tid -> lift . liftSem $ teamSizePublic uid tid) :<|> Named @"accept-team-invitation" (\luid req -> lift $ liftSem $ acceptTeamInvitation luid req.password req.code) :<|> Named @"add-team-collaborator" (\zuid tid (NewTeamCollaborator uid perms) -> lift . liftSem $ createTeamCollaborator zuid uid tid perms) + :<|> Named @"update-team-collaborator" (\zuid tid uid perms -> lift . liftSem $ updateTeamCollaborator zuid uid tid perms) :<|> Named @"get-team-collaborators" (\zuid tid -> lift . liftSem $ getAllTeamCollaborators zuid tid) teamSizePublic :: From b52ba65cdcd6223d7af66ee5e3f57fe55da7f985 Mon Sep 17 00:00:00 2001 From: Gautier DI FOLCO Date: Mon, 4 Aug 2025 08:58:49 +0200 Subject: [PATCH 2/9] trigger conversation closing --- .../src/Wire/TeamCollaboratorsSubsystem/Interpreter.hs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libs/wire-subsystems/src/Wire/TeamCollaboratorsSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/TeamCollaboratorsSubsystem/Interpreter.hs index 033fd1044c..ae3b98b4d2 100644 --- a/libs/wire-subsystems/src/Wire/TeamCollaboratorsSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/TeamCollaboratorsSubsystem/Interpreter.hs @@ -97,7 +97,8 @@ updateTeamCollaboratorImpl :: Member (Error TeamCollaboratorsError) r, Member Store.TeamCollaboratorsStore r, Member Now r, - Member NotificationSubsystem r + Member NotificationSubsystem r, + Member ConversationsSubsystem r ) => Local UserId -> UserId -> @@ -107,9 +108,8 @@ updateTeamCollaboratorImpl :: updateTeamCollaboratorImpl zUser user team perms = do guardPermission (tUnqualified zUser) team TeamMember.UpdateTeamCollaborator InsufficientRights Store.updateTeamCollaborator user team perms - unless (Set.member ImplicitConnection perms) $ - -- TODO gdf remove O2O conversations - pure () + when (Set.null $ Set.intersection (Set.fromList [CreateTeamConversation, ImplicitConnection]) perms) $ + internalCloseConversationsFrom team user now <- get let event = newEvent team now (EdCollaboratorUpdate user $ Set.toList perms) From 1180f6b7afc8d2289d9d387539fbf3c1ee054dd1 Mon Sep 17 00:00:00 2001 From: Gautier DI FOLCO Date: Tue, 5 Aug 2025 17:34:24 +0200 Subject: [PATCH 3/9] fix formating --- .../test/unit/Wire/MockInterpreters/TeamCollaboratorsStore.hs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/TeamCollaboratorsStore.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/TeamCollaboratorsStore.hs index c8855eb3ab..0cdf3a3406 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/TeamCollaboratorsStore.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/TeamCollaboratorsStore.hs @@ -30,8 +30,8 @@ inMemoryTeamCollaboratorsStoreInterpreter = UpdateTeamCollaborator userId teamId permissions -> let updatePermissions teamCollaborator = if teamCollaborator.gUser == userId - then teamCollaborator {gPermissions = permissions} - else teamCollaborator + then teamCollaborator {gPermissions = permissions} + else teamCollaborator in modify $ Map.adjust (fmap updatePermissions) teamId RemoveTeamCollaborator userId teamId -> modify $ Map.alter (fmap $ filter $ (/= userId) . gUser) teamId From 1ca84b2e96276364a60e31c424cad94321cf99c3 Mon Sep 17 00:00:00 2001 From: Gautier DI FOLCO Date: Fri, 5 Sep 2025 12:21:31 +0200 Subject: [PATCH 4/9] fix: rebase --- .../src/Wire/TeamCollaboratorsSubsystem/Interpreter.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/wire-subsystems/src/Wire/TeamCollaboratorsSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/TeamCollaboratorsSubsystem/Interpreter.hs index ae3b98b4d2..6cf4992ae6 100644 --- a/libs/wire-subsystems/src/Wire/TeamCollaboratorsSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/TeamCollaboratorsSubsystem/Interpreter.hs @@ -109,7 +109,7 @@ updateTeamCollaboratorImpl zUser user team perms = do guardPermission (tUnqualified zUser) team TeamMember.UpdateTeamCollaborator InsufficientRights Store.updateTeamCollaborator user team perms when (Set.null $ Set.intersection (Set.fromList [CreateTeamConversation, ImplicitConnection]) perms) $ - internalCloseConversationsFrom team user + internalLeavingConversationsFrom team user now <- get let event = newEvent team now (EdCollaboratorUpdate user $ Set.toList perms) From afcb9d683035974112db0c9419e44f8ae2fda9b2 Mon Sep 17 00:00:00 2001 From: Gautier DI FOLCO Date: Fri, 5 Sep 2025 16:01:05 +0200 Subject: [PATCH 5/9] fix: add perform-ability to leave --- .../TeamCollaboratorsSubsystem/Interpreter.hs | 56 +++++++++++++------ 1 file changed, 38 insertions(+), 18 deletions(-) diff --git a/libs/wire-subsystems/src/Wire/TeamCollaboratorsSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/TeamCollaboratorsSubsystem/Interpreter.hs index 6cf4992ae6..a802bbe126 100644 --- a/libs/wire-subsystems/src/Wire/TeamCollaboratorsSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/TeamCollaboratorsSubsystem/Interpreter.hs @@ -108,29 +108,49 @@ updateTeamCollaboratorImpl :: updateTeamCollaboratorImpl zUser user team perms = do guardPermission (tUnqualified zUser) team TeamMember.UpdateTeamCollaborator InsufficientRights Store.updateTeamCollaborator user team perms - when (Set.null $ Set.intersection (Set.fromList [CreateTeamConversation, ImplicitConnection]) perms) $ - internalLeavingConversationsFrom team user now <- get - let event = newEvent team now (EdCollaboratorUpdate user $ Set.toList perms) teamMembersList <- internalGetTeamAdmins team let teamMembers :: [UserId] = view TeamMember.userId <$> (teamMembersList ^. TeamMember.teamMembers) + + extraNotifications <- + if Set.null $ Set.intersection (Set.fromList [CreateTeamConversation, ImplicitConnection]) perms + then do + leavingConversations <- internalLeaveConversationsFrom team user + pure $ + leavingConversations.close <&> \convId -> + def + { origin = Just (tUnqualified zUser), + json = toJSONObject $ newEvent team now (EdConvDelete convId), + recipients = + ( \uid -> + Recipient + { recipientUserId = uid, + recipientClients = Push.RecipientClientsAll + } + ) + <$> teamMembers, + transient = False + } + else pure [] + + let event = newEvent team now (EdCollaboratorUpdate user $ Set.toList perms) -- TODO: Review the event's values - pushNotifications - [ def - { origin = Just (tUnqualified zUser), - json = toJSONObject $ event, - recipients = - ( \uid -> - Recipient - { recipientUserId = uid, - recipientClients = Push.RecipientClientsAll - } - ) - <$> teamMembers, - transient = False - } - ] + pushNotifications $ + def + { origin = Just (tUnqualified zUser), + json = toJSONObject $ event, + recipients = + ( \uid -> + Recipient + { recipientUserId = uid, + recipientClients = Push.RecipientClientsAll + } + ) + <$> teamMembers, + transient = False + } + : extraNotifications internalRemoveTeamCollaboratorImpl :: ( Member Store.TeamCollaboratorsStore r From 6bba6beb789d429c3000d862910002c5f051ceb1 Mon Sep 17 00:00:00 2001 From: Gautier DI FOLCO Date: Fri, 5 Sep 2025 19:03:39 +0200 Subject: [PATCH 6/9] fix: rebase --- .../src/Wire/AppSubsystem/Interpreter.hs | 2 +- .../TeamCollaboratorsSubsystem/Interpreter.hs | 39 ++----------------- .../src/Wire/TeamSubsystem/Util.hs | 17 ++++---- 3 files changed, 12 insertions(+), 46 deletions(-) diff --git a/libs/wire-subsystems/src/Wire/AppSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/AppSubsystem/Interpreter.hs index d7cf1f05ad..e6f4380526 100644 --- a/libs/wire-subsystems/src/Wire/AppSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/AppSubsystem/Interpreter.hs @@ -93,7 +93,7 @@ createAppImpl lusr tid new = do Store.createUser u Nothing -- generate a team event - generateTeamEvent creator.id tid (EdAppCreate u.id) + generateTeamEvents creator.id tid [EdAppCreate u.id] c :: Cookie (Token U) <- newCookie u.id Nothing PersistentCookie (Just "app") pure diff --git a/libs/wire-subsystems/src/Wire/TeamCollaboratorsSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/TeamCollaboratorsSubsystem/Interpreter.hs index a802bbe126..964cb7b221 100644 --- a/libs/wire-subsystems/src/Wire/TeamCollaboratorsSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/TeamCollaboratorsSubsystem/Interpreter.hs @@ -69,7 +69,7 @@ createTeamCollaboratorImpl zUser user team perms = do Store.createTeamCollaborator user team perms -- TODO: Review the event's values - generateTeamEvent (tUnqualified zUser) team (EdCollaboratorAdd user (Set.toList perms)) + generateTeamEvents (tUnqualified zUser) team [EdCollaboratorAdd user (Set.toList perms)] getAllTeamCollaboratorsImpl :: ( Member TeamSubsystem r, @@ -109,48 +109,15 @@ updateTeamCollaboratorImpl zUser user team perms = do guardPermission (tUnqualified zUser) team TeamMember.UpdateTeamCollaborator InsufficientRights Store.updateTeamCollaborator user team perms - now <- get - teamMembersList <- internalGetTeamAdmins team - let teamMembers :: [UserId] = view TeamMember.userId <$> (teamMembersList ^. TeamMember.teamMembers) - extraNotifications <- if Set.null $ Set.intersection (Set.fromList [CreateTeamConversation, ImplicitConnection]) perms then do leavingConversations <- internalLeaveConversationsFrom team user - pure $ - leavingConversations.close <&> \convId -> - def - { origin = Just (tUnqualified zUser), - json = toJSONObject $ newEvent team now (EdConvDelete convId), - recipients = - ( \uid -> - Recipient - { recipientUserId = uid, - recipientClients = Push.RecipientClientsAll - } - ) - <$> teamMembers, - transient = False - } + pure $ EdConvDelete <$> leavingConversations.close else pure [] - let event = newEvent team now (EdCollaboratorUpdate user $ Set.toList perms) -- TODO: Review the event's values - pushNotifications $ - def - { origin = Just (tUnqualified zUser), - json = toJSONObject $ event, - recipients = - ( \uid -> - Recipient - { recipientUserId = uid, - recipientClients = Push.RecipientClientsAll - } - ) - <$> teamMembers, - transient = False - } - : extraNotifications + generateTeamEvents (tUnqualified zUser) team (EdCollaboratorAdd user (Set.toList perms) : extraNotifications) internalRemoveTeamCollaboratorImpl :: ( Member Store.TeamCollaboratorsStore r diff --git a/libs/wire-subsystems/src/Wire/TeamSubsystem/Util.hs b/libs/wire-subsystems/src/Wire/TeamSubsystem/Util.hs index a7f219f976..2b1763612d 100644 --- a/libs/wire-subsystems/src/Wire/TeamSubsystem/Util.hs +++ b/libs/wire-subsystems/src/Wire/TeamSubsystem/Util.hs @@ -13,24 +13,24 @@ import Wire.NotificationSubsystem import Wire.Sem.Now import Wire.TeamSubsystem --- | Generate a team event and send it to all team admins -generateTeamEvent :: +-- | Generate team events and send them to all team admins +generateTeamEvents :: ( Member Now r, Member TeamSubsystem r, Member NotificationSubsystem r ) => UserId -> TeamId -> - EventData -> + [EventData] -> Sem r () -generateTeamEvent uid tid edata = do +generateTeamEvents uid tid eventsData = do now <- get - let event = newEvent tid now edata admins <- internalGetTeamAdmins tid - pushNotifications - [ def + pushNotifications $ + eventsData <&> \eData -> + def { origin = Just uid, - json = toJSONObject $ event, + json = toJSONObject $ newEvent tid now eData, recipients = [ Recipient { recipientUserId = u, @@ -40,4 +40,3 @@ generateTeamEvent uid tid edata = do ], transient = False } - ] From be7caa95a1b372032af76c39db3eb139bafda7e7 Mon Sep 17 00:00:00 2001 From: Gautier DI FOLCO Date: Fri, 12 Sep 2025 17:01:11 +0200 Subject: [PATCH 7/9] refactor: move to galley --- integration/test/API/Brig.hs | 15 --------- integration/test/API/Galley.hs | 14 ++++++++ integration/test/Test/TeamCollaborators.hs | 4 +-- .../src/Wire/API/Routes/Public/Brig.hs | 11 ------- .../API/Routes/Public/Galley/TeamMember.hs | 14 ++++++++ .../src/Wire/TeamCollaboratorsSubsystem.hs | 2 +- .../TeamCollaboratorsSubsystem/Interpreter.hs | 25 +++----------- services/brig/src/Brig/Team/API.hs | 1 - .../src/Galley/API/Public/TeamMember.hs | 1 + services/galley/src/Galley/API/Teams.hs | 33 +++++++++++++++++++ 10 files changed, 69 insertions(+), 51 deletions(-) diff --git a/integration/test/API/Brig.hs b/integration/test/API/Brig.hs index 64222269b5..4cfaab51de 100644 --- a/integration/test/API/Brig.hs +++ b/integration/test/API/Brig.hs @@ -1178,22 +1178,7 @@ createApp creator tid new = do "accent_id" .= new.accentId, "metadata" .= new.meta ] - refreshAppCookie :: (MakesValue u) => u -> String -> String -> App Response refreshAppCookie u tid appId = do req <- baseRequest u Brig Versioned $ joinHttpPath ["teams", tid, "apps", appId, "cookies"] submit "POST" req - -updateTeamCollaborator :: (MakesValue owner, MakesValue collaborator, HasCallStack) => owner -> String -> collaborator -> [String] -> App Response -updateTeamCollaborator owner tid collaborator permissions = do - (_, collabId) <- objQid collaborator - req <- baseRequest owner Brig Versioned $ joinHttpPath ["teams", tid, "collaborators", collabId] - submit "PUT" $ - req - & addJSON permissions - -removeTeamCollaborator :: (MakesValue owner, MakesValue collaborator, HasCallStack) => owner -> String -> collaborator -> App Response -removeTeamCollaborator owner tid collaborator = do - (_, collabId) <- objQid collaborator - req <- baseRequest owner Galley Versioned $ joinHttpPath ["teams", tid, "collaborators", collabId] - submit "DELETE" req diff --git a/integration/test/API/Galley.hs b/integration/test/API/Galley.hs index 556aac162a..dcdb82fb4f 100644 --- a/integration/test/API/Galley.hs +++ b/integration/test/API/Galley.hs @@ -867,3 +867,17 @@ resetConversation user groupId epoch = do req <- baseRequest user Galley Versioned (joinHttpPath ["mls", "reset-conversation"]) let payload = object ["group_id" .= groupId, "epoch" .= epoch] submit "POST" $ req & addJSON payload + +updateTeamCollaborator :: (MakesValue owner, MakesValue collaborator, HasCallStack) => owner -> String -> collaborator -> [String] -> App Response +updateTeamCollaborator owner tid collaborator permissions = do + (_, collabId) <- objQid collaborator + req <- baseRequest owner Galley Versioned $ joinHttpPath ["teams", tid, "collaborators", collabId] + submit "PUT" + $ req + & addJSON permissions + +removeTeamCollaborator :: (MakesValue owner, MakesValue collaborator, HasCallStack) => owner -> String -> collaborator -> App Response +removeTeamCollaborator owner tid collaborator = do + (_, collabId) <- objQid collaborator + req <- baseRequest owner Galley Versioned $ joinHttpPath ["teams", tid, "collaborators", collabId] + submit "DELETE" req diff --git a/integration/test/Test/TeamCollaborators.hs b/integration/test/Test/TeamCollaborators.hs index 00d4e259bd..b892c3aa05 100644 --- a/integration/test/Test/TeamCollaborators.hs +++ b/integration/test/Test/TeamCollaborators.hs @@ -272,8 +272,8 @@ testRemoveCollaboratorInTeamConversation = do otherMembers <- asList (resp.json %. "members.others") traverse (%. "qualified_id") otherMembers `shouldMatchSet` traverse (%. "qualified_id") [owner, alice] -testUpdateMember :: (HasCallStack) => App () -testUpdateMember = do +testUpdateCollaborator :: (HasCallStack) => App () +testUpdateCollaborator = do (owner, team, [alice]) <- createTeam OwnDomain 2 -- At the time of writing, it wasn't clear if this should be a bot instead. 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 f30adb2190..b54f9db2a8 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs @@ -2062,17 +2062,6 @@ type TeamsAPI = :> MultiVerb1 'POST '[JSON] (RespondEmpty 200 "") ) :<|> Named - "update-team-collaborator" - ( Summary "Update a collaborator permissions from the team." - :> From 'V11 - :> ZLocalUser - :> "teams" - :> Capture "tid" TeamId - :> "collaborators" - :> Capture "uid" UserId - :> ReqBody '[JSON] (Set CollaboratorPermission) - :> MultiVerb1 'PUT '[JSON] (RespondEmpty 200 "") - ) "get-team-collaborators" ( Summary "Get all collaborators of the team." :> From 'V10 diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/TeamMember.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/TeamMember.hs index f3aee85819..86ed3bd25f 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/TeamMember.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/TeamMember.hs @@ -20,6 +20,7 @@ module Wire.API.Routes.Public.Galley.TeamMember where import Data.Id import Data.Int import Data.Range +import Data.Set (Set) import GHC.Generics import Generics.SOP qualified as GSOP import Servant @@ -32,6 +33,7 @@ import Wire.API.Routes.MultiVerb import Wire.API.Routes.Named import Wire.API.Routes.Public import Wire.API.Routes.Version +import Wire.API.Team.Collaborator import Wire.API.Team.Member import Wire.API.User qualified as User @@ -207,6 +209,18 @@ type TeamMemberAPI = "CSV of team members" CSV ) + :<|> Named + "update-team-collaborator" + ( Summary "Update a collaborator permissions from the team." + :> From 'V11 + :> ZLocalUser + :> "teams" + :> Capture "tid" TeamId + :> "collaborators" + :> Capture "uid" UserId + :> ReqBody '[JSON] (Set CollaboratorPermission) + :> MultiVerb1 'PUT '[JSON] (RespondEmpty 200 "") + ) :<|> Named "remove-team-collaborator" ( Summary "Remove a collaborator from the team." diff --git a/libs/wire-subsystems/src/Wire/TeamCollaboratorsSubsystem.hs b/libs/wire-subsystems/src/Wire/TeamCollaboratorsSubsystem.hs index 77e06f051e..6b4e680d4d 100644 --- a/libs/wire-subsystems/src/Wire/TeamCollaboratorsSubsystem.hs +++ b/libs/wire-subsystems/src/Wire/TeamCollaboratorsSubsystem.hs @@ -14,7 +14,7 @@ data TeamCollaboratorsSubsystem m a where InternalGetTeamCollaborator :: TeamId -> UserId -> TeamCollaboratorsSubsystem m (Maybe TeamCollaborator) InternalGetTeamCollaborations :: UserId -> TeamCollaboratorsSubsystem m [TeamCollaborator] InternalGetTeamCollaboratorsWithIds :: Set TeamId -> Set UserId -> TeamCollaboratorsSubsystem m [TeamCollaborator] - UpdateTeamCollaborator :: Local UserId -> UserId -> TeamId -> Set CollaboratorPermission -> TeamCollaboratorsSubsystem m () + InternalUpdateTeamCollaborator :: UserId -> TeamId -> Set CollaboratorPermission -> TeamCollaboratorsSubsystem m () InternalRemoveTeamCollaborator :: UserId -> TeamId -> TeamCollaboratorsSubsystem m () makeSem ''TeamCollaboratorsSubsystem diff --git a/libs/wire-subsystems/src/Wire/TeamCollaboratorsSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/TeamCollaboratorsSubsystem/Interpreter.hs index 964cb7b221..dacdf4da78 100644 --- a/libs/wire-subsystems/src/Wire/TeamCollaboratorsSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/TeamCollaboratorsSubsystem/Interpreter.hs @@ -34,7 +34,7 @@ interpretTeamCollaboratorsSubsystem = interpret $ \case InternalGetTeamCollaborator team user -> internalGetTeamCollaboratorImpl team user InternalGetTeamCollaborations userId -> internalGetTeamCollaborationsImpl userId InternalGetTeamCollaboratorsWithIds teams userIds -> internalGetTeamCollaboratorsWithIdsImpl teams userIds - UpdateTeamCollaborator zUser user team perms -> updateTeamCollaboratorImpl zUser user team perms + InternalUpdateTeamCollaborator user team perms -> internalUpdateTeamCollaboratorImpl user team perms InternalRemoveTeamCollaborator user team -> internalRemoveTeamCollaboratorImpl user team internalGetTeamCollaboratorImpl :: @@ -92,33 +92,16 @@ internalGetTeamCollaboratorsWithIdsImpl :: internalGetTeamCollaboratorsWithIdsImpl = do Store.getTeamCollaboratorsWithIds -updateTeamCollaboratorImpl :: - ( Member TeamSubsystem r, - Member (Error TeamCollaboratorsError) r, - Member Store.TeamCollaboratorsStore r, - Member Now r, - Member NotificationSubsystem r, - Member ConversationsSubsystem r +internalUpdateTeamCollaboratorImpl :: + ( Member Store.TeamCollaboratorsStore r ) => - Local UserId -> UserId -> TeamId -> Set CollaboratorPermission -> Sem r () -updateTeamCollaboratorImpl zUser user team perms = do - guardPermission (tUnqualified zUser) team TeamMember.UpdateTeamCollaborator InsufficientRights +internalUpdateTeamCollaboratorImpl user team perms = do Store.updateTeamCollaborator user team perms - extraNotifications <- - if Set.null $ Set.intersection (Set.fromList [CreateTeamConversation, ImplicitConnection]) perms - then do - leavingConversations <- internalLeaveConversationsFrom team user - pure $ EdConvDelete <$> leavingConversations.close - else pure [] - - -- TODO: Review the event's values - generateTeamEvents (tUnqualified zUser) team (EdCollaboratorAdd user (Set.toList perms) : extraNotifications) - internalRemoveTeamCollaboratorImpl :: ( Member Store.TeamCollaboratorsStore r ) => diff --git a/services/brig/src/Brig/Team/API.hs b/services/brig/src/Brig/Team/API.hs index 115cb05622..d9d375f157 100644 --- a/services/brig/src/Brig/Team/API.hs +++ b/services/brig/src/Brig/Team/API.hs @@ -114,7 +114,6 @@ servantAPI = :<|> Named @"get-team-size" (\uid tid -> lift . liftSem $ teamSizePublic uid tid) :<|> Named @"accept-team-invitation" (\luid req -> lift $ liftSem $ acceptTeamInvitation luid req.password req.code) :<|> Named @"add-team-collaborator" (\zuid tid (NewTeamCollaborator uid perms) -> lift . liftSem $ createTeamCollaborator zuid uid tid perms) - :<|> Named @"update-team-collaborator" (\zuid tid uid perms -> lift . liftSem $ updateTeamCollaborator zuid uid tid perms) :<|> Named @"get-team-collaborators" (\zuid tid -> lift . liftSem $ getAllTeamCollaborators zuid tid) teamSizePublic :: diff --git a/services/galley/src/Galley/API/Public/TeamMember.hs b/services/galley/src/Galley/API/Public/TeamMember.hs index 549f3a750a..d85d89f514 100644 --- a/services/galley/src/Galley/API/Public/TeamMember.hs +++ b/services/galley/src/Galley/API/Public/TeamMember.hs @@ -33,4 +33,5 @@ teamMemberAPI = <@> mkNamedAPI @"delete-non-binding-team-member" deleteNonBindingTeamMember <@> mkNamedAPI @"update-team-member" updateTeamMember <@> mkNamedAPI @"get-team-members-csv" Export.getTeamMembersCSV + <@> mkNamedAPI @"update-team-collaborator" updateTeamCollaborator <@> mkNamedAPI @"remove-team-collaborator" removeTeamCollaborator diff --git a/services/galley/src/Galley/API/Teams.hs b/services/galley/src/Galley/API/Teams.hs index 652c6e1b3b..0d3b2cc4da 100644 --- a/services/galley/src/Galley/API/Teams.hs +++ b/services/galley/src/Galley/API/Teams.hs @@ -52,6 +52,7 @@ module Galley.API.Teams ensureNotTooLargeForLegalHold, ensureNotTooLargeToActivateLegalHold, internalDeleteBindingTeam, + updateTeamCollaborator, removeTeamCollaborator, ) where @@ -117,6 +118,8 @@ import Wire.API.Routes.MultiTablePaging (MultiTablePage (..), MultiTablePagingSt import Wire.API.Routes.Public.Galley.TeamMember import Wire.API.Team import Wire.API.Team qualified as Public +import Wire.API.Team.Collaborator qualified as Collaborator +import Wire.API.Team.Collaborator qualified as TeamCollaborator import Wire.API.Team.Conversation import Wire.API.Team.Conversation qualified as Public import Wire.API.Team.Feature @@ -1305,6 +1308,36 @@ checkAdminLimit adminCount = when (adminCount > 2000) $ throwS @'TooManyTeamAdmins +-- | Updating a team collaborator permissions eventually cleaning their conversations +updateTeamCollaborator :: + forall r. + ( Member BackendNotificationQueueAccess r, + Member ConversationStore r, + Member (Error FederationError) r, + Member (ErrorS OperationDenied) r, + Member (ErrorS NotATeamMember) r, + Member ExternalAccess r, + Member NotificationSubsystem r, + Member Now r, + Member P.TinyLog r, + Member TeamStore r, + Member TeamCollaboratorsSubsystem r + ) => + Local UserId -> + TeamId -> + UserId -> + Set TeamCollaborator.CollaboratorPermission -> + Sem r () +updateTeamCollaborator lusr tid rusr perms = do + P.debug $ + Log.field "targets" (toByteString rusr) + . Log.field "action" (Log.val "Teams.updateTeamCollaborator") + zusrMember <- E.getTeamMember tid (tUnqualified lusr) + void $ permissionCheck UpdateTeamCollaborator zusrMember + when (Set.null $ Set.intersection (Set.fromList [Collaborator.CreateTeamConversation, Collaborator.ImplicitConnection]) perms) $ + removeFromConvsAndPushConvLeaveEvent lusr Nothing tid rusr + internalUpdateTeamCollaborator rusr tid perms + -- | Removing a team collaborator and clean their conversations removeTeamCollaborator :: forall r. From c163493bd5adace53859b10cd6e6dc2b81666020 Mon Sep 17 00:00:00 2001 From: Gautier DI FOLCO Date: Wed, 24 Sep 2025 12:34:15 +0200 Subject: [PATCH 8/9] fix: ormolu --- integration/test/API/Brig.hs | 1 + 1 file changed, 1 insertion(+) diff --git a/integration/test/API/Brig.hs b/integration/test/API/Brig.hs index 4cfaab51de..11f09b8052 100644 --- a/integration/test/API/Brig.hs +++ b/integration/test/API/Brig.hs @@ -1178,6 +1178,7 @@ createApp creator tid new = do "accent_id" .= new.accentId, "metadata" .= new.meta ] + refreshAppCookie :: (MakesValue u) => u -> String -> String -> App Response refreshAppCookie u tid appId = do req <- baseRequest u Brig Versioned $ joinHttpPath ["teams", tid, "apps", appId, "cookies"] From 14c0d63cffd611d721b4f0c085cf4b7296adcdc2 Mon Sep 17 00:00:00 2001 From: Gautier DI FOLCO Date: Wed, 24 Sep 2025 13:55:51 +0200 Subject: [PATCH 9/9] fix: re-trigger ci --- libs/wire-api/src/Wire/API/Event/Team.hs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libs/wire-api/src/Wire/API/Event/Team.hs b/libs/wire-api/src/Wire/API/Event/Team.hs index 1ea37463a4..3efdb53265 100644 --- a/libs/wire-api/src/Wire/API/Event/Team.hs +++ b/libs/wire-api/src/Wire/API/Event/Team.hs @@ -241,7 +241,7 @@ parseEventData TeamCreate Nothing = fail "missing event data for type 'team.crea parseEventData TeamCreate (Just j) = EdTeamCreate <$> parseJSON j parseEventData TeamUpdate Nothing = fail "missing event data for type 'team.update'" parseEventData TeamUpdate (Just j) = EdTeamUpdate <$> parseJSON j -parseEventData CollaboratorAdd Nothing = fail "missing event data for type 'team.collaborator-add" +parseEventData CollaboratorAdd Nothing = fail "missing event data for type 'team.collaborator-add'" parseEventData CollaboratorAdd (Just j) = do let f o = EdCollaboratorAdd <$> o .: "user" <*> o .: "permissions" withObject "collaborator add data" f j @@ -249,11 +249,11 @@ parseEventData AppCreate Nothing = fail "missing event data for type 'team.app-c parseEventData AppCreate (Just j) = do let f o = EdAppCreate <$> o .: "user" withObject "app create data" f j -parseEventData CollaboratorUpdate Nothing = fail "missing event data for type 'team.collaborator-update" +parseEventData CollaboratorUpdate Nothing = fail "missing event data for type 'team.collaborator-update'" parseEventData CollaboratorUpdate (Just j) = do let f o = EdCollaboratorUpdate <$> o .: "user" <*> o .: "permissions" withObject "collaborator update data" f j -parseEventData CollaboratorRemove Nothing = fail "missing event data for type 'team.collaborator-remove" +parseEventData CollaboratorRemove Nothing = fail "missing event data for type 'team.collaborator-remove'" parseEventData CollaboratorRemove (Just j) = do let f o = EdCollaboratorRemove <$> o .: "user" withObject "collaborator remove data" f j