Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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-21964
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add meetings listings endpoint `/meetings/list`.
5 changes: 5 additions & 0 deletions integration/test/API/Galley.hs
Original file line number Diff line number Diff line change
Expand Up @@ -1003,3 +1003,8 @@ getMeeting :: (HasCallStack, MakesValue user) => user -> String -> String -> App
getMeeting user domain meetingId = do
req <- baseRequest user Galley Versioned (joinHttpPath ["meetings", domain, meetingId])
submit "GET" req

getMeetingsList :: (HasCallStack, MakesValue user) => user -> App Response
getMeetingsList user = do
req <- baseRequest user Galley Versioned "/meetings/list"
submit "GET" req
68 changes: 68 additions & 0 deletions integration/test/Test/Meetings.hs
Original file line number Diff line number Diff line change
Expand Up @@ -197,3 +197,71 @@ testMeetingUpdateUnauthorized = do
]

putMeeting otherUser domain meetingId update >>= assertStatus 404

testMeetingListEmpty :: (HasCallStack) => App ()
testMeetingListEmpty = do
(owner, _tid, _members) <- createTeam OwnDomain 1
resp <- getMeetingsList owner
assertSuccess resp
meetings <- resp.json & asList
length (meetings :: [Value]) `shouldMatchInt` 0

testMeetingListNoMeetings :: (HasCallStack) => App ()
testMeetingListNoMeetings = do
(owner, _tid, _members) <- createTeam OwnDomain 1
_ <- createTeam OwnDomain 1
resp <- getMeetingsList owner
assertSuccess resp
meetings <- resp.json & asList
length (meetings :: [Value]) `shouldMatchInt` 0

testMeetingListMultiple :: (HasCallStack) => App ()
testMeetingListMultiple = do
(owner, _tid, _members) <- createTeam OwnDomain 1
now <- liftIO getCurrentTime
let firstMeeting = defaultMeetingJson "First Meeting" (addUTCTime 3600 now) (addUTCTime 7200 now) []
secondMeeting = defaultMeetingJson "Second Meeting" (addUTCTime 3600 now) (addUTCTime 7200 now) []
thirdMeeting = defaultMeetingJson "Third Meeting" (addUTCTime 3600 now) (addUTCTime 7200 now) []
r1 <- postMeetings owner firstMeeting
assertSuccess r1
m1 <- getJSON 201 r1
(id1, _) <- getMeetingIdAndDomain m1

r2 <- postMeetings owner secondMeeting
assertSuccess r2
m2 <- getJSON 201 r2
(id2, _) <- getMeetingIdAndDomain m2

r3 <- postMeetings owner thirdMeeting
assertSuccess r3
m3 <- getJSON 201 r3
(id3, _) <- getMeetingIdAndDomain m3

resp <- getMeetingsList owner
assertSuccess resp
meetings <- resp.json & asList
length (meetings :: [Value]) `shouldMatchInt` 3
Comment thread
supersven marked this conversation as resolved.

titles <- forM meetings $ \m -> m %. "title" >>= asString
let expectedTitles = ["First Meeting", "Second Meeting", "Third Meeting"]
(all (`elem` titles) expectedTitles) `shouldMatch` True

fetchedIds <- forM meetings $ \m -> m %. "qualified_id" %. "id" >>= asString
let expectedIds = [id1, id2, id3]
(all (`elem` fetchedIds) expectedIds) `shouldMatch` True

testMeetingListPagination :: (HasCallStack) => App ()
Comment thread
supersven marked this conversation as resolved.
testMeetingListPagination = do
(owner, _tid, _members) <- createTeam OwnDomain 1
now <- liftIO getCurrentTime

-- The internal page size is 1000, so we create 1001 meetings to test pagination.
-- This ensures `hasMore = True` is triggered and multiple pages are fetched.
forM_ [(1 :: Int) .. 1001] $ \i -> do
let meeting = defaultMeetingJson ("Meeting " <> show i) (addUTCTime 3600 now) (addUTCTime 7200 now) []
postMeetings owner meeting >>= assertStatus 201

resp <- getMeetingsList owner
assertSuccess resp
meetings <- resp.json & asList
length (meetings :: [Value]) `shouldMatchInt` 1001
9 changes: 9 additions & 0 deletions libs/wire-api/src/Wire/API/Routes/Public/Galley/Meetings.hs
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,12 @@ type MeetingsAPI =
:> CanThrow 'MeetingNotFound
:> Get '[JSON] Meeting
)
:<|> Named
"list-meetings"
( Summary "List all meetings for the authenticated user"
:> From 'V15
:> ZLocalUser
:> "meetings"
:> "list"
:> Get '[JSON] [Meeting]
)
54 changes: 0 additions & 54 deletions libs/wire-subsystems/src/Wire/ConversationStore.hs
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,6 @@

module Wire.ConversationStore where

import Control.Error (lastMay)
import Data.Aeson qualified as Aeson
import Data.ByteString qualified as BS
import Data.Id
import Data.Misc
import Data.Qualified
Expand All @@ -42,7 +39,6 @@ import Wire.API.MLS.LeafNode
import Wire.API.MLS.SubConversation
import Wire.API.Pagination
import Wire.API.Provider.Service
import Wire.API.Routes.MultiTablePaging
import Wire.ConversationStore.MLS.Types
import Wire.Sem.Paging.Cassandra
import Wire.StoredConversation
Expand Down Expand Up @@ -154,56 +150,6 @@ acceptConnectConversation cid = setConversationType cid One2OneConv
upsertMember :: (Member ConversationStore r) => Local ConvId -> Local UserId -> Sem r [LocalMember]
upsertMember c u = fst <$> upsertMembers (tUnqualified c) (UserList [(tUnqualified u, roleNameWireAdmin)] [])

getConversationIdsResultSet :: forall r. (Member ConversationStore r) => Local UserId -> Range 1 1000 Int32 -> Maybe (Qualified ConvId) -> Sem r (ResultSet (Qualified ConvId))
getConversationIdsResultSet lusr maxIds mLastId = do
case fmap (flip relativeTo lusr) mLastId of
Nothing -> getLocals Nothing
Just (Local (tUnqualified -> lastId)) -> getLocals (Just lastId)
Just (Remote lastId) -> getRemotes (Just lastId) maxIds
where
localDomain = tDomain lusr
usr = tUnqualified lusr

getLocals :: Maybe ConvId -> Sem r (ResultSet (Qualified ConvId))
getLocals lastId = do
localPage <- flip Qualified localDomain <$$> getLocalConversationIds usr lastId maxIds
let remainingSize = fromRange maxIds - fromIntegral (length localPage.resultSetResult)
case checked remainingSize of
Nothing -> pure localPage {resultSetType = ResultSetTruncated}
Just checkedRemaining -> do
remotePage <- getRemotes Nothing checkedRemaining
pure
remotePage
{ resultSetResult = localPage.resultSetResult <> remotePage.resultSetResult
}

getRemotes :: Maybe (Remote ConvId) -> Range 1 1000 Int32 -> Sem r (ResultSet (Qualified ConvId))
getRemotes lastRemote maxRemotes = tUntagged <$$> getRemoteConversationIds usr lastRemote maxRemotes

-- | This function only exists because we use the 'MultiTablePage' type for the
-- endpoint. Since now the pagination is based on the qualified ids, we can
-- remove the use of this type in future API versions.
getConversationIds :: forall r. (Member ConversationStore r) => Local UserId -> Range 1 1000 Int32 -> Maybe ConversationPagingState -> Sem r ConvIdsPage
getConversationIds lusr maxIds pagingState = do
let mLastId = Aeson.decode . BS.fromStrict =<< (.mtpsState) =<< pagingState
resultSet <- getConversationIdsResultSet lusr maxIds mLastId
let mLastResult = lastMay resultSet.resultSetResult
pure
MultiTablePage
{ mtpResults = resultSet.resultSetResult,
mtpHasMore = case resultSet.resultSetType of
ResultSetTruncated -> True
ResultSetComplete -> False,
mtpPagingState =
MultiTablePagingState
{ mtpsTable = case fmap (flip relativeTo lusr) mLastResult of
Just (Local _) -> PagingLocals
Just (Remote _) -> PagingRemotes
Nothing -> PagingRemotes,
mtpsState = BS.toStrict . Aeson.encode <$> mLastResult
}
}

getConvOrSubGroupInfo ::
(Member ConversationStore r) =>
ConvOrSubConvId ->
Expand Down
11 changes: 10 additions & 1 deletion libs/wire-subsystems/src/Wire/ConversationSubsystem.hs
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,12 @@ module Wire.ConversationSubsystem where

import Data.Id
import Data.Qualified
import Data.Range (Range)
import Data.Singletons (Sing)
import Galley.Types.Clients (Clients)
import Imports
import Polysemy
import Wire.API.Conversation (ExtraConversationData, NewConv, NewOne2OneConv)
import Wire.API.Conversation (ConvIdsPage, ConversationPagingState, ExtraConversationData, NewConv, NewOne2OneConv)
import Wire.API.Conversation.Action
import Wire.API.Event.Conversation
import Wire.NotificationSubsystem (LocalConversationUpdate)
Expand Down Expand Up @@ -62,6 +63,14 @@ data ConversationSubsystem m a where
Maybe ConnId ->
Connect ->
ConversationSubsystem m (StoredConversation, Bool)
GetConversations ::
[ConvId] ->
ConversationSubsystem m [StoredConversation]
GetConversationIds ::
Local UserId ->
Range 1 1000 Int32 ->
Maybe ConversationPagingState ->
ConversationSubsystem r ConvIdsPage
InternalGetClientIds :: [UserId] -> ConversationSubsystem m Clients
InternalGetLocalMember ::
ConvId ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ module Wire.ConversationSubsystem.Interpreter
)
where

import Control.Error (headMay)
import Control.Error (headMay, lastMay)
import Control.Lens hiding ((??))
import Data.Aeson qualified as Aeson
import Data.ByteString qualified as BS
import Data.Default
import Data.Id
import Data.Json.Util (ToJSONObject (toJSONObject))
Expand Down Expand Up @@ -54,6 +56,7 @@ import Wire.API.Federation.Client (FederatorClient)
import Wire.API.Federation.Error
import Wire.API.History (History (HistoryPrivate))
import Wire.API.Push.V2 qualified as PushV2
import Wire.API.Routes.MultiTablePaging
import Wire.API.Team
import Wire.API.Team.Collaborator qualified as CollaboratorPermission
import Wire.API.Team.Feature
Expand All @@ -78,6 +81,7 @@ import Wire.LegalHoldStore (LegalHoldStore)
import Wire.NotificationSubsystem as NS
import Wire.Sem.Now (Now)
import Wire.Sem.Now qualified as Now
import Wire.Sem.Paging.Cassandra (ResultSet (..), ResultSetType (..))
import Wire.Sem.Random (Random)
import Wire.Sem.Random qualified as Random
import Wire.StoredConversation hiding (convTeam, id_, localOne2OneConvId)
Expand Down Expand Up @@ -141,6 +145,10 @@ interpretConversationSubsystem = interpret $ \case
createProteusSelfConversationLogic lusr
ConversationSubsystem.CreateConnectConversation lusr conn j ->
createConnectConversationLogic lusr conn j
ConversationSubsystem.GetConversations convIds ->
ConvStore.getConversations convIds
ConversationSubsystem.GetConversationIds lusr maxIds pagingState ->
getConversationIdsImpl lusr maxIds pagingState
InternalGetClientIds uids ->
internalGetClientIdsImpl uids
ConversationSubsystem.InternalGetLocalMember cid uid ->
Expand Down Expand Up @@ -820,3 +828,65 @@ internalGetClientIdsImpl users = do
if isInternal
then fromUserClients <$> lookupClients users
else UserClientIndexStore.getClients users

getConversationIdsResultSetImpl ::
forall r.
(Member ConversationStore r) =>
Local UserId ->
Range 1 1000 Int32 ->
Maybe (Qualified ConvId) ->
Sem r (ResultSet (Qualified ConvId))
getConversationIdsResultSetImpl lusr maxIds mLastId = do
case fmap (flip relativeTo lusr) mLastId of
Nothing -> getLocals Nothing
Just (Local (tUnqualified -> lastId)) -> getLocals (Just lastId)
Just (Remote lastId) -> getRemotes (Just lastId) maxIds
where
localDomain = tDomain lusr
usr = tUnqualified lusr

getLocals :: Maybe ConvId -> Sem r (ResultSet (Qualified ConvId))
getLocals lastId = do
localPage <- flip Qualified localDomain <$$> ConvStore.getLocalConversationIds usr lastId maxIds
let remainingSize = fromRange maxIds - fromIntegral (length localPage.resultSetResult)
case checked remainingSize of
Nothing -> pure localPage {resultSetType = ResultSetTruncated}
Just checkedRemaining -> do
remotePage <- getRemotes Nothing checkedRemaining
pure
remotePage
{ resultSetResult = localPage.resultSetResult <> remotePage.resultSetResult
}

getRemotes :: Maybe (Remote ConvId) -> Range 1 1000 Int32 -> Sem r (ResultSet (Qualified ConvId))
getRemotes lastRemote maxRemotes = tUntagged <$$> ConvStore.getRemoteConversationIds usr lastRemote maxRemotes

-- | This function only exists because we use the 'MultiTablePage' type for the
-- endpoint. Since now the pagination is based on the qualified ids, we can
-- remove the use of this type in future API versions.
getConversationIdsImpl ::
forall r.
(Member ConversationStore r) =>
Local UserId ->
Range 1 1000 Int32 ->
Maybe ConversationPagingState ->
Sem r ConvIdsPage
getConversationIdsImpl lusr maxIds pagingState = do
let mLastId = Aeson.decode . BS.fromStrict =<< (.mtpsState) =<< pagingState
resultSet <- getConversationIdsResultSetImpl lusr maxIds mLastId
let mLastResult = lastMay resultSet.resultSetResult
pure
MultiTablePage
{ mtpResults = resultSet.resultSetResult,
mtpHasMore = case resultSet.resultSetType of
ResultSetTruncated -> True
ResultSetComplete -> False,
mtpPagingState =
MultiTablePagingState
{ mtpsTable = case fmap (flip relativeTo lusr) mLastResult of
Just (Local _) -> PagingLocals
Just (Remote _) -> PagingRemotes
Nothing -> PagingRemotes,
mtpsState = BS.toStrict . Aeson.encode <$> mLastResult
}
}
8 changes: 8 additions & 0 deletions libs/wire-subsystems/src/Wire/MeetingsStore.hs
Original file line number Diff line number Diff line change
Expand Up @@ -148,5 +148,13 @@ data MeetingsStore m a where
GetMeeting ::
MeetingId ->
MeetingsStore m (Maybe StoredMeeting)
ListMeetingsByUser ::
UserId ->
UTCTime ->
MeetingsStore m [StoredMeeting]
ListMeetingsByConversation ::
ConvId ->
UTCTime ->
MeetingsStore m [StoredMeeting]

makeSem ''MeetingsStore
Loading