Skip to content

Commit f957075

Browse files
authored
WPB-18190: Add route to delete collaborator from team (#4694)
1 parent ffab1dc commit f957075

File tree

17 files changed

+312
-61
lines changed

17 files changed

+312
-61
lines changed

changelog.d/2-features/WPB-18190

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Allow collaborator to be removed from a team.

integration/test/API/Brig.hs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1183,3 +1183,9 @@ refreshAppCookie :: (MakesValue u) => u -> String -> String -> App Response
11831183
refreshAppCookie u tid appId = do
11841184
req <- baseRequest u Brig Versioned $ joinHttpPath ["teams", tid, "apps", appId, "cookies"]
11851185
submit "POST" req
1186+
1187+
removeTeamCollaborator :: (MakesValue owner, MakesValue collaborator, HasCallStack) => owner -> String -> collaborator -> App Response
1188+
removeTeamCollaborator owner tid collaborator = do
1189+
(_, collabId) <- objQid collaborator
1190+
req <- baseRequest owner Galley Versioned $ joinHttpPath ["teams", tid, "collaborators", collabId]
1191+
submit "DELETE" req

integration/test/Notifications.hs

Lines changed: 34 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -106,47 +106,47 @@ awaitNotification user lastNotifId selector = do
106106
since0 <- mapM objId lastNotifId
107107
head <$> awaitNotifications user (Nothing :: Maybe ()) since0 1 selector
108108

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

198-
isUserActivateNotif :: (MakesValue a) => a -> App Bool
198+
isTeamCollaboratorRemovedNotif :: (HasCallStack, MakesValue a) => a -> App Bool
199+
isTeamCollaboratorRemovedNotif = notifTypeIsEqual "team.collaborator-remove"
200+
201+
isUserActivateNotif :: (HasCallStack, MakesValue a) => a -> App Bool
199202
isUserActivateNotif = notifTypeIsEqual "user.activate"
200203

201-
isUserClientAddNotif :: (MakesValue a) => a -> App Bool
204+
isUserClientAddNotif :: (HasCallStack, MakesValue a) => a -> App Bool
202205
isUserClientAddNotif = notifTypeIsEqual "user.client-add"
203206

204-
isUserUpdatedNotif :: (MakesValue a) => a -> App Bool
207+
isUserUpdatedNotif :: (HasCallStack, MakesValue a) => a -> App Bool
205208
isUserUpdatedNotif = notifTypeIsEqual "user.update"
206209

207-
isUserClientRemoveNotif :: (MakesValue a) => a -> App Bool
210+
isUserClientRemoveNotif :: (HasCallStack, MakesValue a) => a -> App Bool
208211
isUserClientRemoveNotif = notifTypeIsEqual "user.client-remove"
209212

210-
isUserLegalholdRequestNotif :: (MakesValue a) => a -> App Bool
213+
isUserLegalholdRequestNotif :: (HasCallStack, MakesValue a) => a -> App Bool
211214
isUserLegalholdRequestNotif = notifTypeIsEqual "user.legalhold-request"
212215

213-
isUserLegalholdEnabledNotif :: (MakesValue a) => a -> App Bool
216+
isUserLegalholdEnabledNotif :: (HasCallStack, MakesValue a) => a -> App Bool
214217
isUserLegalholdEnabledNotif = notifTypeIsEqual "user.legalhold-enable"
215218

216-
isUserLegalholdDisabledNotif :: (MakesValue a) => a -> App Bool
219+
isUserLegalholdDisabledNotif :: (HasCallStack, MakesValue a) => a -> App Bool
217220
isUserLegalholdDisabledNotif = notifTypeIsEqual "user.legalhold-disable"
218221

219-
isUserConnectionNotif :: (MakesValue a) => a -> App Bool
222+
isUserConnectionNotif :: (HasCallStack, MakesValue a) => a -> App Bool
220223
isUserConnectionNotif = notifTypeIsEqual "user.connection"
221224

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

230-
isUserGroupCreatedNotif :: (MakesValue a) => a -> App Bool
233+
isUserGroupCreatedNotif :: (HasCallStack, MakesValue a) => a -> App Bool
231234
isUserGroupCreatedNotif = notifTypeIsEqual "user-group.created"
232235

233-
isUserGroupUpdatedNotif :: (MakesValue a) => a -> App Bool
236+
isUserGroupUpdatedNotif :: (HasCallStack, MakesValue a) => a -> App Bool
234237
isUserGroupUpdatedNotif = notifTypeIsEqual "user-group.updated"
235238

236239
isConvResetNotif :: (HasCallStack, MakesValue n) => n -> App Bool
@@ -264,7 +267,7 @@ assertLeaveNotification fromUser conv user client leaver =
264267
]
265268
)
266269

267-
assertConvUserDeletedNotif :: (MakesValue leaverId) => WebSocket -> leaverId -> App ()
270+
assertConvUserDeletedNotif :: (HasCallStack, MakesValue leaverId) => WebSocket -> leaverId -> App ()
268271
assertConvUserDeletedNotif ws leaverId = do
269272
n <- awaitMatch isConvLeaveNotif ws
270273
nPayload n %. "data.qualified_user_ids.0" `shouldMatch` leaverId

integration/test/Test/TeamCollaborators.hs

Lines changed: 116 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ module Test.TeamCollaborators where
22

33
import API.Brig
44
import API.Galley
5+
import qualified API.GalleyInternal as Internal
56
import Data.Tuple.Extra
6-
import Notifications (isTeamCollaboratorAddedNotif)
7+
import Notifications (isConvLeaveNotif, isTeamCollaboratorAddedNotif, isTeamCollaboratorRemovedNotif, isTeamMemberLeaveNotif)
78
import SetupHelpers
89
import Testlib.Prelude
910

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

159-
postOne2OneConversation alice bob team0 "chat-chit" >>= assertLabel 403 "non-binding-team-members"
160+
testRemoveCollaboratorInTeamsO2O :: (HasCallStack) => App ()
161+
testRemoveCollaboratorInTeamsO2O = do
162+
(owner0, team0, [alice]) <- createTeam OwnDomain 2
163+
(owner1, team1, [bob]) <- createTeam OwnDomain 2
164+
165+
-- At the time of writing, it wasn't clear if this should be a bot instead.
166+
charlie <- randomUser OwnDomain def
167+
addTeamCollaborator owner0 team0 charlie ["implicit_connection"] >>= assertSuccess
168+
addTeamCollaborator owner1 team1 charlie ["implicit_connection"] >>= assertSuccess
169+
170+
convId <-
171+
postOne2OneConversation charlie alice team0 "chit-chat" `bindResponse` \resp -> do
172+
resp.status `shouldMatchInt` 201
173+
resp.json %. "qualified_id"
174+
postOne2OneConversation charlie bob team1 "chit-chat" >>= assertSuccess
175+
Internal.getConversation convId >>= assertSuccess
176+
177+
removeTeamCollaborator owner0 team0 charlie >>= assertSuccess
178+
179+
getMLSOne2OneConversation charlie alice >>= assertLabel 403 "not-connected"
180+
postOne2OneConversation charlie alice team0 "chit-chat" >>= assertLabel 403 "no-team-member"
181+
Internal.getConversation convId >>= assertLabel 404 "no-conversation"
182+
getMLSOne2OneConversation charlie bob >>= assertSuccess
183+
184+
testRemoveCollaboratorInO2OConnected :: (HasCallStack) => App ()
185+
testRemoveCollaboratorInO2OConnected = do
186+
(owner0, team0, [alice]) <- createTeam OwnDomain 2
187+
188+
-- At the time of writing, it wasn't clear if this should be a bot instead.
189+
bob <- randomUser OwnDomain def
190+
connectTwoUsers alice bob
191+
192+
addTeamCollaborator owner0 team0 bob ["implicit_connection"] >>= assertSuccess
193+
194+
postOne2OneConversation bob alice team0 "chit-chat" >>= assertSuccess
195+
196+
removeTeamCollaborator owner0 team0 bob >>= assertSuccess
197+
198+
getMLSOne2OneConversation bob alice >>= assertSuccess
199+
200+
testRemoveCollaboratorInO2O :: (HasCallStack) => App ()
201+
testRemoveCollaboratorInO2O = do
202+
(owner0, team0, [alice]) <- createTeam OwnDomain 2
203+
204+
-- At the time of writing, it wasn't clear if this should be a bot instead.
205+
bob <- randomUser OwnDomain def
206+
addTeamCollaborator owner0 team0 bob ["implicit_connection"] >>= assertSuccess
207+
208+
teamConvId <-
209+
postOne2OneConversation bob alice team0 "chit-chat" `bindResponse` \resp -> do
210+
resp.status `shouldMatchInt` 201
211+
resp.json %. "qualified_id"
212+
Internal.getConversation teamConvId >>= assertSuccess
213+
214+
connectTwoUsers alice bob
215+
personalConvId <- postConversation alice defProteus {qualifiedUsers = [bob]} >>= getJSON 201
216+
Internal.getConversation personalConvId >>= assertSuccess
217+
218+
removeTeamCollaborator owner0 team0 bob >>= assertSuccess
219+
220+
postOne2OneConversation bob alice team0 "chit-chat" >>= assertLabel 403 "no-team-member"
221+
Internal.getConversation teamConvId >>= assertLabel 404 "no-conversation"
222+
223+
getMLSOne2OneConversation bob alice >>= assertSuccess
224+
Internal.getConversation personalConvId >>= assertSuccess
225+
226+
testRemoveCollaboratorInTeamConversation :: (HasCallStack) => App ()
227+
testRemoveCollaboratorInTeamConversation = do
228+
(owner, team, [alice, bob]) <- createTeam OwnDomain 3
229+
230+
conv <-
231+
postConversation
232+
owner
233+
defProteus {team = Just team, qualifiedUsers = [alice, bob]}
234+
>>= getJSON 201
235+
236+
withWebSockets [owner, alice, bob] $ \[wsOwner, wsAlice, wsBob] -> do
237+
removeTeamCollaborator owner team bob >>= assertSuccess
238+
239+
bobId <- bob %. "qualified_id"
240+
bobUnqualifiedId <- bobId %. "id"
241+
let checkLeaveEvent :: (MakesValue a, HasCallStack) => a -> App ()
242+
checkLeaveEvent evt = do
243+
evt %. "payload.0.data.user" `shouldMatch` bobUnqualifiedId
244+
evt %. "payload.0.team" `shouldMatch` team
245+
checkRemoveEvent :: (MakesValue a, HasCallStack) => a -> App ()
246+
checkRemoveEvent evt = do
247+
evt %. "payload.0.data.user" `shouldMatch` bobUnqualifiedId
248+
evt %. "payload.0.team" `shouldMatch` team
249+
checkConvLeaveEvent :: (MakesValue a, HasCallStack) => a -> App ()
250+
checkConvLeaveEvent evt = do
251+
evt %. "payload.0.data.qualified_user_ids" `shouldMatch` [bobId]
252+
evt %. "payload.0.team" `shouldMatch` team
253+
254+
awaitMatch isTeamMemberLeaveNotif wsOwner >>= checkLeaveEvent
255+
awaitMatch isTeamMemberLeaveNotif wsAlice >>= checkRemoveEvent
256+
awaitMatch isTeamMemberLeaveNotif wsBob >>= checkLeaveEvent
257+
awaitMatch isTeamCollaboratorRemovedNotif wsOwner >>= checkRemoveEvent
258+
awaitMatch isConvLeaveNotif wsAlice >>= checkConvLeaveEvent
259+
260+
getConversation alice conv `bindResponse` \resp -> do
261+
resp.status `shouldMatchInt` 200
262+
otherMember <- assertOne =<< asList (resp.json %. "members.others")
263+
otherMember %. "qualified_id" `shouldNotMatch` (bob %. "qualified_id")
264+
265+
getConversation bob conv `bindResponse` \resp -> do
266+
-- should be 404
267+
resp.status `shouldMatchInt` 403
268+
resp.json %. "label" `shouldMatch` "access-denied"
269+
270+
Internal.getConversation conv `bindResponse` \resp -> do
271+
resp.status `shouldMatchInt` 200
272+
otherMembers <- asList (resp.json %. "members.others")
273+
traverse (%. "qualified_id") otherMembers `shouldMatchSet` traverse (%. "qualified_id") [owner, alice]

libs/wire-api/src/Wire/API/Event/Team.hs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ data EventType
125125
| ConvDelete
126126
| CollaboratorAdd
127127
| AppCreate
128+
| CollaboratorRemove
128129
deriving stock (Eq, Show, Generic)
129130
deriving (Arbitrary) via (GenericUniform EventType)
130131
deriving (FromJSON, ToJSON, S.ToSchema) via Schema EventType
@@ -142,7 +143,8 @@ instance ToSchema EventType where
142143
element "team.conversation-create" ConvCreate,
143144
element "team.conversation-delete" ConvDelete,
144145
element "team.collaborator-add" CollaboratorAdd,
145-
element "team.app-create" AppCreate
146+
element "team.app-create" AppCreate,
147+
element "team.collaborator-remove" CollaboratorRemove
146148
]
147149

148150
--------------------------------------------------------------------------------
@@ -159,6 +161,7 @@ data EventData
159161
| EdConvDelete ConvId
160162
| EdCollaboratorAdd UserId [CollaboratorPermission]
161163
| EdAppCreate UserId
164+
| EdCollaboratorRemove UserId
162165
deriving stock (Eq, Show, Generic)
163166

164167
-- FUTUREWORK: this is outright wrong; see "Wire.API.Event.Conversation" on how to do this properly.
@@ -189,6 +192,7 @@ instance ToJSON EventData where
189192
"permissions" A..= perms
190193
]
191194
toJSON (EdAppCreate usr) = A.object ["user" A..= usr]
195+
toJSON (EdCollaboratorRemove usr) = A.object ["user" A..= usr]
192196

193197
eventDataType :: EventData -> EventType
194198
eventDataType (EdTeamCreate _) = TeamCreate
@@ -201,6 +205,7 @@ eventDataType (EdConvCreate _) = ConvCreate
201205
eventDataType (EdConvDelete _) = ConvDelete
202206
eventDataType (EdCollaboratorAdd _ _) = CollaboratorAdd
203207
eventDataType (EdAppCreate _) = AppCreate
208+
eventDataType (EdCollaboratorRemove _) = CollaboratorRemove
204209

205210
parseEventData :: EventType -> Maybe Value -> Parser EventData
206211
parseEventData MemberJoin Nothing = fail "missing event data for type 'team.member-join'"
@@ -215,11 +220,11 @@ parseEventData MemberLeave Nothing = fail "missing event data for type 'team.mem
215220
parseEventData MemberLeave (Just j) = do
216221
let f o = EdMemberLeave <$> o .: "user"
217222
withObject "member leave data" f j
218-
parseEventData ConvCreate Nothing = fail "missing event data for type 'team.conversation-create"
223+
parseEventData ConvCreate Nothing = fail "missing event data for type 'team.conversation-create'"
219224
parseEventData ConvCreate (Just j) = do
220225
let f o = EdConvCreate <$> o .: "conv"
221226
withObject "conversation create data" f j
222-
parseEventData ConvDelete Nothing = fail "missing event data for type 'team.conversation-delete"
227+
parseEventData ConvDelete Nothing = fail "missing event data for type 'team.conversation-delete'"
223228
parseEventData ConvDelete (Just j) = do
224229
let f o = EdConvDelete <$> o .: "conv"
225230
withObject "conversation delete data" f j
@@ -235,6 +240,10 @@ parseEventData AppCreate Nothing = fail "missing event data for type 'team.app-c
235240
parseEventData AppCreate (Just j) = do
236241
let f o = EdAppCreate <$> o .: "user"
237242
withObject "app create data" f j
243+
parseEventData CollaboratorRemove Nothing = fail "missing event data for type 'team.collaborator-remove"
244+
parseEventData CollaboratorRemove (Just j) = do
245+
let f o = EdCollaboratorRemove <$> o .: "user"
246+
withObject "collaborator remove data" f j
238247
parseEventData _ Nothing = pure EdTeamDelete
239248
parseEventData t (Just _) = fail $ "unexpected event data for type " <> show t
240249

@@ -250,5 +259,6 @@ genEventData = \case
250259
ConvDelete -> EdConvDelete <$> arbitrary
251260
CollaboratorAdd -> EdCollaboratorAdd <$> arbitrary <*> arbitrary
252261
AppCreate -> EdAppCreate <$> arbitrary
262+
CollaboratorRemove -> EdCollaboratorRemove <$> arbitrary
253263

254264
makeLenses ''Event

libs/wire-api/src/Wire/API/Routes/Public/Galley/TeamMember.hs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,19 @@ type TeamMemberAPI =
207207
"CSV of team members"
208208
CSV
209209
)
210+
:<|> Named
211+
"remove-team-collaborator"
212+
( Summary "Remove a collaborator from the team."
213+
:> CanThrow OperationDenied
214+
:> CanThrow 'NotATeamMember
215+
:> From 'V12
216+
:> ZLocalUser
217+
:> "teams"
218+
:> Capture "tid" TeamId
219+
:> "collaborators"
220+
:> Capture "uid" UserId
221+
:> MultiVerb1 'DELETE '[JSON] (RespondEmpty 200 "")
222+
)
210223

211224
type TeamMemberDeleteResultResponseType =
212225
'[ RespondEmpty 202 "Team member scheduled for deletion",

0 commit comments

Comments
 (0)