diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e0af5fa..bdd71fb 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -28,7 +28,7 @@ jobs: ${{ runner.os }}-${{ matrix.ghc }}- ${{ runner.os }}- - run: cabal update - - run: cabal build --enable-tests --flags=ci all + - run: cabal build --enable-tests --flags=ci all --write-ghc-environment-files=always - run: cabal test --enable-tests --flags=ci --test-show-details=direct all - run: cabal haddock all - run: cabal sdist all diff --git a/matrix-client/CHANGELOG.md b/matrix-client/CHANGELOG.md index 085ee03..0d9d3a9 100644 --- a/matrix-client/CHANGELOG.md +++ b/matrix-client/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 0.1.5.0 + +- Replaces MatrixIO with a new mtl style API using ExceptT MatrixError and ReaderT ClientSession. +- Adds loginToken. +- Adds loginTokenWithManager and createSessionWithManager to support custom http Manager. +- Replaces IdentitySession with ClientSession. + ## 0.1.4.0 - Completes The Room API diff --git a/matrix-client/matrix-client.cabal b/matrix-client/matrix-client.cabal index 4cd99c0..4dde553 100644 --- a/matrix-client/matrix-client.cabal +++ b/matrix-client/matrix-client.cabal @@ -1,6 +1,6 @@ cabal-version: 2.4 name: matrix-client -version: 0.1.4.0 +version: 0.1.5.0 synopsis: A matrix client library description: Matrix client is a library to interface with https://matrix.org. @@ -56,6 +56,7 @@ common lib-depends , http-client >= 0.5.0 && < 0.8 , http-client-tls >= 0.2.0 && < 0.4 , http-types >= 0.10.0 && < 0.13 + , mtl , network-uri , profunctors , retry ^>= 0.8 @@ -85,3 +86,4 @@ test-suite unit , hspec >= 2 , matrix-client , text + , doctest diff --git a/matrix-client/src/Network/Matrix/Client.hs b/matrix-client/src/Network/Matrix/Client.hs index d1d0c42..c66e4ed 100644 --- a/matrix-client/src/Network/Matrix/Client.hs +++ b/matrix-client/src/Network/Matrix/Client.hs @@ -6,8 +6,8 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE TypeApplications #-} {-# LANGUAGE TupleSections #-} +{-# LANGUAGE TypeApplications #-} -- | This module contains the client-server API -- https://matrix.org/docs/spec/client_server/r0.6.1 @@ -23,13 +23,17 @@ module Network.Matrix.Client LoginResponse (..), getTokenFromEnv, createSession, + createSessionWithManager, login, loginToken, + loginTokenWithManager, logout, -- * API MatrixM, MatrixIO, + runMatrixM, + runMatrixIO, MatrixError (..), retry, retryWithLog, @@ -113,8 +117,7 @@ module Network.Matrix.Client getFilter, -- * Account data - - AccountData(accountDataType), + AccountData (accountDataType), getAccountData, getAccountData', setAccountData, @@ -136,42 +139,71 @@ module Network.Matrix.Client ) where -import Control.Monad (mzero, void) -import Control.Monad.IO.Class (MonadIO(liftIO)) +import Control.Applicative (Alternative ((<|>))) +import Control.Monad.Catch (MonadMask) +import Control.Monad.Except (throwError) +import Control.Monad.Reader import Data.Aeson (FromJSON (..), ToJSON (..), Value (Object, String), encode, genericParseJSON, genericToJSON, object, withObject, withText, (.:), (.:?), (.=)) import qualified Data.Aeson as Aeson import Data.Aeson.Casing (aesonPrefix, snakeCase) +import Data.Aeson.Types (Parser) +import Data.Bifunctor (bimap) +import qualified Data.ByteString as B +import qualified Data.ByteString.Lazy as BL +import Data.Coerce (coerce) import Data.Hashable (Hashable) +import Data.List (intersperse) import Data.List.NonEmpty (NonEmpty (..)) import Data.Map.Strict (Map, foldrWithKey) -import Data.Maybe (fromMaybe, catMaybes) -import Data.Proxy (Proxy(Proxy)) +import Data.Maybe (catMaybes, fromMaybe) +import Data.Proxy (Proxy (Proxy)) import qualified Data.Text as T import Data.Text.Encoding (decodeUtf8, encodeUtf8) -import GHC.Generics +import GHC.Generics (Generic) import qualified Network.HTTP.Client as HTTP import Network.HTTP.Types.URI (urlEncode) import Network.Matrix.Events import Network.Matrix.Internal + ( ClientSession (..), + DeviceId (..), + InitialDeviceDisplayName (..), + LoginResponse (..), + LoginSecret (..), + MatrixError (..), + MatrixIO, + MatrixM, + MatrixToken (..), + UserID (..), + Username (..), + createSession, + createSessionWithManager, + doRequest, + doRequest', + getTokenFromEnv, + mkLoginRequest', + mkLogoutRequest', + mkManager, + mkRequest, + retry, + retryWithLog, + runMatrixIO, + runMatrixM, + ) import Network.Matrix.Room + ( RoomCreatePreset (..), + RoomCreateRequest (..), + ) import qualified Network.URI as URI -import Data.Coerce -import Data.Bifunctor (bimap) -import Data.List (intersperse) -import Data.Aeson.Types (Parser) -import Control.Applicative -import qualified Data.ByteString as B -import qualified Data.ByteString.Lazy as BL -- $setup -- >>> import Data.Aeson (decode) data LoginCredentials = LoginCredentials - { lUsername :: Username - , lLoginSecret :: LoginSecret - , lBaseUrl :: T.Text - , lDeviceId :: Maybe DeviceId - , lInitialDeviceDisplayName :: Maybe InitialDeviceDisplayName + { lUsername :: Username, + lLoginSecret :: LoginSecret, + lBaseUrl :: T.Text, + lDeviceId :: Maybe DeviceId, + lInitialDeviceDisplayName :: Maybe InitialDeviceDisplayName } mkLoginRequest :: LoginCredentials -> IO HTTP.Request @@ -179,56 +211,41 @@ mkLoginRequest LoginCredentials {..} = mkLoginRequest' lBaseUrl lDeviceId lInitialDeviceDisplayName lUsername lLoginSecret -- | 'login' allows you to generate a session token. -login :: LoginCredentials -> IO ClientSession -login = fmap fst . loginToken +login :: LoginCredentials -> IO (Either MatrixError ClientSession) +login = fmap (fmap fst) . loginToken -- | 'loginToken' allows you to generate a session token and recover the Matrix auth token. -loginToken :: LoginCredentials -> IO (ClientSession, MatrixToken) +loginToken :: LoginCredentials -> IO (Either MatrixError (ClientSession, MatrixToken)) loginToken cred = do - req <- mkLoginRequest cred manager <- mkManager - resp' <- doRequest' manager req - case resp' of - Right LoginResponse {..} -> pure (ClientSession (lBaseUrl cred) (MatrixToken lrAccessToken) manager, (MatrixToken lrAccessToken)) - Left err -> - -- NOTE: There is nothing to recover after a failed login attempt - fail $ show err + loginTokenWithManager manager cred + +-- | 'loginTokenWithManager' allows you to generate a session token with a custom http manager and recover the Matrix auth token. +loginTokenWithManager :: HTTP.Manager -> LoginCredentials -> IO (Either MatrixError (ClientSession, MatrixToken)) +loginTokenWithManager manager cred = do + req <- mkLoginRequest cred + resp <- doRequest' manager req + pure $ case resp of + Left e -> Left e + Right (LoginResponse {..}) -> + let token = MatrixToken lrAccessToken + in Right (ClientSession (lBaseUrl cred) token manager, token) mkLogoutRequest :: ClientSession -> IO HTTP.Request mkLogoutRequest ClientSession {..} = mkLogoutRequest' baseUrl token -- | 'logout' allows you to destroy a session token. logout :: ClientSession -> MatrixIO () -logout session@ClientSession {..} = do - req <- mkLogoutRequest session - fmap (() <$) $ doRequest' @Value manager req - --- | The session record, use 'createSession' to create it. -data ClientSession = ClientSession - { baseUrl :: T.Text, - token :: MatrixToken, - manager :: HTTP.Manager - } - --- | 'createSession' creates the session record. -createSession :: - -- | The matrix client-server base url, e.g. "https://matrix.org" - T.Text -> - -- | The user token - MatrixToken -> - IO ClientSession -createSession baseUrl' token' = ClientSession baseUrl' token' <$> mkManager - -mkRequest :: ClientSession -> Bool -> T.Text -> IO HTTP.Request -mkRequest ClientSession {..} = mkRequest' baseUrl token - -doRequest :: FromJSON a => ClientSession -> HTTP.Request -> MatrixIO a -doRequest ClientSession {..} = doRequest' manager +logout session = do + req <- liftIO $ mkLogoutRequest session + void $ doRequest @Value req + pure () -- | 'getTokenOwner' gets information about the owner of a given access token. -getTokenOwner :: ClientSession -> MatrixIO UserID -getTokenOwner session = - doRequest session =<< mkRequest session True "/_matrix/client/r0/account/whoami" +getTokenOwner :: MatrixIO UserID +getTokenOwner = do + request <- mkRequest True "/_matrix/client/r0/account/whoami" + doRequest request -- | A workaround data type to handle room create error being reported with a {message: "error"} response data CreateRoomResponse = CreateRoomResponse @@ -243,13 +260,13 @@ instance FromJSON CreateRoomResponse where ------------------------------------------------------------------------------- -- Room Event API Calls https://spec.matrix.org/v1.1/client-server-api/#getting-events-for-a-room -getRoomEvent :: ClientSession -> RoomID -> EventID -> MatrixIO RoomEvent -getRoomEvent session (RoomID rid) (EventID eid) = do - request <- mkRequest session True $ "/_matrix/client/v3/rooms/" <> rid <> "/event/" <> eid - doRequest session request +getRoomEvent :: RoomID -> EventID -> MatrixIO RoomEvent +getRoomEvent (RoomID rid) (EventID eid) = do + request <- mkRequest True $ "/_matrix/client/v3/rooms/" <> rid <> "/event/" <> eid + doRequest request -data User = User { userDisplayName :: T.Text, userAvatarUrl :: Maybe T.Text } - deriving Show +data User = User {userDisplayName :: T.Text, userAvatarUrl :: Maybe T.Text} + deriving (Show) instance FromJSON User where parseJSON = withObject "User" $ \o -> do @@ -269,21 +286,21 @@ instance FromJSON JoinedUsers where -- members of the room. The current user must be in the room for it to -- work. -- https://spec.matrix.org/v1.1/client-server-api/#get_matrixclientv3roomsroomidjoined_members -getRoomMembers :: ClientSession -> RoomID -> MatrixIO (Map UserID User) -getRoomMembers session (RoomID rid) = do - request <- mkRequest session True $ "/_matrix/client/v3/rooms/" <> rid <> "/joined_members" - fmap (fmap coerce) $ doRequest @JoinedUsers session request - +getRoomMembers :: RoomID -> MatrixIO (Map UserID User) +getRoomMembers (RoomID rid) = do + request <- mkRequest True $ "/_matrix/client/v3/rooms/" <> rid <> "/joined_members" + fmap coerce $ doRequest @JoinedUsers request + newtype StateKey = StateKey T.Text - deriving stock Show - deriving newtype FromJSON + deriving stock (Show) + deriving newtype (FromJSON) newtype EventType = EventType T.Text - deriving stock Show - deriving newtype FromJSON + deriving stock (Show) + deriving newtype (FromJSON) -data MRCreate = MRCreate { mrcCreator :: UserID, mrcRoomVersion :: Integer } - deriving Show +data MRCreate = MRCreate {mrcCreator :: UserID, mrcRoomVersion :: Integer} + deriving (Show) instance FromJSON MRCreate where parseJSON = withObject "RoomCreate" $ \o -> do @@ -291,54 +308,54 @@ instance FromJSON MRCreate where mrcRoomVersion <- o .: "room_version" pure $ MRCreate {..} -newtype MRName = MRName { mrnName :: T.Text } - deriving Show +newtype MRName = MRName {mrnName :: T.Text} + deriving (Show) instance FromJSON MRName where parseJSON = withObject "RoomName" $ \o -> MRName <$> (o .: "name") -newtype MRCanonicalAlias = MRCanonicalAlias { mrcAlias :: T.Text } - deriving Show +newtype MRCanonicalAlias = MRCanonicalAlias {mrcAlias :: T.Text} + deriving (Show) instance FromJSON MRCanonicalAlias where parseJSON = withObject "RoomCanonicalAlias" $ \o -> MRCanonicalAlias <$> (o .: "alias") -newtype MRGuestAccess = MRGuestAccess { mrGuestAccess :: T.Text } - deriving Show +newtype MRGuestAccess = MRGuestAccess {mrGuestAccess :: T.Text} + deriving (Show) instance FromJSON MRGuestAccess where parseJSON = withObject "GuestAccess" $ \o -> MRGuestAccess <$> (o .: "guest_access") -newtype MRHistoryVisibility = MRHistoryVisibility { mrHistoryVisibility :: T.Text } - deriving Show +newtype MRHistoryVisibility = MRHistoryVisibility {mrHistoryVisibility :: T.Text} + deriving (Show) instance FromJSON MRHistoryVisibility where parseJSON = withObject "HistoryVisibility" $ \o -> MRHistoryVisibility <$> (o .: "history_visibility") -newtype MRTopic = MRTopic { mrTopic :: T.Text } - deriving Show +newtype MRTopic = MRTopic {mrTopic :: T.Text} + deriving (Show) instance FromJSON MRTopic where parseJSON = withObject "RoomTopic" $ \o -> MRTopic <$> (o .: "topic") - -data StateContent = - StRoomCreate MRCreate - -- | StRoomMember MRMember - -- | StRoomPowerLevels MRPowerLevels - -- | StRoomJoinRules MRJoinRules - | StRoomCanonicalAlias MRCanonicalAlias + +data StateContent + = StRoomCreate MRCreate + | -- | StRoomMember MRMember + -- | StRoomPowerLevels MRPowerLevels + -- | StRoomJoinRules MRJoinRules + StRoomCanonicalAlias MRCanonicalAlias | StRoomGuestAccess MRGuestAccess | StRoomHistoryVisibility MRHistoryVisibility | StRoomName MRName | StRoomTopic MRTopic | StOther Value - --- | StSpaceParent MRSpaceParent - deriving Show + --- | StSpaceParent MRSpaceParent + deriving (Show) pStRoomCreate :: Value -> Parser StateContent pStRoomCreate v = StRoomCreate <$> parseJSON v @@ -360,29 +377,30 @@ pStRoomTopic v = StRoomTopic <$> parseJSON v pStRoomOther :: Value -> Parser StateContent pStRoomOther v = StOther <$> parseJSON v - + instance FromJSON StateContent where - parseJSON v = - pStRoomCreate v - <|> pStRoomCanonicAlias v - <|> pStRoomGuestAccess v - <|> pStRoomHistoryVisibility v - <|> pStRoomName v - <|> pStRoomTopic v - <|> pStRoomOther v + parseJSON v = + pStRoomCreate v + <|> pStRoomCanonicAlias v + <|> pStRoomGuestAccess v + <|> pStRoomHistoryVisibility v + <|> pStRoomName v + <|> pStRoomTopic v + <|> pStRoomOther v -- TODO(SOLOMON): Should This constructor be in 'Event'? data StateEvent = StateEvent - { seContent :: StateContent - , seEventId :: EventID - , seOriginServerTimestamp :: Integer - , sePreviousContent :: Maybe Value - , seRoomId :: RoomID - , seSender :: UserID - , seStateKey :: StateKey - , seEventType :: EventType - , seUnsigned :: Maybe Value - } deriving Show + { seContent :: StateContent, + seEventId :: EventID, + seOriginServerTimestamp :: Integer, + sePreviousContent :: Maybe Value, + seRoomId :: RoomID, + seSender :: UserID, + seStateKey :: StateKey, + seEventType :: EventType, + seUnsigned :: Maybe Value + } + deriving (Show) instance FromJSON StateEvent where parseJSON = withObject "StateEvent" $ \o -> do @@ -396,23 +414,23 @@ instance FromJSON StateEvent where seEventType <- o .: "type" seUnsigned <- o .:? "unsigned" pure $ StateEvent {..} - + -- | Get the state events for the current state of a room. -- https://spec.matrix.org/v1.1/client-server-api/#get_matrixclientv3roomsroomidstate -getRoomState :: ClientSession -> RoomID -> MatrixIO [StateEvent] -getRoomState session (RoomID rid) = do - request <- mkRequest session True $ "/_matrix/client/v3/rooms/" <> rid <> "/state" - doRequest session request +getRoomState :: RoomID -> MatrixIO [StateEvent] +getRoomState (RoomID rid) = do + request <- mkRequest True $ "/_matrix/client/v3/rooms/" <> rid <> "/state" + doRequest request -- | Looks up the contents of a state event in a room. If the user is -- joined to the room then the state is taken from the current state -- of the room. If the user has left the room then the state is taken -- from the state of the room when they left. -- https://spec.matrix.org/v1.1/client-server-api/#get_matrixclientv3roomsroomidstateeventtypestatekey -getRoomStateEvent :: ClientSession -> RoomID -> EventType -> StateKey -> MatrixIO StateEvent -getRoomStateEvent session (RoomID rid) (EventType et) (StateKey key) = do - request <- mkRequest session True $ "/_matrix/client/v3/rooms/" <> rid <> "/state" <> et <> "/" <> key - doRequest session request +getRoomStateEvent :: RoomID -> EventType -> StateKey -> MatrixIO StateEvent +getRoomStateEvent (RoomID rid) (EventType et) (StateKey key) = do + request <- mkRequest True $ "/_matrix/client/v3/rooms/" <> rid <> "/state" <> et <> "/" <> key + doRequest request data Dir = F | B @@ -421,14 +439,15 @@ renderDir F = "f" renderDir B = "b" data PaginatedRoomMessages = PaginatedRoomMessages - { chunk :: [RoomEvent] - , end :: Maybe T.Text - -- ^ A token corresponding to the end of chunk. - , start :: T.Text - -- ^ A token corresponding to the start of chunk. - , state :: [StateEvent] - -- ^ A list of state events relevant to showing the chunk. - } deriving Show + { chunk :: [RoomEvent], + -- | A token corresponding to the end of chunk. + end :: Maybe T.Text, + -- | A token corresponding to the start of chunk. + start :: T.Text, + -- | A list of state events relevant to showing the chunk. + state :: [StateEvent] + } + deriving (Show) instance FromJSON PaginatedRoomMessages where parseJSON = withObject "PaginatedRoomMessages" $ \o -> do @@ -439,50 +458,48 @@ instance FromJSON PaginatedRoomMessages where pure $ PaginatedRoomMessages {..} getRoomMessages :: - ClientSession -> - -- | The room to get events from. RoomID -> -- | The direction to return events from. Dir -> -- | A 'RoomEventFilter' to filter returned events with. - Maybe RoomEventFilter -> - -- | The Since value to start returning events from. + Maybe RoomEventFilter -> + -- | The Since value to start returning events from. T.Text -> -- | The maximum number of events to return. Default: 10. Maybe Int -> - -- | The token to stop returning events at. + -- | The token to stop returning events at. Maybe Int -> MatrixIO PaginatedRoomMessages -getRoomMessages session (RoomID rid) dir roomFilter fromToken limit toToken = do - request <- mkRequest session True $ "/_matrix/client/v3/rooms/" <> rid <> "/messages" +getRoomMessages (RoomID rid) dir roomFilter fromToken limit toToken = do + request <- mkRequest True $ "/_matrix/client/v3/rooms/" <> rid <> "/messages" let dir' = "dir=" <> renderDir dir filter' = BL.toStrict . mappend "filter=" . encode <$> roomFilter from' = encodeUtf8 $ "from=" <> fromToken limit' = BL.toStrict . mappend "limit=" . encode <$> limit to' = BL.toStrict . mappend "from=" . encode <$> toToken - queryString = mappend "?" $ mconcat $ intersperse "&" $ [dir', from' ] <> catMaybes [to', limit', filter'] - doRequest session $ request { HTTP.queryString = queryString } + queryString = mappend "?" $ mconcat $ intersperse "&" $ [dir', from'] <> catMaybes [to', limit', filter'] + doRequest $ request {HTTP.queryString = queryString} -- | Send arbitrary state events to a room. These events will be overwritten if -- , and all match. -- https://spec.matrix.org/v1.1/client-server-api/#put_matrixclientv3roomsroomidstateeventtypestatekey -sendRoomStateEvent :: ClientSession -> RoomID -> EventType -> StateKey -> Value -> MatrixIO EventID -sendRoomStateEvent session (RoomID rid) (EventType et) (StateKey key) event = do - request <- mkRequest session True $ "/_matrix/client/v3/rooms/" <> escapeUriComponent rid <> "/state/" <> escapeUriComponent et <> "/" <> escapeUriComponent key - doRequest session $ - request { HTTP.method = "PUT" - , HTTP.requestBody = HTTP.RequestBodyLBS $ encode event - } +sendRoomStateEvent :: RoomID -> EventType -> StateKey -> Value -> MatrixIO EventID +sendRoomStateEvent (RoomID rid) (EventType et) (StateKey key) event = do + request <- mkRequest True $ "/_matrix/client/v3/rooms/" <> escapeUriComponent rid <> "/state/" <> escapeUriComponent et <> "/" <> escapeUriComponent key + doRequest $ + request + { HTTP.method = "PUT", + HTTP.requestBody = HTTP.RequestBodyLBS $ encode event + } newtype TxnID = TxnID T.Text deriving (Show, Eq) -- | This endpoint is used to send a message event to a room. -- https://spec.matrix.org/v1.1/client-server-api/#put_matrixclientv3roomsroomidsendeventtypetxnid -sendMessage :: ClientSession -> RoomID -> Event -> TxnID -> MatrixIO EventID -sendMessage session (RoomID roomId) event (TxnID txnId) = do - request <- mkRequest session True path +sendMessage :: RoomID -> Event -> TxnID -> MatrixIO EventID +sendMessage (RoomID roomId) event (TxnID txnId) = do + request <- mkRequest True path doRequest - session ( request { HTTP.method = "PUT", HTTP.requestBody = HTTP.RequestBodyLBS $ encode event @@ -492,54 +509,54 @@ sendMessage session (RoomID roomId) event (TxnID txnId) = do path = "/_matrix/client/r0/rooms/" <> roomId <> "/send/" <> eventId <> "/" <> txnId eventId = eventType event -redact :: ClientSession -> RoomID -> EventID -> TxnID -> T.Text -> MatrixIO EventID -redact session (RoomID rid) (EventID eid) (TxnID txnid) reason = do - request <- mkRequest session True $ "/_matrix/client/v3/rooms/" <> rid <> "/redact/" <> eid <> "/" <> txnid +redact :: RoomID -> EventID -> TxnID -> T.Text -> MatrixIO EventID +redact (RoomID rid) (EventID eid) (TxnID txnid) reason = do + request <- mkRequest True $ "/_matrix/client/v3/rooms/" <> rid <> "/redact/" <> eid <> "/" <> txnid let body = object ["reason" .= String reason] - doRequest session $ - request { HTTP.method = "PUT" - , HTTP.requestBody = HTTP.RequestBodyLBS $ encode body - } + doRequest $ + request + { HTTP.method = "PUT", + HTTP.requestBody = HTTP.RequestBodyLBS $ encode body + } ------------------------------------------------------------------------------- -- Room API Calls https://spec.matrix.org/v1.1/client-server-api/#rooms-1 -- | Create a new room with various configuration options. -- https://spec.matrix.org/v1.1/client-server-api/#post_matrixclientv3createroom -createRoom :: ClientSession -> RoomCreateRequest -> MatrixIO RoomID -createRoom session rcr = do - request <- mkRequest session True "/_matrix/client/v3/createRoom" - toRoomID - <$> doRequest - session - ( request - { HTTP.method = "POST", - HTTP.requestBody = HTTP.RequestBodyLBS $ encode rcr - } - ) +createRoom :: RoomCreateRequest -> MatrixIO RoomID +createRoom rcr = do + request <- mkRequest True "/_matrix/client/v3/createRoom" + resp <- + doRequest $ + request + { HTTP.method = "POST", + HTTP.requestBody = HTTP.RequestBodyLBS $ encode rcr + } + toRoomID resp where - toRoomID :: Either MatrixError CreateRoomResponse -> Either MatrixError RoomID - toRoomID resp = case resp of - Left err -> Left err - Right crr -> case (crrID crr, crrMessage crr) of + toRoomID :: CreateRoomResponse -> MatrixIO RoomID + toRoomID crr = + case (crrID crr, crrMessage crr) of (Just roomID, _) -> pure $ RoomID roomID - (_, Just message) -> Left $ MatrixError "UNKNOWN" message Nothing - _ -> Left $ MatrixError "UNKOWN" "" Nothing + (_, Just message) -> throwError $ MatrixError "UNKNOWN" message Nothing + _ -> throwError $ MatrixError "UNKOWN" "" Nothing newtype RoomAlias = RoomAlias T.Text deriving (Show, Eq, Ord, Hashable) data ResolvedRoomAlias = ResolvedRoomAlias - { roomAlias :: RoomAlias - , roomID :: RoomID - -- ^ The room ID for this room alias. - , servers :: [T.Text] - -- ^ A list of servers that are aware of this room alias. - } deriving Show + { roomAlias :: RoomAlias, + -- | The room ID for this room alias. + roomID :: RoomID, + -- | A list of servers that are aware of this room alias. + servers :: [T.Text] + } + deriving (Show) -- | Boilerplate data type for an aeson instance data RoomAliasMetadata = RoomAliasMetadata - { ramRoomID :: RoomID - , ramServers :: [T.Text] + { ramRoomID :: RoomID, + ramServers :: [T.Text] } instance FromJSON RoomAliasMetadata where @@ -550,30 +567,29 @@ instance FromJSON RoomAliasMetadata where -- | Requests that the server resolve a room alias to a room ID. -- https://spec.matrix.org/v1.1/client-server-api/#get_matrixclientv3directoryroomroomalias -resolveRoomAlias :: ClientSession -> RoomAlias -> MatrixIO ResolvedRoomAlias -resolveRoomAlias session r@(RoomAlias alias) = do - request <- mkRequest session True $ "/_matrix/client/v3/directory/room/" <> escapeUriComponent alias - resp <- doRequest session $ request { HTTP.method = "GET" } - case resp of - Left err -> pure $ Left err - Right RoomAliasMetadata {..} -> pure $ Right $ ResolvedRoomAlias r ramRoomID ramServers +resolveRoomAlias :: RoomAlias -> MatrixIO ResolvedRoomAlias +resolveRoomAlias r@(RoomAlias alias) = do + request <- mkRequest True $ "/_matrix/client/v3/directory/room/" <> escapeUriComponent alias + RoomAliasMetadata {..} <- doRequest $ request {HTTP.method = "GET"} + pure $ ResolvedRoomAlias r ramRoomID ramServers -- | Create a mapping of room alias to room ID. -- https://spec.matrix.org/v1.1/client-server-api/#put_matrixclientv3directoryroomroomalias -setRoomAlias :: ClientSession -> RoomAlias -> RoomID -> MatrixIO () -setRoomAlias session (RoomAlias alias) (RoomID roomId)= do - request <- mkRequest session True $ "/_matrix/client/v3/directory/room/" <> escapeUriComponent alias - doRequest - session $ - request { HTTP.method = "PUT" - , HTTP.requestBody = HTTP.RequestBodyLBS $ encode $ object [("room_id" .= roomId)] - } +setRoomAlias :: RoomAlias -> RoomID -> MatrixIO () +setRoomAlias (RoomAlias alias) (RoomID roomId) = do + request <- mkRequest True $ "/_matrix/client/v3/directory/room/" <> escapeUriComponent alias + doRequest $ + request + { HTTP.method = "PUT", + HTTP.requestBody = HTTP.RequestBodyLBS $ encode $ object [("room_id" .= roomId)] + } + -- | Delete a mapping of room alias to room ID. -- https://spec.matrix.org/v1.1/client-server-api/#delete_matrixclientv3directoryroomroomalias -deleteRoomAlias :: ClientSession -> RoomAlias -> MatrixIO () -deleteRoomAlias session (RoomAlias alias) = do - request <- mkRequest session True $ "/_matrix/client/v3/directory/room/" <> escapeUriComponent alias - doRequest session $ request { HTTP.method = "DELETE" } +deleteRoomAlias :: RoomAlias -> MatrixIO () +deleteRoomAlias (RoomAlias alias) = do + request <- mkRequest True $ "/_matrix/client/v3/directory/room/" <> escapeUriComponent alias + doRequest $ request {HTTP.method = "DELETE"} data ResolvedAliases = ResolvedAliases [RoomAlias] @@ -581,18 +597,15 @@ instance FromJSON ResolvedAliases where parseJSON = withObject "ResolvedAliases" $ \o -> do aliases <- o .: "aliases" pure $ ResolvedAliases (RoomAlias <$> aliases) - + -- | Get a list of aliases maintained by the local server for the given room. -- https://spec.matrix.org/v1.1/client-server-api/#get_matrixclientv3roomsroomidaliases -getRoomAliases :: ClientSession -> RoomID -> MatrixIO [RoomAlias] -getRoomAliases session (RoomID rid) = do - request <- mkRequest session True $ "/_matrix/client/v3/rooms/" <> rid <> "/aliases" - resp <- doRequest - session $ - request { HTTP.method = "GET" } - case resp of - Left err -> pure $ Left err - Right (ResolvedAliases aliases) -> pure $ Right aliases +getRoomAliases :: RoomID -> MatrixIO [RoomAlias] +getRoomAliases (RoomID rid) = do + request <- mkRequest True $ "/_matrix/client/v3/rooms/" <> rid <> "/aliases" + ResolvedAliases aliases <- doRequest $ request {HTTP.method = "GET"} + pure aliases + -- | A newtype wrapper to decoded nested list -- -- >>> decode "{\"joined_rooms\": [\"!foo:example.com\"]}" :: Maybe JoinedRooms @@ -607,11 +620,11 @@ instance FromJSON JoinedRooms where -- | Returns a list of the user’s current rooms. -- https://spec.matrix.org/v1.1/client-server-api/#get_matrixclientv3joined_rooms -getJoinedRooms :: ClientSession -> MatrixIO [RoomID] -getJoinedRooms session = do - request <- mkRequest session True "/_matrix/client/r0/joined_rooms" - response <- doRequest session request - pure $ unRooms <$> response +getJoinedRooms :: MatrixIO [RoomID] +getJoinedRooms = do + request <- mkRequest True "/_matrix/client/r0/joined_rooms" + response <- doRequest request + pure $ unRooms response newtype RoomID = RoomID T.Text deriving (Show, Eq, Ord, Hashable) @@ -622,119 +635,111 @@ instance FromJSON RoomID where -- | Invites a user to participate in a particular room. They do not -- start participating in the room until they actually join the room. -- https://spec.matrix.org/v1.1/client-server-api/#post_matrixclientv3roomsroomidinvite -inviteToRoom :: ClientSession -> RoomID -> UserID -> Maybe T.Text -> MatrixIO () -inviteToRoom session (RoomID rid) (UserID uid) reason = do - request <- mkRequest session True $ "/_matrix/client/v3/rooms/" <> rid <> "/invite" +inviteToRoom :: RoomID -> UserID -> Maybe T.Text -> MatrixIO () +inviteToRoom (RoomID rid) (UserID uid) reason = do + request <- mkRequest True $ "/_matrix/client/v3/rooms/" <> rid <> "/invite" let body = object $ [("user_id", toJSON uid)] <> catMaybes [fmap (("reason",) . toJSON) reason] - doRequest session $ - request { HTTP.method = "POST" - , HTTP.requestBody = HTTP.RequestBodyLBS $ encode body - } + doRequest $ + request + { HTTP.method = "POST", + HTTP.requestBody = HTTP.RequestBodyLBS $ encode body + } -- | Note that this API takes either a room ID or alias, unlike 'joinRoomById' -- https://spec.matrix.org/v1.1/client-server-api/#post_matrixclientv3joinroomidoralias -joinRoom :: ClientSession -> T.Text -> MatrixIO RoomID -joinRoom session roomName = do - request <- mkRequest session True $ "/_matrix/client/r0/join/" <> roomNameUrl - doRequest session (request {HTTP.method = "POST"}) +joinRoom :: T.Text -> MatrixIO RoomID +joinRoom roomName = do + request <- mkRequest True $ "/_matrix/client/r0/join/" <> roomNameUrl + doRequest (request {HTTP.method = "POST"}) where roomNameUrl = decodeUtf8 . urlEncode True . encodeUtf8 $ roomName -- | Starts a user participating in a particular room, if that user is -- allowed to participate in that room. -- https://spec.matrix.org/v1.1/client-server-api/#post_matrixclientv3roomsroomidjoin -joinRoomById :: ClientSession -> RoomID -> MatrixIO RoomID -joinRoomById session (RoomID roomId) = do - request <- mkRequest session True $ "/_matrix/client/r0/rooms/" <> roomId <> "/join" - doRequest session (request {HTTP.method = "POST"}) +joinRoomById :: RoomID -> MatrixIO RoomID +joinRoomById (RoomID roomId) = do + request <- mkRequest True $ "/_matrix/client/r0/rooms/" <> roomId <> "/join" + doRequest (request {HTTP.method = "POST"}) -- | This API “knocks” on the room to ask for permission to join, if -- the user is allowed to knock on the room. -- https://spec.matrix.org/v1.1/client-server-api/#post_matrixclientv3knockroomidoralias -knockOnRoom :: ClientSession -> Either RoomID RoomAlias -> [T.Text] -> Maybe T.Text -> MatrixIO RoomID -knockOnRoom session room servers reason = do - request <- mkRequest session True $ " /_matrix/client/v3/knock/" <> indistinct (bimap coerce coerce room) +knockOnRoom :: Either RoomID RoomAlias -> [T.Text] -> Maybe T.Text -> MatrixIO RoomID +knockOnRoom room servers reason = do + request <- mkRequest True $ " /_matrix/client/v3/knock/" <> indistinct (bimap coerce coerce room) let body = object $ catMaybes [fmap (("reason",) . toJSON) reason] - doRequest session $ - request { HTTP.method = "POST" - , HTTP.requestBody = HTTP.RequestBodyLBS $ encode body - , HTTP.queryString = encodeUtf8 $ "?server_name=" <> mconcat (intersperse "," servers) - } + doRequest $ + request + { HTTP.method = "POST", + HTTP.requestBody = HTTP.RequestBodyLBS $ encode body, + HTTP.queryString = encodeUtf8 $ "?server_name=" <> mconcat (intersperse "," servers) + } + +ensureEmptyObject :: Value -> () +ensureEmptyObject value = case value of + Object xs | xs == mempty -> () + _anyOther -> error $ "Unknown leave response: " <> show value -- | Stops remembering a particular room. -- https://spec.matrix.org/v1.1/client-server-api/#post_matrixclientv3roomsroomidforget -forgetRoom :: ClientSession -> RoomID -> MatrixIO () -forgetRoom session (RoomID roomId) = do - request <- mkRequest session True $ "/_matrix/client/v3/rooms/" <> roomId <> "/forget" - fmap ensureEmptyObject <$> doRequest session (request {HTTP.method = "POST"}) - where - ensureEmptyObject :: Value -> () - ensureEmptyObject value = case value of - Object xs | xs == mempty -> () - _anyOther -> error $ "Unknown forget response: " <> show value - +forgetRoom :: RoomID -> MatrixIO () +forgetRoom (RoomID roomId) = do + request <- mkRequest True $ "/_matrix/client/v3/rooms/" <> roomId <> "/forget" + resp <- doRequest (request {HTTP.method = "POST"}) + pure $ ensureEmptyObject resp -- | Stop participating in a particular room. -- https://spec.matrix.org/v1.1/client-server-api/#post_matrixclientv3roomsroomidleave -leaveRoomById :: ClientSession -> RoomID -> MatrixIO () -leaveRoomById session (RoomID roomId) = do - request <- mkRequest session True $ "/_matrix/client/r0/rooms/" <> roomId <> "/leave" - fmap ensureEmptyObject <$> doRequest session (request {HTTP.method = "POST"}) - where - ensureEmptyObject :: Value -> () - ensureEmptyObject value = case value of - Object xs | xs == mempty -> () - _anyOther -> error $ "Unknown leave response: " <> show value +leaveRoomById :: RoomID -> MatrixIO () +leaveRoomById (RoomID roomId) = do + request <- mkRequest True $ "/_matrix/client/r0/rooms/" <> roomId <> "/leave" + resp <- doRequest (request {HTTP.method = "POST"}) + pure $ ensureEmptyObject resp -- | Kick a user from the room. -- https://spec.matrix.org/v1.1/client-server-api/#post_matrixclientv3roomsroomidkick -kickUser :: ClientSession -> RoomID -> UserID -> Maybe T.Text -> MatrixIO () -kickUser session (RoomID roomId) (UserID uid) reason = do - request <- mkRequest session True $ "/_matrix/client/v3/rooms/" <> roomId <> "/kick" +kickUser :: RoomID -> UserID -> Maybe T.Text -> MatrixIO () +kickUser (RoomID roomId) (UserID uid) reason = do + request <- mkRequest True $ "/_matrix/client/v3/rooms/" <> roomId <> "/kick" let body = object $ [("user_id", toJSON uid)] <> catMaybes [fmap (("reason",) . toJSON) reason] - fmap (fmap ensureEmptyObject) $ doRequest session $ - request { HTTP.method = "POST" - , HTTP.requestBody = HTTP.RequestBodyLBS $ encode body - } - where - ensureEmptyObject :: Value -> () - ensureEmptyObject value = case value of - Object xs | xs == mempty -> () - _anyOther -> error $ "Unknown leave response: " <> show value + resp <- + doRequest $ + request + { HTTP.method = "POST", + HTTP.requestBody = HTTP.RequestBodyLBS $ encode body + } + pure $ ensureEmptyObject resp -- | Ban a user in the room. If the user is currently in the room, also kick them. -- https://spec.matrix.org/v1.1/client-server-api/#post_matrixclientv3roomsroomidban -banUser :: ClientSession -> RoomID -> UserID -> Maybe T.Text -> MatrixIO () -banUser session (RoomID roomId) (UserID uid) reason = do - request <- mkRequest session True $ "/_matrix/client/v3/rooms/" <> roomId <> "/ban" +banUser :: RoomID -> UserID -> Maybe T.Text -> MatrixIO () +banUser (RoomID roomId) (UserID uid) reason = do + request <- mkRequest True $ "/_matrix/client/v3/rooms/" <> roomId <> "/ban" let body = object $ [("user_id", toJSON uid)] <> catMaybes [fmap (("reason",) . toJSON) reason] - fmap (fmap ensureEmptyObject) $ doRequest session $ - request { HTTP.method = "POST" - , HTTP.requestBody = HTTP.RequestBodyLBS $ encode body - } - where - ensureEmptyObject :: Value -> () - ensureEmptyObject value = case value of - Object xs | xs == mempty -> () - _anyOther -> error $ "Unknown leave response: " <> show value + resp <- + doRequest $ + request + { HTTP.method = "POST", + HTTP.requestBody = HTTP.RequestBodyLBS $ encode body + } + pure $ ensureEmptyObject resp -- | Unban a user from the room. This allows them to be invited to the -- room, and join if they would otherwise be allowed to join according -- to its join rules. -- https://spec.matrix.org/v1.1/client-server-api/#post_matrixclientv3roomsroomidunban -unbanUser :: ClientSession -> RoomID -> UserID -> Maybe T.Text -> MatrixIO () -unbanUser session (RoomID roomId) (UserID uid) reason = do - request <- mkRequest session True $ "/_matrix/client/v3/rooms/" <> roomId <> "/unban" +unbanUser :: RoomID -> UserID -> Maybe T.Text -> MatrixIO () +unbanUser (RoomID roomId) (UserID uid) reason = do + request <- mkRequest True $ "/_matrix/client/v3/rooms/" <> roomId <> "/unban" let body = object $ [("user_id", toJSON uid)] <> catMaybes [fmap (("reason",) . toJSON) reason] - fmap (fmap ensureEmptyObject) $ doRequest session $ - request { HTTP.method = "POST" - , HTTP.requestBody = HTTP.RequestBodyLBS $ encode body - } - where - ensureEmptyObject :: Value -> () - ensureEmptyObject value = case value of - Object xs | xs == mempty -> () - _anyOther -> error $ "Unknown leave response: " <> show value + resp <- + doRequest $ + request + { HTTP.method = "POST", + HTTP.requestBody = HTTP.RequestBodyLBS $ encode body + } + pure $ ensureEmptyObject resp data Visibility = Public | Private deriving (Show) @@ -750,60 +755,60 @@ instance FromJSON Visibility where "private" -> pure Private _ -> mzero -newtype GetVisibility = GetVisibility { getVisibility :: Visibility } +newtype GetVisibility = GetVisibility {getVisibility :: Visibility} instance FromJSON GetVisibility where parseJSON = withObject "GetVisibility" $ \o -> do getVisibility <- o .: "visibility" pure $ GetVisibility {..} - + -- | Gets the visibility of a given room on the server’s public room directory. -- https://spec.matrix.org/v1.1/client-server-api/#get_matrixclientv3directorylistroomroomid -checkRoomVisibility :: ClientSession -> RoomID -> MatrixIO Visibility -checkRoomVisibility session (RoomID rid) = do - request <- mkRequest session True $ "/_matrix/client/v3/directory/list/room/" <> rid - fmap (fmap getVisibility) $ doRequest session request - +checkRoomVisibility :: RoomID -> MatrixIO Visibility +checkRoomVisibility (RoomID rid) = do + request <- mkRequest True $ "/_matrix/client/v3/directory/list/room/" <> rid + resp <- doRequest request + pure $ getVisibility resp + -- | Sets the visibility of a given room in the server’s public room directory. -- https://spec.matrix.org/v1.1/client-server-api/#put_matrixclientv3directorylistroomroomid -setRoomVisibility :: ClientSession -> RoomID -> Visibility -> MatrixIO () -setRoomVisibility session (RoomID rid) visibility = do - request <- mkRequest session True $ "/_matrix/client/v3/directory/list/room/" <> rid +setRoomVisibility :: RoomID -> Visibility -> MatrixIO () +setRoomVisibility (RoomID rid) visibility = do + request <- mkRequest True $ "/_matrix/client/v3/directory/list/room/" <> rid let body = object $ [("visibility", toJSON visibility)] - fmap (fmap ensureEmptyObject) $ doRequest session $ - request { HTTP.method = "PUT" - , HTTP.requestBody = HTTP.RequestBodyLBS $ encode body - } - where - ensureEmptyObject :: Value -> () - ensureEmptyObject value = case value of - Object xs | xs == mempty -> () - _anyOther -> error $ "Unknown setRoomVisibility response: " <> show value + resp <- + doRequest $ + request + { HTTP.method = "PUT", + HTTP.requestBody = HTTP.RequestBodyLBS $ encode body + } + pure $ ensureEmptyObject resp -- | A pagination token from a previous request, allowing clients to -- get the next (or previous) batch of rooms. The direction of -- pagination is specified solely by which token is supplied, rather -- than via an explicit flag. -newtype PaginationChunk = PaginationChunk { getChunk :: T.Text } +newtype PaginationChunk = PaginationChunk {getChunk :: T.Text} deriving stock (Show) deriving newtype (ToJSON, FromJSON) data Room = Room - { aliases :: Maybe [T.Text] - , avatarUrl :: Maybe T.Text - , canonicalAlias :: Maybe T.Text - , guestCanJoin :: Bool - , joinRule :: Maybe T.Text - , name :: Maybe T.Text - , numJoinedMembers :: Int - , roomId :: RoomID - , topic :: Maybe T.Text - , worldReadable :: Bool - } deriving Show + { aliases :: Maybe [T.Text], + avatarUrl :: Maybe T.Text, + canonicalAlias :: Maybe T.Text, + guestCanJoin :: Bool, + joinRule :: Maybe T.Text, + name :: Maybe T.Text, + numJoinedMembers :: Int, + roomId :: RoomID, + topic :: Maybe T.Text, + worldReadable :: Bool + } + deriving (Show) instance FromJSON Room where parseJSON = withObject "Room" $ \o -> do - aliases <- o .:? "aliases" + aliases <- o .:? "aliases" avatarUrl <- o .:? "avatar_url" canonicalAlias <- o .:? "canonical_alias" guestCanJoin <- o .: "guest_can_join" @@ -816,11 +821,12 @@ instance FromJSON Room where pure $ Room {..} data PublicRooms = PublicRooms - { prChunk :: [Room] - , prNextBatch :: Maybe PaginationChunk - , prPrevBatch :: Maybe PaginationChunk - , prTotalRoomCountEstimate :: Maybe Int - } deriving Show + { prChunk :: [Room], + prNextBatch :: Maybe PaginationChunk, + prPrevBatch :: Maybe PaginationChunk, + prTotalRoomCountEstimate :: Maybe Int + } + deriving (Show) instance FromJSON PublicRooms where parseJSON = withObject "PublicRooms" $ \o -> do @@ -832,34 +838,35 @@ instance FromJSON PublicRooms where -- | Lists the public rooms on the server. -- https://spec.matrix.org/v1.1/client-server-api/#get_matrixclientv3publicrooms -getPublicRooms :: ClientSession -> Maybe Int -> Maybe PaginationChunk -> MatrixIO PublicRooms -getPublicRooms session limit chunk = do - request <- mkRequest session True "/_matrix/client/v3/publicRooms" +getPublicRooms :: Maybe Int -> Maybe PaginationChunk -> MatrixIO PublicRooms +getPublicRooms limit chunk = do + request <- mkRequest True "/_matrix/client/v3/publicRooms" let since = fmap (mappend "since=" . getChunk) chunk limit' = fmap (mappend "limit=" . tshow) limit queryString = encodeUtf8 $ mconcat $ intersperse "&" $ catMaybes [since, limit'] - doRequest session $ - request { HTTP.queryString = queryString } + doRequest $ + request {HTTP.queryString = queryString} newtype ThirdPartyInstanceId = ThirdPartyInstanceId T.Text deriving (FromJSON, ToJSON) -- | Lists the public rooms on the server, with optional filter. -- https://spec.matrix.org/v1.1/client-server-api/#post_matrixclientv3publicrooms -getPublicRooms' :: ClientSession -> Maybe Int -> Maybe PaginationChunk -> Maybe T.Text -> Maybe Bool -> Maybe ThirdPartyInstanceId-> MatrixIO PublicRooms -getPublicRooms' session limit chunk searchTerm includeAllNetworks thirdPartyId = do - request <- mkRequest session True "/_matrix/client/v3/publicRooms" - let filter' = object $ catMaybes [ fmap (("generic_search_term",) . toJSON) searchTerm] +getPublicRooms' :: Maybe Int -> Maybe PaginationChunk -> Maybe T.Text -> Maybe Bool -> Maybe ThirdPartyInstanceId -> MatrixIO PublicRooms +getPublicRooms' limit chunk searchTerm includeAllNetworks thirdPartyId = do + request <- mkRequest True "/_matrix/client/v3/publicRooms" + let filter' = object $ catMaybes [fmap (("generic_search_term",) . toJSON) searchTerm] since = fmap (("since",) . toJSON) chunk limit' = fmap (("limit",) . toJSON) limit includeAllNetworks' = fmap (("include_all_networks",) . toJSON) includeAllNetworks thirdPartyId' = fmap (("third_party_instance_id",) . toJSON) thirdPartyId - body = object $ [("filter", filter')] <> catMaybes [ since, limit', includeAllNetworks', thirdPartyId' ] - doRequest session $ - request { HTTP.method = "POST" - , HTTP.requestBody = HTTP.RequestBodyLBS $ encode body - } - + body = object $ [("filter", filter')] <> catMaybes [since, limit', includeAllNetworks', thirdPartyId'] + doRequest $ + request + { HTTP.method = "POST", + HTTP.requestBody = HTTP.RequestBodyLBS $ encode body + } + ------------------------------------------------------------------------------- -- https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-user-userid-filter newtype FilterID = FilterID T.Text deriving (Show, Eq, Hashable) @@ -1021,18 +1028,15 @@ instance FromJSON Filter where -- | Upload a new filter definition to the homeserver -- https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-user-userid-filter createFilter :: - -- | The client session, use 'createSession' to get one. - ClientSession -> -- | The userID, use 'getTokenOwner' to get it. UserID -> -- | The filter definition, use 'defaultFilter' to create one or use the 'messageFilter' example. Filter -> -- | The function returns a 'FilterID' suitable for the 'sync' function. MatrixIO FilterID -createFilter session (UserID userID) body = do - request <- mkRequest session True path +createFilter (UserID userID) body = do + request <- mkRequest True path doRequest - session ( request { HTTP.method = "POST", HTTP.requestBody = HTTP.RequestBodyLBS $ encode body @@ -1041,9 +1045,9 @@ createFilter session (UserID userID) body = do where path = "/_matrix/client/r0/user/" <> userID <> "/filter" -getFilter :: ClientSession -> UserID -> FilterID -> MatrixIO Filter -getFilter session (UserID userID) (FilterID filterID) = - doRequest session =<< mkRequest session True path +getFilter :: UserID -> FilterID -> MatrixIO Filter +getFilter (UserID userID) (FilterID filterID) = + doRequest =<< mkRequest True path where path = "/_matrix/client/r0/user/" <> userID <> "/filter/" <> filterID @@ -1105,8 +1109,8 @@ data SyncResult = SyncResult deriving (Show, Eq, Generic) data SyncResultRoom = SyncResultRoom - { srrJoin :: Maybe (Map T.Text JoinedRoomSync) - , srrInvite :: Maybe (Map T.Text InvitedRoomSync) + { srrJoin :: Maybe (Map T.Text JoinedRoomSync), + srrInvite :: Maybe (Map T.Text InvitedRoomSync) } deriving (Show, Eq, Generic) @@ -1199,24 +1203,22 @@ mkReply room re mt = EventUnknown x -> error $ "Can't reply to " <> show x in EventRoomReply eventID (RoomMessageText newMessage) -sync :: ClientSession -> Maybe FilterID -> Maybe T.Text -> Maybe Presence -> Maybe Int -> MatrixIO SyncResult -sync session filterM sinceM presenceM timeoutM = do - request <- mkRequest session True "/_matrix/client/r0/sync" - doRequest session (HTTP.setQueryString qs request) +sync :: MonadIO m => Maybe FilterID -> Maybe T.Text -> Maybe Presence -> Maybe Int -> MatrixM m SyncResult +sync filterM' sinceM presenceM timeoutM = do + request <- mkRequest True "/_matrix/client/r0/sync" + doRequest (HTTP.setQueryString qs request) where toQs name = \case Nothing -> [] Just v -> [(name, Just . encodeUtf8 $ v)] qs = - toQs "filter" (unFilterID <$> filterM) + toQs "filter" (unFilterID <$> filterM') <> toQs "since" sinceM <> toQs "set_presence" (tshow <$> presenceM) <> toQs "timeout" (tshow <$> timeoutM) syncPoll :: - (MonadIO m) => - -- | The client session, use 'createSession' to get one. - ClientSession -> + (MonadMask m, MonadIO m) => -- | A sync filter, use 'createFilter' to get one. Maybe FilterID -> -- | A since value, get it from a previous sync result using the 'srNextBatch' field. @@ -1227,13 +1229,12 @@ syncPoll :: (SyncResult -> m ()) -> -- | This function does not return unless there is an error. MatrixM m () -syncPoll session filterM sinceM presenceM cb = go sinceM +syncPoll filterM' sinceM presenceM cb = go sinceM where go since = do - syncResultE <- liftIO $ retry $ sync session filterM since presenceM (Just 10_000) - case syncResultE of - Left err -> pure (Left err) - Right sr -> cb sr >> go (Just (srNextBatch sr)) + sr <- retry $ sync filterM' since presenceM (Just 10_000) + lift (cb sr) + go (Just (srNextBatch sr)) -- | Extract room events from a sync result getTimelines :: SyncResult -> [(RoomID, NonEmpty RoomEvent)] @@ -1298,18 +1299,21 @@ instance ToJSON SyncResultRoom where instance FromJSON SyncResultRoom where parseJSON = genericParseJSON aesonOptions -getAccountData' :: (FromJSON a) => ClientSession -> UserID -> T.Text -> MatrixIO a -getAccountData' session userID t = - mkRequest session True (accountDataPath userID t) >>= doRequest session - -setAccountData' :: (ToJSON a) => ClientSession -> UserID -> T.Text -> a -> MatrixIO () -setAccountData' session userID t value = do - request <- mkRequest session True $ accountDataPath userID t - void <$> (doRequest session $ request - { HTTP.method = "PUT" - , HTTP.requestBody = HTTP.RequestBodyLBS $ encode value - } :: MatrixIO Aeson.Object - ) +getAccountData' :: (FromJSON a) => UserID -> T.Text -> MatrixIO a +getAccountData' userID t = + mkRequest True (accountDataPath userID t) >>= doRequest + +setAccountData' :: (ToJSON a) => UserID -> T.Text -> a -> MatrixIO () +setAccountData' userID t value = do + request <- mkRequest True $ accountDataPath userID t + void $ + ( doRequest $ + request + { HTTP.method = "PUT", + HTTP.requestBody = HTTP.RequestBodyLBS $ encode value + } :: + MatrixIO Aeson.Object + ) accountDataPath :: UserID -> T.Text -> T.Text accountDataPath (UserID userID) t = @@ -1318,13 +1322,15 @@ accountDataPath (UserID userID) t = class (FromJSON a, ToJSON a) => AccountData a where accountDataType :: proxy a -> T.Text -getAccountData :: forall a. (AccountData a) => ClientSession -> UserID -> MatrixIO a -getAccountData session userID = getAccountData' session userID $ - accountDataType (Proxy :: Proxy a) +getAccountData :: forall a. (AccountData a) => UserID -> MatrixIO a +getAccountData userID = + getAccountData' userID $ + accountDataType (Proxy :: Proxy a) -setAccountData :: forall a. (AccountData a) => ClientSession -> UserID -> a -> MatrixIO () -setAccountData session userID = setAccountData' session userID $ - accountDataType (Proxy :: Proxy a) +setAccountData :: forall a. (AccountData a) => UserID -> a -> MatrixIO () +setAccountData userID = + setAccountData' userID $ + accountDataType (Proxy :: Proxy a) ------------------------------------------------------------------------------- -- Utils diff --git a/matrix-client/src/Network/Matrix/Identity.hs b/matrix-client/src/Network/Matrix/Identity.hs index 30b3e38..8d4991d 100644 --- a/matrix-client/src/Network/Matrix/Identity.hs +++ b/matrix-client/src/Network/Matrix/Identity.hs @@ -1,14 +1,17 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE RankNTypes #-} -- | This module contains the Identity service API -- https://matrix.org/docs/spec/identity_service/r0.3.0.html module Network.Matrix.Identity ( -- * Client - IdentitySession, MatrixToken (..), getTokenFromEnv, - createIdentitySession, + createSession, + createSessionWithManager, -- * API MatrixIO, @@ -35,7 +38,6 @@ module Network.Matrix.Identity ) where -import Control.Monad (mzero) import Data.Aeson (FromJSON (..), Value (Object, String), encode, object, (.:), (.=)) import Data.ByteString.Lazy (fromStrict) import Data.ByteString.Lazy.Base64.URL (encodeBase64Unpadded) @@ -49,36 +51,16 @@ import Data.Text.Encoding (encodeUtf8) import Data.Text.Lazy (toStrict) import qualified Network.HTTP.Client as HTTP import Network.Matrix.Internal +import Control.Monad.Except -- $setup -- >>> import Data.Aeson (decode) --- | The session record, use 'createSession' to create it. -data IdentitySession = IdentitySession - { baseUrl :: Text, - token :: MatrixToken, - manager :: HTTP.Manager - } - --- | 'createSession' creates the session record. -createIdentitySession :: - -- | The matrix identity base url, e.g. "https://matrix.org" - Text -> - -- | The user identity token - MatrixToken -> - IO IdentitySession -createIdentitySession baseUrl' token' = IdentitySession baseUrl' token' <$> mkManager - -mkRequest :: IdentitySession -> Bool -> Text -> IO HTTP.Request -mkRequest IdentitySession {..} = mkRequest' baseUrl token - -doRequest :: FromJSON a => IdentitySession -> HTTP.Request -> MatrixIO a -doRequest IdentitySession {..} = doRequest' manager - -- | 'getIdentityTokenOwner' gets information about the owner of a given access token. -getIdentityTokenOwner :: IdentitySession -> MatrixIO UserID -getIdentityTokenOwner session = - doRequest session =<< mkRequest session True "/_matrix/identity/v2/account" +getIdentityTokenOwner :: MatrixIO UserID +getIdentityTokenOwner = do + request <- mkRequest True "/_matrix/identity/v2/account" + doRequest request data HashDetails = HashDetails { hdAlgorithms :: NonEmpty Text, @@ -90,14 +72,16 @@ instance FromJSON HashDetails where parseJSON (Object v) = HashDetails <$> v .: "algorithms" <*> v .: "lookup_pepper" parseJSON _ = mzero -hashDetails :: IdentitySession -> MatrixIO HashDetails -hashDetails session = - doRequest session =<< mkRequest session True "/_matrix/identity/v2/hash_details" +hashDetails :: MatrixIO HashDetails +hashDetails = do + request <- mkRequest True "/_matrix/identity/v2/hash_details" + doRequest request -- | Use 'identityLookup' to lookup a single identity, otherwise uses the full 'identitiesLookup'. -identityLookup :: IdentitySession -> HashDetails -> Identity -> MatrixIO (Maybe UserID) -identityLookup session hd ident = do - fmap toUserIDM <$> identitiesLookup session ilr +identityLookup :: HashDetails -> Identity -> MatrixIO (Maybe UserID) +identityLookup hd ident = do + userId <- identitiesLookup ilr + pure $ toUserIDM userId where toUserIDM = lookupIdentity address address = toHashedAddress hd ident @@ -130,11 +114,10 @@ instance FromJSON IdentityLookupResponse where toTuple _ = Nothing parseJSON _ = mzero -identitiesLookup :: IdentitySession -> IdentityLookupRequest -> MatrixIO IdentityLookupResponse -identitiesLookup session ilr = do - request <- mkRequest session True "/_matrix/identity/v2/lookup" +identitiesLookup :: IdentityLookupRequest -> MatrixIO IdentityLookupResponse +identitiesLookup ilr = do + request <- mkRequest True "/_matrix/identity/v2/lookup" doRequest - session ( request { HTTP.method = "POST", HTTP.requestBody = HTTP.RequestBodyLBS body diff --git a/matrix-client/src/Network/Matrix/Internal.hs b/matrix-client/src/Network/Matrix/Internal.hs index fa7e22b..ce8d0a6 100644 --- a/matrix-client/src/Network/Matrix/Internal.hs +++ b/matrix-client/src/Network/Matrix/Internal.hs @@ -1,17 +1,22 @@ {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NumericUnderscores #-} +{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE OverloadedStrings #-} {-# OPTIONS_GHC -Wno-missing-export-lists #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE DeriveFunctor #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE StandaloneDeriving #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE DerivingVia #-} +{-# LANGUAGE TypeApplications #-} -- | This module contains low-level HTTP utility module Network.Matrix.Internal where import Control.Concurrent (threadDelay) -import Control.Exception (Exception, throw, throwIO) -import Control.Monad (mzero, unless, void) -import Control.Monad.Catch (Handler (Handler), MonadMask) -import Control.Monad.IO.Class (MonadIO, liftIO) +import Control.Exception (throw, throwIO) import Control.Retry (RetryStatus (..)) import qualified Control.Retry as Retry import Data.Aeson (FromJSON (..), FromJSONKey (..), Value (Object), encode, eitherDecode, object, withObject, (.:), (.:?), (.=)) @@ -27,11 +32,14 @@ import Network.HTTP.Types (Status (..)) import Network.HTTP.Types.Status (statusIsSuccessful) import System.Environment (getEnv) import System.IO (stderr) +import Control.Monad.Except +import Control.Monad.Catch.Pure +import Control.Monad.Reader newtype MatrixToken = MatrixToken Text newtype Username = Username { username :: Text } newtype DeviceId = DeviceId { deviceId :: Text } -newtype InitialDeviceDisplayName = InitialDeviceDisplayName { initialDeviceDisplayName :: Text} +newtype InitialDeviceDisplayName = InitialDeviceDisplayName { initialDeviceDisplayName :: Text} data LoginSecret = Password Text | Token Text data LoginResponse = LoginResponse @@ -72,9 +80,9 @@ throwResponseError req res chunk = where ex = HTTP.StatusCodeException (void res) (toStrict chunk) -mkRequest' :: Text -> MatrixToken -> Bool -> Text -> IO HTTP.Request -mkRequest' baseUrl (MatrixToken token) auth path = do - initRequest <- HTTP.parseUrlThrow (unpack $ baseUrl <> path) +mkRequest' :: MonadIO m => Text -> MatrixToken -> Bool -> Text -> m HTTP.Request +mkRequest' baseUrl' (MatrixToken token') auth path = do + initRequest <- liftIO $ HTTP.parseUrlThrow (unpack $ baseUrl' <> path) pure $ initRequest { HTTP.requestHeaders = @@ -83,12 +91,12 @@ mkRequest' baseUrl (MatrixToken token) auth path = do } where authHeaders = - [("Authorization", "Bearer " <> encodeUtf8 token) | auth] + [("Authorization", "Bearer " <> encodeUtf8 token') | auth] mkLoginRequest' :: Text -> Maybe DeviceId -> Maybe InitialDeviceDisplayName -> Username -> LoginSecret -> IO HTTP.Request -mkLoginRequest' baseUrl did idn (Username name) secret' = do +mkLoginRequest' baseUrl' did idn (Username name) secret' = do let path = "/_matrix/client/r0/login" - initRequest <- HTTP.parseUrlThrow (unpack $ baseUrl <> path) + initRequest <- HTTP.parseUrlThrow (unpack $ baseUrl' <> path) let (secretKey, secret, secretType) = case secret' of Password pass -> ("password", pass, "m.login.password") @@ -105,15 +113,15 @@ mkLoginRequest' baseUrl did idn (Username name) secret' = do pure $ initRequest { HTTP.method = "POST", HTTP.requestBody = body, HTTP.requestHeaders = [("Content-Type", "application/json")] } mkLogoutRequest' :: Text -> MatrixToken -> IO HTTP.Request -mkLogoutRequest' baseUrl (MatrixToken token) = do +mkLogoutRequest' baseUrl' (MatrixToken token') = do let path = "/_matrix/client/r0/logout" - initRequest <- HTTP.parseUrlThrow (unpack $ baseUrl <> path) - let headers = [("Authorization", encodeUtf8 $ "Bearer " <> token)] + initRequest <- HTTP.parseUrlThrow (unpack $ baseUrl' <> path) + let headers = [("Authorization", encodeUtf8 $ "Bearer " <> token')] pure $ initRequest { HTTP.method = "POST", HTTP.requestHeaders = headers } doRequest' :: FromJSON a => HTTP.Manager -> HTTP.Request -> IO (Either MatrixError a) -doRequest' manager request = do - response <- HTTP.httpLbs request manager +doRequest' manager' request = do + response <- HTTP.httpLbs request manager' case decodeResp $ HTTP.responseBody response of Right x -> pure x Left e -> if statusIsSuccessful $ HTTP.responseStatus response @@ -156,7 +164,67 @@ instance FromJSON MatrixError where -- | 'MatrixIO' is a convenient type alias for server response type MatrixIO a = MatrixM IO a -type MatrixM m a = m (Either MatrixError a) +-- | The session record, use 'createSession' to create it. +data ClientSession = ClientSession + { baseUrl :: Text, + token :: MatrixToken, + manager :: HTTP.Manager + } + +-- | 'createSession' creates the session record. +createSession :: + -- | The matrix client-server base url, e.g. "https://matrix.org" + Text -> + -- | The user token + MatrixToken -> + IO ClientSession +createSession baseUrl' token' = ClientSession baseUrl' token' <$> mkManager + +-- | 'createSession' creates the session record. +createSessionWithManager :: + -- | The matrix client-server base url, e.g. "https://matrix.org" + Text -> + -- | The user token + MatrixToken -> + -- | A 'http-client' Manager + HTTP.Manager -> + ClientSession +createSessionWithManager = ClientSession + +mkRequest :: MonadIO m => Bool -> Text -> MatrixM m HTTP.Request +mkRequest auth path = do + ClientSession {..} <- ask + liftIO $ mkRequest' baseUrl token auth path + +doRequest :: forall a m. (MonadIO m, FromJSON a) => HTTP.Request -> MatrixM m a +doRequest request = do + ClientSession {..} <- ask + MatrixM $ ExceptT $ liftIO $ doRequest' manager request + +newtype MatrixM m a = MatrixM { unMatrixM :: ExceptT MatrixError (ReaderT ClientSession m) a } + deriving ( Functor + , Applicative + , Monad + , MonadError MatrixError + , MonadFail + , MonadIO + , MonadThrow + , MonadCatch + , MonadMask + , MonadReader ClientSession + ) via (ExceptT MatrixError (ReaderT ClientSession m)) + +instance MonadTrans MatrixM where + lift = MatrixM . lift . lift + +-- | Interpret MatrixM into your inner monad. Wraps the calls that +-- interacts with the Matrix API. +runMatrixM :: ClientSession -> MatrixM m a -> m (Either MatrixError a) +runMatrixM session = flip runReaderT session . runExceptT . unMatrixM + +-- | Run Matrix actions in 'IO'. +runMatrixIO :: ClientSession -> MatrixIO a -> IO (Either MatrixError a) +runMatrixIO = runMatrixM -- | Retry a network action retryWithLog :: @@ -172,18 +240,16 @@ retryWithLog limit logRetry action = Retry.recovering (Retry.exponentialBackoff backoff <> Retry.limitRetries limit) [handler, rateLimitHandler] - (const checkAction) + (const (checkAction)) where - checkAction = do - res <- action - case res of - Left (MatrixError "M_LIMIT_EXCEEDED" err delayMS) -> do + checkAction = + action `catchError` \case + MatrixError "M_LIMIT_EXCEEDED" err delayMS -> do -- Reponse contains a retry_after_ms - logRetry $ "RateLimit: " <> err <> " (delay: " <> pack (show delayMS) <> ")" + lift $ logRetry $ "RateLimit: " <> err <> " (delay: " <> pack (show delayMS) <> ")" liftIO $ threadDelay $ fromMaybe 5_000 delayMS * 1000 throw MatrixRateLimit - _ -> pure res - + e -> throwError e backoff = 1000000 -- 1sec rateLimitHandler _ = Handler $ \case MatrixRateLimit -> pure True @@ -193,7 +259,7 @@ retryWithLog limit logRetry action = let url = decodeUtf8 (HTTP.host req) <> ":" <> pack (show (HTTP.port req)) <> decodeUtf8 (HTTP.path req) arg = decodeUtf8 $ HTTP.queryString req loc = if num == 0 then url <> arg else url - logRetry $ + lift $ logRetry $ "NetworkFailure: " <> pack (show num) <> "/5 " diff --git a/matrix-client/src/Network/Matrix/Tutorial.hs b/matrix-client/src/Network/Matrix/Tutorial.hs index 012d96d..b287b46 100644 --- a/matrix-client/src/Network/Matrix/Tutorial.hs +++ b/matrix-client/src/Network/Matrix/Tutorial.hs @@ -47,37 +47,42 @@ where -- > Prelude Netowrk.Matrix.Client> :set prompt "> " -- > > :set -XOverloadedStrings -- > > :type getTokenOwner --- > getTokenOwner :: ClientSession -> MatrixIO WhoAmI +-- > getTokenOwner :: MatrixIO UserID -- $session --- Most functions require 'Network.Matrix.Client.ClientSession' which carries the --- endpoint url and the http client manager. --- --- The only way to get the client is through the 'Network.Matrix.Client.createSession' function: +-- Most functions operates in the 'MatrixIO' context, and to get their output you need to use +-- the 'runMatrixIO' helper. This helper expects a 'ClientSession' that can be created with +-- 'Network.Matrix.Client.createSession': -- -- > > token <- getTokenFromEnv "MATRIX_TOKEN" --- > > sess <- createSession "https://matrix.org" token --- > > getTokenOwner sess --- > Right (WhoAmI "@tristanc_:matrix.org") +-- > > session <- createSession "https://matrix.org" token +-- > > runMatrixIO session getTokenOwner +-- > Right (UserID "@tristanc_:matrix.org") +-- +-- For the purpose of this tutorial, we can create a `withSession` wrapper: +-- +-- > > let withSession = runMatrixIO session :: MatrixIO a -> IO (Either MatrixError a) +-- > > withSession getTokenOwner +-- > Right (UserID "@tristanc_:matrix.org") -- $sync -- Create a filter to limit the sync result using the 'Network.Matrix.Client.createFilter' function. -- To keep room message only, use the 'Network.Matrix.Client.messageFilter' default filter: -- --- > > Right userId <- getTokenOwner sess --- > > Right filterId <- createFilter sess userId messageFilter --- > > getFilter sess (UserID "@gerritbot:matrix.org") filterId +-- > > Right userId <- withSession getTokenOwner +-- > > Right filterId <- withSession (createFilter userId messageFilter) +-- > > withSession (getFilter userId filterId) -- > Right (Filter {filterEventFields = ...}) -- -- Call the 'Network.Matrix.Client.sync' function to synchronize your client state: -- --- > > Right syncResult <- sync sess (Just filterId) Nothing (Just Online) Nothing +-- > > Right syncResult <- withSession (sync (Just filterId) Nothing (Just Online) Nothing) -- > > putStrLn $ take 512 $ show (getTimelines syncResult) -- > SyncResult {srNextBatch = ...} -- -- Get next batch with a 300 second timeout using the @since@ argument: -- --- > > Right syncResult' <- sync sess (Just filterId) (Just (srNextBatch syncResult)) (Just Online) (Just 300000) +-- > > Right syncResult' <- withSession (sync (Just filterId) (Just (srNextBatch syncResult)) (Just Online) (Just 300000)) -- -- Here are some helpers function to format the messages from sync results, copy them in your REPL: -- @@ -96,7 +101,7 @@ where -- Use the 'Network.Matrix.Client.syncPoll' utility function to continuously get events, -- here is an example to print new messages, similar to a @tail -f@ process: -- --- > > syncPoll sess (Just filterId) (Just (srNextBatch syncResult)) (Just Online) printTimelines +-- > > withSession (syncPoll (Just filterId) (Just (srNextBatch syncResult)) (Just Online) printTimelines) -- > room1| test-user: Hello world! -- > ... @@ -110,7 +115,7 @@ where -- -- > > import Network.Matrix.Identity -- > > tokenId <- getTokenFromEnv "MATRIX_IDENTITY_TOKEN" --- > > sessId <- createIdentitySession "https://matrix.org" tokenId --- > > Right hd <- hashDetails sessId --- > > identityLookup sessId hd (Email "tdecacqu@redhat.com") +-- > > sessionId <- createSession "https://matrix.org" tokenId +-- > > Right hd <- runMatrixIO sessionId hashDetails +-- > > runMatrixIO sessionId (identityLookup hd (Email "tdecacqu@redhat.com")) -- > Right (Just (UserID "@tristanc_:matrix.org")) diff --git a/matrix-client/test/Spec.hs b/matrix-client/test/Spec.hs index f629761..a2726ff 100644 --- a/matrix-client/test/Spec.hs +++ b/matrix-client/test/Spec.hs @@ -4,7 +4,7 @@ -- | The matrix client specification tests module Main (main) where -import Control.Monad (void) +import Control.Monad.Except import qualified Data.Aeson.Encode.Pretty as Aeson import qualified Data.ByteString.Lazy as BS import Data.Either (isLeft) @@ -13,6 +13,7 @@ import Data.Time.Clock.System (SystemTime (..), getSystemTime) import Network.Matrix.Client import Network.Matrix.Internal import System.Environment (lookupEnv) +import Test.DocTest (doctest) import Test.Hspec main :: IO () @@ -26,15 +27,20 @@ main = do _ -> do putStrLn "Skipping integration test" pure $ pure mempty - hspec (parallel $ spec >> runIntegration) + hspec (parallel $ spec >> runIntegration >> docTest) + +docTest :: Spec +docTest = do + describe "doctest" $ do + it "works" $ do + doctest ["-XOverloadedStrings", "src/"] integration :: ClientSession -> ClientSession -> Spec integration sess1 sess2 = do describe "integration tests" $ do it "create room" $ do - resp <- + resp <- runMatrixM sess1 $ do createRoom - sess1 ( RoomCreateRequest { rcrPreset = PublicChat, rcrRoomAliasName = "test", @@ -44,25 +50,51 @@ integration sess1 sess2 = do ) case resp of Left err -> meError err `shouldBe` "Alias already exists" - Right (RoomID roomID) -> roomID `shouldSatisfy` (/= mempty) + Right (RoomID roomID') -> roomID' `shouldSatisfy` (/= mempty) it "join room" $ do - resp <- joinRoom sess1 "#test:localhost" + resp <- runMatrixM sess1 $joinRoom "#test:localhost" case resp of Left err -> error (show err) - Right (RoomID roomID) -> roomID `shouldSatisfy` (/= mempty) - resp' <- joinRoom sess2 "#test:localhost" + Right (RoomID roomID') -> roomID' `shouldSatisfy` (/= mempty) + resp' <- runMatrixM sess2 $ joinRoom "#test:localhost" case resp' of Left err -> error (show err) - Right (RoomID roomID) -> roomID `shouldSatisfy` (/= mempty) + Right (RoomID roomID') -> roomID' `shouldSatisfy` (/= mempty) it "send message and reply" $ do - -- Flush previous events - Right sr <- sync sess2 Nothing Nothing Nothing Nothing - Right [room] <- getJoinedRooms sess1 - let msg body = RoomMessageText $ MessageText body TextType Nothing Nothing - let since = srNextBatch sr - Right eventID <- sendMessage sess1 room (EventRoomMessage $ msg "Hello") (TxnID since) - Right reply <- sendMessage sess2 room (EventRoomReply eventID $ msg "Hi!") (TxnID since) - reply `shouldNotBe` eventID + result <- runMatrixM sess2 $ do + -- Flush previous events + sr <- sync Nothing Nothing Nothing Nothing + [room] <- getJoinedRooms + let msg body = RoomMessageText $ MessageText body TextType Nothing Nothing + let since = srNextBatch sr + eventID <- sendMessage room (EventRoomMessage $ msg "Hello") (TxnID since) + reply <- sendMessage room (EventRoomReply eventID $ msg "Hi!") (TxnID since) + pure (reply, eventID) + case result of + Left err -> error (show err) + Right (reply, eventID) -> reply `shouldNotBe` eventID + it "does not retry on success" $ + checkPause (<=) $ do + res <- runMatrixM sess1 $ retry (pure True) + res `shouldBe` pure True + it "does not retry on regular failure" $ + checkPause (<=) $ do + let resp = MatrixError "test" "error" Nothing + res <- runMatrixM sess1 $ retry (throwError resp :: MatrixIO Int) + res `shouldBe` Left resp + it "retry on rate limit failure" $ + checkPause (>=) $ do + let resp = MatrixError "M_LIMIT_EXCEEDED" "error" (Just 1000) + (runMatrixM sess1 $ retryWithLog 1 (const $ pure ()) (throwError resp)) + `shouldThrow` rateLimitSelector + where + rateLimitSelector :: MatrixException -> Bool + rateLimitSelector MatrixRateLimit = True + checkPause op action = do + MkSystemTime start' _ <- getSystemTime + void action + MkSystemTime end' _ <- getSystemTime + (end' - start') `shouldSatisfy` (`op` 1) spec :: Spec spec = describe "unit tests" $ do @@ -93,29 +125,7 @@ spec = describe "unit tests" $ do it "encode room message" $ encodePretty (RoomMessageText (MessageText "Hello" TextType Nothing Nothing)) `shouldBe` "{\"body\":\"Hello\",\"msgtype\":\"m.text\"}" - it "does not retry on success" $ - checkPause (<=) $ do - let resp = Right True - res <- retry (pure resp) - res `shouldBe` resp - it "does not retry on regular failre" $ - checkPause (<=) $ do - let resp = Left $ MatrixError "test" "error" Nothing - res <- (retry (pure resp) :: MatrixIO Int) - res `shouldBe` resp - it "retry on rate limit failure" $ - checkPause (>=) $ do - let resp = Left $ MatrixError "M_LIMIT_EXCEEDED" "error" (Just 1000) - (retryWithLog 1 (const $ pure ()) (pure resp) :: MatrixIO Int) - `shouldThrow` rateLimitSelector where - rateLimitSelector :: MatrixException -> Bool - rateLimitSelector MatrixRateLimit = True - checkPause op action = do - MkSystemTime start _ <- getSystemTime - void action - MkSystemTime end _ <- getSystemTime - (end - start) `shouldSatisfy` (`op` 1) encodePretty = Aeson.encodePretty' ( Aeson.defConfig {Aeson.confIndent = Aeson.Spaces 0, Aeson.confCompare = compare @Text}