Skip to content

Feature/mtl refactor #18

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
Open
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
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -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
7 changes: 7 additions & 0 deletions matrix-client/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
4 changes: 3 additions & 1 deletion matrix-client/matrix-client.cabal
Original file line number Diff line number Diff line change
@@ -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
780 changes: 393 additions & 387 deletions matrix-client/src/Network/Matrix/Client.hs

Large diffs are not rendered by default.

59 changes: 21 additions & 38 deletions matrix-client/src/Network/Matrix/Identity.hs
Original file line number Diff line number Diff line change
@@ -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
118 changes: 92 additions & 26 deletions matrix-client/src/Network/Matrix/Internal.hs
Original file line number Diff line number Diff line change
@@ -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 }

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO we'd better call this MatrixT as it's a monad transformer. Also, we can have a interface like:

class MonadMatrix m where
  getClientSession :: m ClientSession
  throwMatrixError :: MatrixError -> m a
  performHttpRequest :: FromJSON a => HTTP.Request -> m a

Then MatrixT could be a default implementation of MonadMatrix:

instance MonadIO m => MonadMatrix (MatrixT m) where
  getClientSession = ask
  throwMatrixError = throwError
  performHttpRequest = ...

and users may create their own monads with or wihout MatrixT.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Presently, this is going to be tricky when using both a regular session and a identity session, I think we need to implement a ClientSession that support both endpoints, otherwise I think it's going to be tricky to implement such Monad for both use-case.

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 "
39 changes: 22 additions & 17 deletions matrix-client/src/Network/Matrix/Tutorial.hs
Original file line number Diff line number Diff line change
@@ -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"))
88 changes: 49 additions & 39 deletions matrix-client/test/Spec.hs
Original file line number Diff line number Diff line change
@@ -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}