Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
4b35aec
WPB-18190: Add route to delete collaborator from team
blackheaven Jul 30, 2025
c198f86
add conversation drop
blackheaven Aug 1, 2025
112d96a
debug
blackheaven Aug 4, 2025
5257af2
go through GalleyAPI
blackheaven Aug 4, 2025
dbd728c
filter then delete
blackheaven Aug 5, 2025
e27ae8f
missing dependency
blackheaven Aug 5, 2025
7ddb6d0
fix missing nix dependency
blackheaven Aug 5, 2025
8c698c9
fix: remove O2O Conversations and remove users from others
blackheaven Aug 6, 2025
63d7712
refactor: split team quitting and user deletion
blackheaven Aug 14, 2025
bad8c2b
rely on Galley
blackheaven Aug 14, 2025
7eb3d04
fix: restore team collaborator removal
blackheaven Aug 15, 2025
c5140a8
Update changelog.d/2-features/WPB-18190
blackheaven Aug 19, 2025
e2adae4
test: add multiple team & check get conv
blackheaven Aug 19, 2025
c64e797
fix: other connection test
blackheaven Aug 20, 2025
7f01784
test: add group conversation
blackheaven Aug 20, 2025
c296277
fix: drop Cassandra IN search
blackheaven Aug 22, 2025
a4d7c16
refactor: drop explicit queries
blackheaven Aug 27, 2025
888aea2
feat: filter-out O2O connections
blackheaven Aug 28, 2025
17b8d6e
fix: rebase
blackheaven Aug 28, 2025
670bdd5
fix: missing parts
blackheaven Aug 28, 2025
d2e110c
wip extend tests
battermann Aug 29, 2025
9f7d623
Revert "wip extend tests"
blackheaven Sep 1, 2025
410f338
fix: tests and correct conversation removal
blackheaven Sep 1, 2025
1c1d501
fix: rebase
blackheaven Sep 1, 2025
37f54c2
fix: pass around changes to trigger notifications
blackheaven Sep 3, 2025
61841f3
fix: add tests assertions HasCallStack
blackheaven Sep 3, 2025
a25bc6e
fix: could it finally work?
blackheaven Sep 4, 2025
20b0877
fix: filter duplicated convs
blackheaven Sep 4, 2025
a540f26
fix: ormolu
blackheaven Sep 4, 2025
1f47ccd
fix: comment
blackheaven Sep 4, 2025
0ec3937
fix: ormolu
blackheaven Sep 4, 2025
b300a25
fix: rebase
blackheaven Sep 5, 2025
7efab3f
refactor: move to galley
blackheaven Sep 9, 2025
908ea01
fix: condition
blackheaven Sep 9, 2025
01095af
fix: endpoint is on galley now
blackheaven Sep 9, 2025
e2857fc
fix: add forgot collaborator deletion
blackheaven Sep 10, 2025
bd6ad8c
fix: add debug logs
blackheaven Sep 10, 2025
192f75b
fix: ormolu
blackheaven Sep 10, 2025
cd458ab
fix: add debug logs
blackheaven Sep 10, 2025
49c93ef
refactor: drop only conversations in teams
blackheaven Sep 10, 2025
0f38f72
fix: drop debug logs
blackheaven Sep 10, 2025
f7bee64
fix: working
blackheaven Sep 12, 2025
1c2971d
WPB-18191: Add route to collaborator permissions from team
blackheaven Jul 31, 2025
bf8cbc0
trigger conversation closing
blackheaven Aug 4, 2025
f5c3f78
fix formating
blackheaven Aug 5, 2025
4645e66
fix: rebase
blackheaven Sep 5, 2025
6277070
fix: add perform-ability to leave
blackheaven Sep 5, 2025
f114414
fix: rebase
blackheaven Sep 5, 2025
b2a155d
refactor: move to galley
blackheaven Sep 12, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/2-features/WPB-18190
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Allow collaborator to be removed from a team.
1 change: 1 addition & 0 deletions changelog.d/2-features/WPB-18191
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Allow member permissions to be updated in a team.
14 changes: 14 additions & 0 deletions integration/test/API/Galley.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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
62 changes: 31 additions & 31 deletions integration/test/Notifications.hs
Original file line number Diff line number Diff line change
Expand Up @@ -106,47 +106,47 @@ awaitNotification user lastNotifId selector = do
since0 <- mapM objId lastNotifId
head <$> awaitNotifications user (Nothing :: Maybe ()) since0 1 selector

isDeleteUserNotif :: (MakesValue a) => a -> App Bool
isDeleteUserNotif :: (HasCallStack, MakesValue a) => a -> App Bool
isDeleteUserNotif n =
nPayload n %. "type" `isEqual` "user.delete"

isFeatureConfigUpdateNotif :: (MakesValue a) => a -> App Bool
isFeatureConfigUpdateNotif :: (HasCallStack, MakesValue a) => a -> App Bool
isFeatureConfigUpdateNotif n =
nPayload n %. "type" `isEqual` "feature-config.update"

isNewMessageNotif :: (MakesValue a) => a -> App Bool
isNewMessageNotif :: (HasCallStack, MakesValue a) => a -> App Bool
isNewMessageNotif n = fieldEquals n "payload.0.type" "conversation.otr-message-add"

isNewMLSMessageNotif :: (MakesValue a) => a -> App Bool
isNewMLSMessageNotif :: (HasCallStack, MakesValue a) => a -> App Bool
isNewMLSMessageNotif n = fieldEquals n "payload.0.type" "conversation.mls-message-add"

isWelcomeNotif :: (MakesValue a) => a -> App Bool
isWelcomeNotif :: (HasCallStack, MakesValue a) => a -> App Bool
isWelcomeNotif n = fieldEquals n "payload.0.type" "conversation.mls-welcome"

isMemberJoinNotif :: (MakesValue a) => a -> App Bool
isMemberJoinNotif :: (HasCallStack, MakesValue a) => a -> App Bool
isMemberJoinNotif n = fieldEquals n "payload.0.type" "conversation.member-join"

isConvLeaveNotif :: (MakesValue a) => a -> App Bool
isConvLeaveNotif :: (HasCallStack, MakesValue a) => a -> App Bool
isConvLeaveNotif n = fieldEquals n "payload.0.type" "conversation.member-leave"

isConvLeaveNotifWithLeaver :: (MakesValue user, MakesValue a) => user -> a -> App Bool
isConvLeaveNotifWithLeaver :: (HasCallStack, MakesValue user, MakesValue a) => user -> a -> App Bool
isConvLeaveNotifWithLeaver user n =
fieldEquals n "payload.0.type" "conversation.member-leave"
&&~ (n %. "payload.0.data.user_ids.0") `isEqual` (user %. "id")

isNotifConv :: (MakesValue conv, MakesValue a, HasCallStack) => conv -> a -> App Bool
isNotifConv :: (HasCallStack, MakesValue conv, MakesValue a, HasCallStack) => conv -> a -> App Bool
isNotifConv conv n = fieldEquals n "payload.0.qualified_conversation" (objQidObject conv)

isNotifConvId :: (MakesValue a, HasCallStack) => ConvId -> a -> App Bool
isNotifConvId :: (HasCallStack, MakesValue a, HasCallStack) => ConvId -> a -> App Bool
isNotifConvId conv n = do
let subconvField = "payload.0.subconv"
fieldEquals n "payload.0.qualified_conversation" (convIdToQidObject conv)
&&~ maybe (isNothing <$> lookupField n subconvField) (fieldEquals n subconvField) conv.subconvId

isNotifForUser :: (MakesValue user, MakesValue a, HasCallStack) => user -> a -> App Bool
isNotifForUser :: (HasCallStack, MakesValue user, MakesValue a, HasCallStack) => user -> a -> App Bool
isNotifForUser user n = fieldEquals n "payload.0.data.qualified_user_ids.0" (objQidObject user)

isNotifFromUser :: (MakesValue user, MakesValue a, HasCallStack) => user -> a -> App Bool
isNotifFromUser :: (HasCallStack, MakesValue user, MakesValue a, HasCallStack) => user -> a -> App Bool
isNotifFromUser user n = fieldEquals n "payload.0.qualified_from" (objQidObject user)

isConvNameChangeNotif :: (HasCallStack, MakesValue a) => a -> App Bool
Expand All @@ -171,66 +171,66 @@ isConvAccessUpdateNotif :: (HasCallStack, MakesValue n) => n -> App Bool
isConvAccessUpdateNotif n =
fieldEquals n "payload.0.type" "conversation.access-update"

isConvCreateNotif :: (MakesValue a) => a -> App Bool
isConvCreateNotif :: (HasCallStack, MakesValue a) => a -> App Bool
isConvCreateNotif n = fieldEquals n "payload.0.type" "conversation.create"

-- | like 'isConvCreateNotif' but excludes self conversations
isConvCreateNotifNotSelf :: (MakesValue a) => a -> App Bool
isConvCreateNotifNotSelf :: (HasCallStack, MakesValue a) => a -> App Bool
isConvCreateNotifNotSelf n =
fieldEquals n "payload.0.type" "conversation.create"
&&~ do not <$> fieldEquals n "payload.0.data.access" ["private"]

isConvDeleteNotif :: (MakesValue a) => a -> App Bool
isConvDeleteNotif :: (HasCallStack, MakesValue a) => a -> App Bool
isConvDeleteNotif n = fieldEquals n "payload.0.type" "conversation.delete"

notifTypeIsEqual :: (MakesValue a) => String -> a -> App Bool
notifTypeIsEqual :: (HasCallStack, MakesValue a) => String -> a -> App Bool
notifTypeIsEqual typ n = nPayload n %. "type" `isEqual` typ

isTeamMemberJoinNotif :: (MakesValue a) => a -> App Bool
isTeamMemberJoinNotif :: (HasCallStack, MakesValue a) => a -> App Bool
isTeamMemberJoinNotif = notifTypeIsEqual "team.member-join"

isTeamMemberLeaveNotif :: (MakesValue a) => a -> App Bool
isTeamMemberLeaveNotif :: (HasCallStack, MakesValue a) => a -> App Bool
isTeamMemberLeaveNotif = notifTypeIsEqual "team.member-leave"

isTeamCollaboratorAddedNotif :: (MakesValue a) => a -> App Bool
isTeamCollaboratorAddedNotif :: (HasCallStack, MakesValue a) => a -> App Bool
isTeamCollaboratorAddedNotif = notifTypeIsEqual "team.collaborator-add"

isUserActivateNotif :: (MakesValue a) => a -> App Bool
isUserActivateNotif :: (HasCallStack, MakesValue a) => a -> App Bool
isUserActivateNotif = notifTypeIsEqual "user.activate"

isUserClientAddNotif :: (MakesValue a) => a -> App Bool
isUserClientAddNotif :: (HasCallStack, MakesValue a) => a -> App Bool
isUserClientAddNotif = notifTypeIsEqual "user.client-add"

isUserUpdatedNotif :: (MakesValue a) => a -> App Bool
isUserUpdatedNotif :: (HasCallStack, MakesValue a) => a -> App Bool
isUserUpdatedNotif = notifTypeIsEqual "user.update"

isUserClientRemoveNotif :: (MakesValue a) => a -> App Bool
isUserClientRemoveNotif :: (HasCallStack, MakesValue a) => a -> App Bool
isUserClientRemoveNotif = notifTypeIsEqual "user.client-remove"

isUserLegalholdRequestNotif :: (MakesValue a) => a -> App Bool
isUserLegalholdRequestNotif :: (HasCallStack, MakesValue a) => a -> App Bool
isUserLegalholdRequestNotif = notifTypeIsEqual "user.legalhold-request"

isUserLegalholdEnabledNotif :: (MakesValue a) => a -> App Bool
isUserLegalholdEnabledNotif :: (HasCallStack, MakesValue a) => a -> App Bool
isUserLegalholdEnabledNotif = notifTypeIsEqual "user.legalhold-enable"

isUserLegalholdDisabledNotif :: (MakesValue a) => a -> App Bool
isUserLegalholdDisabledNotif :: (HasCallStack, MakesValue a) => a -> App Bool
isUserLegalholdDisabledNotif = notifTypeIsEqual "user.legalhold-disable"

isUserConnectionNotif :: (MakesValue a) => a -> App Bool
isUserConnectionNotif :: (HasCallStack, MakesValue a) => a -> App Bool
isUserConnectionNotif = notifTypeIsEqual "user.connection"

isConnectionNotif :: (MakesValue a) => String -> a -> App Bool
isConnectionNotif :: (HasCallStack, MakesValue a) => String -> a -> App Bool
isConnectionNotif status n =
-- NB:
-- (&&) <$> (print "hello" *> pure False) <*> fail "bla" === _|_
-- runMaybeT $ (lift (print "hello") *> MaybeT (pure Nothing)) *> lift (fail "bla") === pure Nothing
nPayload n %. "type" `isEqual` "user.connection"
&&~ nPayload n %. "connection.status" `isEqual` status

isUserGroupCreatedNotif :: (MakesValue a) => a -> App Bool
isUserGroupCreatedNotif :: (HasCallStack, MakesValue a) => a -> App Bool
isUserGroupCreatedNotif = notifTypeIsEqual "user-group.created"

isUserGroupUpdatedNotif :: (MakesValue a) => a -> App Bool
isUserGroupUpdatedNotif :: (HasCallStack, MakesValue a) => a -> App Bool
isUserGroupUpdatedNotif = notifTypeIsEqual "user-group.updated"

isConvResetNotif :: (HasCallStack, MakesValue n) => n -> App Bool
Expand Down Expand Up @@ -264,7 +264,7 @@ assertLeaveNotification fromUser conv user client leaver =
]
)

assertConvUserDeletedNotif :: (MakesValue leaverId) => WebSocket -> leaverId -> App ()
assertConvUserDeletedNotif :: (HasCallStack, MakesValue leaverId) => WebSocket -> leaverId -> App ()
assertConvUserDeletedNotif ws leaverId = do
n <- awaitMatch isConvLeaveNotif ws
nPayload n %. "data.qualified_user_ids.0" `shouldMatch` leaverId
Expand Down
137 changes: 135 additions & 2 deletions integration/test/Test/TeamCollaborators.hs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ module Test.TeamCollaborators where

import API.Brig
import API.Galley
import qualified API.GalleyInternal as Internal
import Data.Tuple.Extra
import Notifications (isTeamCollaboratorAddedNotif)
import Notifications (isTeamCollaboratorAddedNotif, isTeamMemberLeaveNotif)
import SetupHelpers
import Testlib.Prelude

Expand Down Expand Up @@ -156,4 +157,136 @@ testImplicitConnectionNoCollaborator = do
-- Alice and Bob aren't connected at all.
postOne2OneConversation bob alice team0 "chit-chat" >>= assertLabel 403 "no-team-member"

postOne2OneConversation alice bob team0 "chat-chit" >>= assertLabel 403 "non-binding-team-members"
testRemoveMemberInTeamsO2O :: (HasCallStack) => App ()
testRemoveMemberInTeamsO2O = do
(owner0, team0, [alice]) <- createTeam OwnDomain 2
(owner1, team1, [bob]) <- createTeam OwnDomain 2

-- At the time of writing, it wasn't clear if this should be a bot instead.
charlie <- randomUser OwnDomain def
addTeamCollaborator owner0 team0 charlie ["implicit_connection"] >>= assertSuccess
addTeamCollaborator owner1 team1 charlie ["implicit_connection"] >>= assertSuccess

convId <-
postOne2OneConversation charlie alice team0 "chit-chat" `bindResponse` \resp -> do
resp.status `shouldMatchInt` 201
resp.json %. "qualified_id"
postOne2OneConversation charlie bob team1 "chit-chat" >>= assertSuccess
Internal.getConversation convId >>= assertSuccess

removeTeamCollaborator owner0 team0 charlie >>= assertSuccess

getMLSOne2OneConversation charlie alice >>= assertLabel 403 "not-connected"
postOne2OneConversation charlie alice team0 "chit-chat" >>= assertLabel 403 "no-team-member"
Internal.getConversation convId >>= assertLabel 404 "no-conversation"
getMLSOne2OneConversation charlie bob >>= assertSuccess

testRemoveMemberInO2OConnected :: (HasCallStack) => App ()
testRemoveMemberInO2OConnected = do
(owner0, team0, [alice]) <- createTeam OwnDomain 2

-- At the time of writing, it wasn't clear if this should be a bot instead.
bob <- randomUser OwnDomain def
connectTwoUsers alice bob

addTeamCollaborator owner0 team0 bob ["implicit_connection"] >>= assertSuccess

postOne2OneConversation bob alice team0 "chit-chat" >>= assertSuccess

removeTeamCollaborator owner0 team0 bob >>= assertSuccess

getMLSOne2OneConversation bob alice >>= assertSuccess

testRemoveMemberInO2O :: (HasCallStack) => App ()
testRemoveMemberInO2O = do
(owner0, team0, [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 owner0 team0 bob ["implicit_connection"] >>= assertSuccess

teamConvId <-
postOne2OneConversation bob alice team0 "chit-chat" `bindResponse` \resp -> do
resp.status `shouldMatchInt` 201
resp.json %. "qualified_id"
Internal.getConversation teamConvId >>= assertSuccess

connectTwoUsers alice bob
personalConvId <- postConversation alice defProteus {qualifiedUsers = [bob]} >>= getJSON 201
Internal.getConversation personalConvId >>= assertSuccess

removeTeamCollaborator owner0 team0 bob >>= assertSuccess

postOne2OneConversation bob alice team0 "chit-chat" >>= assertLabel 403 "no-team-member"
Internal.getConversation teamConvId >>= assertLabel 404 "no-conversation"

getMLSOne2OneConversation bob alice >>= assertSuccess
Internal.getConversation personalConvId >>= assertSuccess

testRemoveMemberInTeamConversation :: (HasCallStack) => App ()
testRemoveMemberInTeamConversation = do
(owner, team, [alice, bob]) <- createTeam OwnDomain 3

conv <-
postConversation
owner
defProteus {team = Just team, qualifiedUsers = [alice, bob]}
>>= getJSON 201

withWebSockets [owner, alice] $ \[wsOwner, wsAlice] -> do
removeTeamCollaborator owner team bob >>= assertSuccess

bobUnqualifiedId <- bob %. "qualified_id.id"
let checkEvent :: (MakesValue a) => a -> App ()
checkEvent evt = do
evt %. "payload.0.data.user" `shouldMatch` bobUnqualifiedId
evt %. "payload.0.team" `shouldMatch` team
evt %. "transient" `shouldMatch` True

awaitMatch isTeamMemberLeaveNotif wsOwner >>= checkEvent
assertNoEvent 0 wsAlice

getConversation alice conv `bindResponse` \resp -> do
resp.status `shouldMatchInt` 200
otherMember <- assertOne =<< asList (resp.json %. "members.others")
otherMember %. "qualified_id" `shouldNotMatch` (bob %. "qualified_id")

getConversation bob conv `bindResponse` \resp -> do
-- should be 404
resp.status `shouldMatchInt` 403
resp.json %. "label" `shouldMatch` "access-denied"

Internal.getConversation conv `bindResponse` \resp -> do
resp.status `shouldMatchInt` 200
otherMembers <- asList (resp.json %. "members.others")
traverse (%. "qualified_id") otherMembers `shouldMatchSet` traverse (%. "qualified_id") [owner, alice]

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.
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"
26 changes: 25 additions & 1 deletion libs/wire-api/src/Wire/API/Event/Team.hs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ data EventType
| ConvDelete
| CollaboratorAdd
| AppCreate
| CollaboratorUpdate
| CollaboratorRemove
deriving stock (Eq, Show, Generic)
deriving (Arbitrary) via (GenericUniform EventType)
deriving (FromJSON, ToJSON, S.ToSchema) via Schema EventType
Expand All @@ -142,7 +144,9 @@ instance ToSchema EventType where
element "team.conversation-create" ConvCreate,
element "team.conversation-delete" ConvDelete,
element "team.collaborator-add" CollaboratorAdd,
element "team.app-create" AppCreate
element "team.app-create" AppCreate,
element "team.collaborator-update" CollaboratorUpdate,
element "team.collaborator-remove" CollaboratorRemove
]

--------------------------------------------------------------------------------
Expand All @@ -159,6 +163,8 @@ data EventData
| EdConvDelete ConvId
| EdCollaboratorAdd UserId [CollaboratorPermission]
| EdAppCreate UserId
| EdCollaboratorUpdate UserId [CollaboratorPermission]
| EdCollaboratorRemove UserId
deriving stock (Eq, Show, Generic)

-- FUTUREWORK: this is outright wrong; see "Wire.API.Event.Conversation" on how to do this properly.
Expand Down Expand Up @@ -189,6 +195,12 @@ 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
eventDataType (EdTeamCreate _) = TeamCreate
Expand All @@ -201,6 +213,8 @@ eventDataType (EdConvCreate _) = ConvCreate
eventDataType (EdConvDelete _) = ConvDelete
eventDataType (EdCollaboratorAdd _ _) = CollaboratorAdd
eventDataType (EdAppCreate _) = AppCreate
eventDataType (EdCollaboratorUpdate _ _) = CollaboratorUpdate
eventDataType (EdCollaboratorRemove _) = CollaboratorRemove

parseEventData :: EventType -> Maybe Value -> Parser EventData
parseEventData MemberJoin Nothing = fail "missing event data for type 'team.member-join'"
Expand Down Expand Up @@ -235,6 +249,14 @@ 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"
withObject "collaborator remove data" f j
parseEventData _ Nothing = pure EdTeamDelete
parseEventData t (Just _) = fail $ "unexpected event data for type " <> show t

Expand All @@ -250,5 +272,7 @@ genEventData = \case
ConvDelete -> EdConvDelete <$> arbitrary
CollaboratorAdd -> EdCollaboratorAdd <$> arbitrary <*> arbitrary
AppCreate -> EdAppCreate <$> arbitrary
CollaboratorUpdate -> EdCollaboratorUpdate <$> arbitrary <*> arbitrary
CollaboratorRemove -> EdCollaboratorRemove <$> arbitrary

makeLenses ''Event
Loading