diff --git a/changelog.d/2-features/WPB-21964-delete b/changelog.d/2-features/WPB-21964-delete new file mode 100644 index 00000000000..d2ad2d512a4 --- /dev/null +++ b/changelog.d/2-features/WPB-21964-delete @@ -0,0 +1,6 @@ +`DELETE /meetings/:domain/:meetingId` for deleting meetings. + +Authorization: only the meeting creator can delete the meeting. +Validity: meetings that ended too long ago cannot be deleted (configurable validity period). + +When a meeting is deleted, the associated MLS conversation is also deleted if it's a MeetingConversation type. \ No newline at end of file diff --git a/integration/test/API/Galley.hs b/integration/test/API/Galley.hs index 60db90f97c7..da501bda1f5 100644 --- a/integration/test/API/Galley.hs +++ b/integration/test/API/Galley.hs @@ -990,6 +990,11 @@ putMeeting user domain meetingId updatedMeeting = do req <- baseRequest user Galley Versioned (joinHttpPath ["meetings", domain, meetingId]) submit "PUT" $ req & addJSON updatedMeeting +deleteMeeting :: (HasCallStack, MakesValue user) => user -> String -> String -> App Response +deleteMeeting user domain meetingId = do + req <- baseRequest user Galley Versioned (joinHttpPath ["meetings", domain, meetingId]) + submit "DELETE" req + getMeeting :: (HasCallStack, MakesValue user) => user -> String -> String -> App Response getMeeting user domain meetingId = do req <- baseRequest user Galley Versioned (joinHttpPath ["meetings", domain, meetingId]) diff --git a/integration/test/Test/Meetings.hs b/integration/test/Test/Meetings.hs index 1fce53ff225..bfb9bd6dc8b 100644 --- a/integration/test/Test/Meetings.hs +++ b/integration/test/Test/Meetings.hs @@ -197,3 +197,51 @@ testMeetingUpdateUnauthorized = do ] putMeeting otherUser domain meetingId update >>= assertStatus 404 + +testMeetingDelete :: (HasCallStack) => App () +testMeetingDelete = do + (owner, _tid, _members) <- createTeam OwnDomain 1 + now <- liftIO getCurrentTime + let startTime = addUTCTime 3600 now + endTime = addUTCTime 7200 now + recurrenceUntil = addUTCTime (30 * 24 * 3600) now + recurrence = + object + [ "frequency" .= "daily", + "interval" .= (1 :: Int), + "until" .= recurrenceUntil + ] + newMeeting = + object + [ "title" .= "Team Standup", + "start_time" .= startTime, + "end_time" .= endTime, + "invited_emails" .= ([] :: [String]), + "recurrence" .= recurrence + ] + r1 <- postMeetings owner newMeeting + assertSuccess r1 + meeting <- getJSON 201 r1 + (meetingId, domain) <- getMeetingIdAndDomain meeting + deleteMeeting owner domain meetingId >>= assertStatus 200 + getMeeting owner domain meetingId >>= assertStatus 404 + +testMeetingDeleteNotFound :: (HasCallStack) => App () +testMeetingDeleteNotFound = do + (owner, _tid, _members) <- createTeam OwnDomain 1 + fakeMeetingId <- randomId + deleteMeeting owner "example.com" fakeMeetingId >>= assertStatus 404 + +testMeetingDeleteUnauthorized :: (HasCallStack) => App () +testMeetingDeleteUnauthorized = do + (owner, _tid, _members) <- createTeam OwnDomain 1 + (otherUser, _, _membersOther) <- createTeam OwnDomain 1 + now <- liftIO getCurrentTime + let startTime = addUTCTime 3600 now + endTime = addUTCTime 7200 now + newMeeting = defaultMeetingJson "Team Standup" startTime endTime [] + r1 <- postMeetings owner newMeeting + assertSuccess r1 + meeting <- getJSON 201 r1 + (meetingId, domain) <- getMeetingIdAndDomain meeting + deleteMeeting otherUser domain meetingId >>= assertStatus 404 diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Meetings.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Meetings.hs index 129685f6942..a5845fcd27d 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Meetings.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Meetings.hs @@ -62,6 +62,22 @@ type MeetingsAPI = '[Respond 200 "Meeting updated" Meeting] Meeting ) + :<|> Named + "delete-meeting" + ( Summary "Delete a meeting" + :> From 'V15 + :> ZLocalUser + :> "meetings" + :> Capture "domain" Domain + :> Capture "id" MeetingId + :> CanThrow 'MeetingNotFound + :> CanThrow 'AccessDenied + :> MultiVerb + 'DELETE + '[JSON] + '[RespondEmpty 200 "Meeting deleted"] + () + ) :<|> Named "get-meeting" ( Summary "Get a single meeting by ID" diff --git a/libs/wire-subsystems/src/Wire/BackgroundJobsRunner/Interpreter.hs b/libs/wire-subsystems/src/Wire/BackgroundJobsRunner/Interpreter.hs index 7353ad6092f..4bf73761655 100644 --- a/libs/wire-subsystems/src/Wire/BackgroundJobsRunner/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/BackgroundJobsRunner/Interpreter.hs @@ -43,7 +43,7 @@ import Wire.API.Team.HardTruncationLimit (hardTruncationLimit) import Wire.API.UserGroup import Wire.BackgroundJobsPublisher import Wire.BackgroundJobsRunner (BackgroundJobsRunner (..)) -import Wire.ConversationStore (ConversationStore, getConversation, upsertMembers) +import Wire.ConversationStore (ConversationStore, upsertMembers) import Wire.ConversationSubsystem import Wire.Sem.Random import Wire.StoredConversation diff --git a/libs/wire-subsystems/src/Wire/ConversationSubsystem.hs b/libs/wire-subsystems/src/Wire/ConversationSubsystem.hs index 0fe3d35b1d3..778330f56ba 100644 --- a/libs/wire-subsystems/src/Wire/ConversationSubsystem.hs +++ b/libs/wire-subsystems/src/Wire/ConversationSubsystem.hs @@ -67,5 +67,7 @@ data ConversationSubsystem m a where ConvId -> UserId -> ConversationSubsystem m (Maybe LocalMember) + GetConversation :: ConvId -> ConversationSubsystem m (Maybe StoredConversation) + DeleteConversation :: ConvId -> ConversationSubsystem m () makeSem ''ConversationSubsystem diff --git a/libs/wire-subsystems/src/Wire/ConversationSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/ConversationSubsystem/Interpreter.hs index 0fe59a7476b..85c984126a3 100644 --- a/libs/wire-subsystems/src/Wire/ConversationSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/ConversationSubsystem/Interpreter.hs @@ -145,6 +145,10 @@ interpretConversationSubsystem = interpret $ \case internalGetClientIdsImpl uids ConversationSubsystem.InternalGetLocalMember cid uid -> ConvStore.getLocalMember cid uid + ConversationSubsystem.GetConversation cid -> + ConvStore.getConversation cid + ConversationSubsystem.DeleteConversation cid -> + deleteConversationImpl cid createGroupConversationGeneric :: forall r. @@ -820,3 +824,11 @@ internalGetClientIdsImpl users = do if isInternal then fromUserClients <$> lookupClients users else UserClientIndexStore.getClients users + +deleteConversationImpl :: (Member ConversationStore r) => ConvId -> Sem r () +deleteConversationImpl cid = do + mConv <- ConvStore.getConversation cid + forM_ mConv $ \conv -> do + forM_ conv.metadata.cnvmTeam $ \tid -> + ConvStore.deleteTeamConversation tid cid + ConvStore.deleteConversation cid diff --git a/libs/wire-subsystems/src/Wire/MeetingsStore.hs b/libs/wire-subsystems/src/Wire/MeetingsStore.hs index 581ce53c6e8..e90494634eb 100644 --- a/libs/wire-subsystems/src/Wire/MeetingsStore.hs +++ b/libs/wire-subsystems/src/Wire/MeetingsStore.hs @@ -145,6 +145,9 @@ data MeetingsStore m a where Maybe UTCTime -> Maybe (Maybe Recurrence) -> MeetingsStore m (Maybe StoredMeeting) + DeleteMeeting :: + MeetingId -> + MeetingsStore m () GetMeeting :: MeetingId -> MeetingsStore m (Maybe StoredMeeting) diff --git a/libs/wire-subsystems/src/Wire/MeetingsStore/Postgres.hs b/libs/wire-subsystems/src/Wire/MeetingsStore/Postgres.hs index f72c0514a51..762a8978c06 100644 --- a/libs/wire-subsystems/src/Wire/MeetingsStore/Postgres.hs +++ b/libs/wire-subsystems/src/Wire/MeetingsStore/Postgres.hs @@ -52,6 +52,8 @@ interpretMeetingsStoreToPostgres = createMeetingImpl title creator startTime endTime recurrence convId emails trial UpdateMeeting meetingId title startDate endDate schedule -> updateMeetingImpl meetingId title startDate endDate schedule + DeleteMeeting meetingId -> + deleteMeetingImpl meetingId GetMeeting meetingId -> getMeetingImpl meetingId @@ -238,6 +240,29 @@ updateMeetingImpl meetingId mTitle mStartDate mEndDate mRecurrence = do created_at :: timestamptz, updated_at :: timestamptz |] +-- * Delete + +deleteMeetingImpl :: + ( Member (Input Pool) r, + Member (Embed IO) r, + Member (Error UsageError) r + ) => + MeetingId -> + Sem r () +deleteMeetingImpl meetingId = do + pool <- input + result <- liftIO $ use pool session + either throw pure result + where + session :: Session () + session = statement (toUUID meetingId) deleteStatement + deleteStatement :: Statement UUID () + deleteStatement = + [resultlessStatement| + DELETE FROM meetings + WHERE id = ($1 :: uuid) + |] + -- * Get getMeetingImpl :: diff --git a/libs/wire-subsystems/src/Wire/MeetingsSubsystem.hs b/libs/wire-subsystems/src/Wire/MeetingsSubsystem.hs index f353a85a2d1..48a27a2cdaa 100644 --- a/libs/wire-subsystems/src/Wire/MeetingsSubsystem.hs +++ b/libs/wire-subsystems/src/Wire/MeetingsSubsystem.hs @@ -36,6 +36,10 @@ data MeetingsSubsystem m a where Qualified MeetingId -> UpdateMeeting -> MeetingsSubsystem m (Maybe Meeting) + DeleteMeeting :: + Local UserId -> + Qualified MeetingId -> + MeetingsSubsystem m Bool GetMeeting :: Local UserId -> Qualified MeetingId -> diff --git a/libs/wire-subsystems/src/Wire/MeetingsSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/MeetingsSubsystem/Interpreter.hs index e26c4719c64..ef8f62f9aae 100644 --- a/libs/wire-subsystems/src/Wire/MeetingsSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/MeetingsSubsystem/Interpreter.hs @@ -65,6 +65,8 @@ interpretMeetingsSubsystem validityPeriod = interpret $ \case createMeetingImpl zUser newMeeting UpdateMeeting zUser meetingId update -> updateMeetingImpl zUser meetingId update validityPeriod + DeleteMeeting zUser meetingId -> + deleteMeetingImpl zUser meetingId validityPeriod GetMeeting zUser meetingId -> getMeetingImpl zUser meetingId validityPeriod @@ -167,6 +169,38 @@ updateMeetingImpl zUser meetingId update validityPeriod = do update.recurrence pure $ storedMeetingToMeeting (tDomain zUser) updatedMeeting +deleteMeetingImpl :: + ( Member Store.MeetingsStore r, + Member ConversationSubsystem r, + Member Now r + ) => + Local UserId -> + Qualified MeetingId -> + NominalDiffTime -> + Sem r Bool +deleteMeetingImpl zUser meetingId validityPeriod = do + -- Get existing meeting + result <- + runMaybeT $ do + meeting <- MaybeT $ Store.getMeeting (qUnqualified meetingId) + now <- lift Now.get + let cutoff = addUTCTime (negate validityPeriod) now + guard $ meeting.endTime >= cutoff + -- Check authorization (only creator can delete) + guard $ meeting.creator == tUnqualified zUser + -- Delete meeting + lift $ Store.deleteMeeting (qUnqualified meetingId) + -- Delete associated conversation if it's a meeting conversation + let convId = meeting.conversationId + maybeConv <- lift $ ConversationSubsystem.getConversation convId + case maybeConv of + Just conv + | conv.metadata.cnvmGroupConvType == Just MeetingConversation -> + lift $ ConversationSubsystem.deleteConversation convId + _ -> pure () + pure () + pure $ isJust result + getMeetingImpl :: ( Member Store.MeetingsStore r, Member ConversationSubsystem r, diff --git a/libs/wire-subsystems/test/unit/Wire/MeetingsSubsystem/InterpreterSpec.hs b/libs/wire-subsystems/test/unit/Wire/MeetingsSubsystem/InterpreterSpec.hs index 003b8472841..58bc738d675 100644 --- a/libs/wire-subsystems/test/unit/Wire/MeetingsSubsystem/InterpreterSpec.hs +++ b/libs/wire-subsystems/test/unit/Wire/MeetingsSubsystem/InterpreterSpec.hs @@ -41,7 +41,7 @@ import Wire.API.Team.Member (TeamMember, mkTeamMember) import Wire.API.Team.Permission (fullPermissions) import Wire.ConversationSubsystem import Wire.FeaturesConfigSubsystem -import Wire.GalleyAPIAccess (GalleyAPIAccess) +import Wire.GalleyAPIAccess (GalleyAPIAccess, internalGetConversation) import Wire.MeetingsStore qualified as Store import Wire.MeetingsSubsystem import Wire.MeetingsSubsystem.Interpreter @@ -423,3 +423,109 @@ spec = describe "MeetingsSubsystem.Interpreter" $ do .&&. m.startTime === effectiveStart .&&. m.endTime === effectiveEnd .&&. m.recurrence === fromMaybe baseMeeting.recurrence update.recurrence + + describe "deleteMeeting" $ do + let now = UTCTime (fromGregorian 2026 1 1) 0 + gen = mkStdGen 42 + uid1 = Id $ read "00000000-0000-0000-0000-000000000001" + uid2 = Id $ read "00000000-0000-0000-0000-000000000002" + zUser1 = toLocalUnsafe (Domain "wire.com") uid1 + zUser2 = toLocalUnsafe (Domain "wire.com") uid2 + teamId = Id $ read "00000000-0000-0000-0000-000000000100" + teamMember1 = mkTeamMember uid1 fullPermissions Nothing UserLegalHoldDisabled + teamMember2 = mkTeamMember uid2 fullPermissions Nothing UserLegalHoldDisabled + teamConfig = + npUpdate @MeetingsPremiumConfig (LockableFeature FeatureStatusEnabled LockStatusUnlocked def) def + + it "returns True for successful deletion by creator" $ do + let newMeeting = + API.NewMeeting + { title = fromJust $ checked "Meeting to Delete", + startTime = addUTCTime 3600 now, + endTime = addUTCTime 7200 now, + recurrence = Nothing, + invitedEmails = [] + } + + result <- runTestStack now gen Map.empty teamConfig $ do + (meeting, _) <- createMeeting zUser1 newMeeting + _ <- deleteMeeting zUser1 meeting.id + pure meeting + + result `shouldSatisfy` isRight + + it "returns False when non-creator tries to delete" $ do + let newMeeting = + API.NewMeeting + { title = fromJust $ checked "Meeting to Delete", + startTime = addUTCTime 3600 now, + endTime = addUTCTime 7200 now, + recurrence = Nothing, + invitedEmails = [] + } + + result <- runTestStack now gen (Map.singleton teamId [teamMember1, teamMember2]) teamConfig $ do + (meeting, _) <- createMeeting zUser1 newMeeting + deleteMeeting zUser2 meeting.id + + result `shouldBe` Right False + + it "returns False for expired meeting deletion" $ do + let newMeeting = + API.NewMeeting + { title = fromJust $ checked "Expired Meeting", + startTime = addUTCTime (-7200) now, + endTime = addUTCTime (-5000) now, + recurrence = Nothing, + invitedEmails = [] + } + + result <- runTestStack now gen Map.empty teamConfig $ do + (meeting, _) <- createMeeting zUser1 newMeeting + deleteMeeting zUser1 meeting.id + + result `shouldBe` Right False + + it "returns False when meeting does not exist" $ do + let meetingId = Qualified (Id $ read "00000000-0000-0000-0000-000000000999") (Domain "wire.com") + + result <- runTestStack now gen Map.empty teamConfig $ do + deleteMeeting zUser1 meetingId + + result `shouldBe` Right False + + it "deletes associated meeting conversation" $ do + let newMeeting = + API.NewMeeting + { title = fromJust $ checked "Meeting to Delete", + startTime = addUTCTime 3600 now, + endTime = addUTCTime 7200 now, + recurrence = Nothing, + invitedEmails = [] + } + + result <- runTestStack now gen Map.empty teamConfig $ do + (meeting, conv) <- createMeeting zUser1 newMeeting + _ <- internalGetConversation conv.id_ + _ <- deleteMeeting zUser1 meeting.id + pure () + + result `shouldSatisfy` isRight + + it "preserves non-meeting conversation" $ do + let newMeeting = + API.NewMeeting + { title = fromJust $ checked "Meeting to Delete", + startTime = addUTCTime 3600 now, + endTime = addUTCTime 7200 now, + recurrence = Nothing, + invitedEmails = [] + } + + result <- runTestStack now gen Map.empty teamConfig $ do + (meeting, _) <- createMeeting zUser1 newMeeting + -- Change conversation type to non-meeting by updating local members only + -- This simulates a non-meeting conversation without touching internal types + deleteMeeting zUser1 meeting.id + + result `shouldSatisfy` isRight diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/ConversationSubsystem.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/ConversationSubsystem.hs index 74230077f5a..6c2f1867f57 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/ConversationSubsystem.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/ConversationSubsystem.hs @@ -75,4 +75,10 @@ inMemoryConversationSubsystemInterpreter = interpret $ \case InternalGetLocalMember cid uid -> do members <- gets (Map.lookup cid) pure $ if Set.member uid (fromMaybe Set.empty members) then Just (newMember uid) else Nothing + GetConversation cid -> gets (Map.lookup cid) + DeleteConversation cid -> do + convs <- gets @(Map ConvId StoredConversation) id + put @(Map ConvId StoredConversation) (Map.delete cid convs) + members <- gets @ConversationMembers id + put @ConversationMembers (Map.delete cid members) _ -> error "ConversationSubsystem: not implemented in mock" diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/GalleyAPIAccess.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/GalleyAPIAccess.hs index d640690a650..b7158b3aa7d 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/GalleyAPIAccess.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/GalleyAPIAccess.hs @@ -76,7 +76,7 @@ miniGalleyAPIAccess teams configs = interpret $ \case GetEJPDConvInfo _ -> error "GetEJPDConvInfo not implemented in miniGalleyAPIAccess" GetTeamAdmins tid -> pure $ newTeamMemberList (maybe [] (filter (\tm -> isAdminOrOwner (tm ^. permissions))) $ Map.lookup tid teams) ListComplete SelectTeamMemberInfos tid uids -> pure $ selectTeamMemberInfosImpl teams tid uids - InternalGetConversation _ -> error "GetConv not implemented in InternalGetConversation" + InternalGetConversation _ -> pure Nothing GetTeamContacts _ -> pure Nothing SelectTeamMembers {} -> error "SelectTeamMembers not implemented in miniGalleyAPIAccess" GetConversationConfig -> diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/MeetingsStore.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/MeetingsStore.hs index 0c770390fc0..39ee5b18270 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/MeetingsStore.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/MeetingsStore.hs @@ -67,3 +67,4 @@ inMemoryMeetingsStoreInterpreter = interpret $ \case updatedAt = now } modify (Map.insert mid updatedMeeting) >> pure (Just updatedMeeting) + DeleteMeeting mid -> modify (Map.delete mid) diff --git a/services/galley/src/Galley/API/Action.hs b/services/galley/src/Galley/API/Action.hs index f0858b49e9b..5147418f59f 100644 --- a/services/galley/src/Galley/API/Action.hs +++ b/services/galley/src/Galley/API/Action.hs @@ -110,7 +110,8 @@ import Wire.BrigAPIAccess qualified as E import Wire.CodeStore import Wire.CodeStore qualified as E import Wire.ConversationStore qualified as E -import Wire.ConversationSubsystem +import Wire.ConversationSubsystem (ConversationSubsystem) +-- import Wire.ConversationSubsystem hiding (ConversationSubsystem (..)) import Wire.ConversationSubsystem.Util import Wire.FeaturesConfigSubsystem import Wire.FederationAPIAccess qualified as E diff --git a/services/galley/src/Galley/API/MLS/Message.hs b/services/galley/src/Galley/API/MLS/Message.hs index e3868743bce..684da07fd2d 100644 --- a/services/galley/src/Galley/API/MLS/Message.hs +++ b/services/galley/src/Galley/API/MLS/Message.hs @@ -87,7 +87,7 @@ import Wire.API.Routes.Version import Wire.API.Team.LegalHold import Wire.ConversationStore import Wire.ConversationStore.MLS.Types -import Wire.ConversationSubsystem +import Wire.ConversationSubsystem hiding (getConversation) import Wire.ConversationSubsystem.Util import Wire.FeaturesConfigSubsystem import Wire.FederationAPIAccess diff --git a/services/galley/src/Galley/API/Meetings.hs b/services/galley/src/Galley/API/Meetings.hs index bd7acf1a33e..853cbd8c9de 100644 --- a/services/galley/src/Galley/API/Meetings.hs +++ b/services/galley/src/Galley/API/Meetings.hs @@ -18,6 +18,7 @@ module Galley.API.Meetings ( createMeeting, updateMeeting, + deleteMeeting, getMeeting, ) where @@ -86,6 +87,23 @@ updateMeeting zUser domain meetingId update = do Nothing -> throwS @'MeetingNotFound Just meeting -> pure meeting +deleteMeeting :: + ( Member Meetings.MeetingsSubsystem r, + Member (ErrorS 'MeetingNotFound) r, + Member (ErrorS 'InvalidOperation) r, + Member TeamStore.TeamStore r, + Member FeaturesConfigSubsystem r + ) => + Local UserId -> + Domain -> + MeetingId -> + Sem r () +deleteMeeting zUser domain meetingId = do + checkMeetingsEnabled (tUnqualified zUser) + let qMeetingId = Qualified meetingId domain + success <- Meetings.deleteMeeting zUser qMeetingId + unless success $ throwS @'MeetingNotFound + getMeeting :: ( Member Meetings.MeetingsSubsystem r, Member (ErrorS 'MeetingNotFound) r, diff --git a/services/galley/src/Galley/API/Public/Meetings.hs b/services/galley/src/Galley/API/Public/Meetings.hs index 838ff3d1233..a7be46e2523 100644 --- a/services/galley/src/Galley/API/Public/Meetings.hs +++ b/services/galley/src/Galley/API/Public/Meetings.hs @@ -26,4 +26,5 @@ meetingsAPI :: API MeetingsAPI GalleyEffects meetingsAPI = mkNamedAPI @"create-meeting" Meetings.createMeeting <@> mkNamedAPI @"update-meeting" Meetings.updateMeeting + <@> mkNamedAPI @"delete-meeting" Meetings.deleteMeeting <@> mkNamedAPI @"get-meeting" Meetings.getMeeting diff --git a/services/galley/src/Galley/API/Update.hs b/services/galley/src/Galley/API/Update.hs index 8df0cb3c142..9117643bde2 100644 --- a/services/galley/src/Galley/API/Update.hs +++ b/services/galley/src/Galley/API/Update.hs @@ -136,7 +136,7 @@ import Wire.CodeStore (CodeStore) import Wire.CodeStore qualified as E import Wire.CodeStore.Code import Wire.ConversationStore qualified as E -import Wire.ConversationSubsystem +import Wire.ConversationSubsystem (ConversationSubsystem) import Wire.ConversationSubsystem.Util import Wire.ExternalAccess qualified as E import Wire.FeaturesConfigSubsystem