{-# LANGUAGE ExistentialQuantification #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE AutoDeriveTypeable #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE ViewPatterns #-} {-# LANGUAGE LambdaCase #-} {-# OPTIONS_GHC -fno-warn-name-shadowing #-} -- | API to Betfair (). -- -- You need a Betfair account, API keys and registered SSL certificates to use -- this module. -- -- Using the API may subject you to data charges: -- -- -- -- By default this library will not let you do more than 4 requests per second. -- This limit is global and is enforced even if you have multiple Betfair -- connections. -- -- This should stop you from being able to do anything that incurs data charges -- by API use only (you can still ruin yourself by making bad bets), even the -- heavily weighted charges. -- -- Use `request` to make calls to Betfair. Look at "Network.Betfair.Types" for -- commands to use with `request`. -- module Network.Betfair ( -- * Credentials Credentials(..) , HasCredentials(..) -- * Connection , openBetfair , closeBetfair , Betfair() -- * Operations , request , Network.Betfair.Internal.Request() -- * Types , module Network.Betfair.Types ) where import Control.Applicative import Control.Concurrent import Control.Lens hiding ( (.=) ) import Control.Monad.Catch import Control.Monad.State.Strict import Data.Aeson import Data.ByteString ( ByteString ) import qualified Data.ByteString as B import qualified Data.ByteString.Builder as BL import qualified Data.ByteString.Lazy as BL import Data.IORef import Data.Monoid import Data.Proxy import Data.Text ( Text ) import qualified Data.Text as T import qualified Data.Text.Encoding as T import Data.Typeable import Network.Betfair.Internal import Network.Betfair.Types import Network.HTTP.Client.OpenSSL import OpenSSL.Session import Pipes hiding ( Proxy ) import Pipes.HTTP as PH hiding ( Proxy ) import System.Mem.Weak -- | Login credentials. data Credentials = Credentials { _username :: Text -- ^ Your Betfair username. , _password :: Text -- ^ Your Betfair password. , _certificatePrivateKeyFile :: FilePath , _certificateCertificateFile :: FilePath , _apiKey :: Text } deriving ( Eq, Ord, Show, Read, Typeable ) makeClassy ''Credentials data Work = Work !Url !JsonRPCQuery !(MVar (Either SomeException BL.ByteString)) deriving ( Eq, Typeable ) -- The type of all JSON-based queries sent out data JsonRPCQuery = JsonRPCQuery { params :: !Value , methodName :: !Text } deriving ( Eq, Show, Typeable ) -- The type of all JSON-based queries received newtype JsonRPC a = JsonRPC (Either APIException a) data BetfairHandle = BetfairHandle { _betfairThread :: !ThreadId , _workChannel :: !(MVar Work) } makeClassy ''BetfairHandle instance FromJSON a => FromJSON (JsonRPC a) where parseJSON (Object v) = JsonRPC <$> msum [ Right <$> v .: "result" , do err <- v .: "error" dt <- err .: "data" exc <- dt .: "APINGException" details <- exc .: "errorDetails" code <- exc .: "errorCode" return $ Left $ APIException details code ] parseJSON _ = empty instance ToJSON JsonRPCQuery where toJSON (JsonRPCQuery {..}) = object [ "jsonrpc" .= ("2.0" :: Text) , "method" .= methodName , "id" .= (1 :: Int) , "params" .= params ] -- | A handle to a Betfair connection. newtype Betfair = Betfair (MVar (Maybe BetfairHandle)) deriving ( Eq, Typeable ) -- | Connect to Betfair with given credentials. -- -- The connection is cut when the returned `Betfair` value is garbage collected -- or closed with `closeBetfair`. openBetfair :: MonadIO m => Credentials -> m Betfair openBetfair credentials = liftIO $ mask_ $ do -- MVars. MVars everywhere. semaphore <- newEmptyMVar work_mvar <- newEmptyMVar weak_mvar_mvar <- newEmptyMVar bftid <- forkIOWithUnmask $ \unmask -> -- catch `CloseBetfair` to suppress message flip catch (\CloseBetfair -> return ()) $ do withOpenSSL $ do weak_mvar <- takeMVar weak_mvar_mvar finally (unmask $ betfairConnection credentials semaphore work_mvar) (deRefWeak weak_mvar >>= \case Nothing -> return () Just mvar -> modifyMVar_ mvar $ \_ -> return Nothing) let handle = BetfairHandle { _betfairThread = bftid , _workChannel = work_mvar } mvar <- newEmptyMVar weak_mvar <- mkWeakMVar mvar $ closeBetfairHandle handle putMVar weak_mvar_mvar weak_mvar flip onException (killThread bftid) $ takeMVar semaphore >>= \case Left err -> throwM err Right _ -> do putMVar mvar (Just handle) return $ Betfair mvar withBetfair :: Betfair -> (BetfairHandle -> IO a) -> IO a withBetfair (Betfair mvar) action = withMVar mvar $ \case Nothing -> throwM BetfairIsClosed Just bhandle -> action bhandle work :: (FromJSON a) => Betfair -> Url -> JsonRPCQuery -> IO a work bf url query = withBetfair bf $ \handle -> do result <- newEmptyMVar putMVar (_workChannel handle) $ Work url query result takeMVar result >>= \case Left exc -> throwM exc Right result -> case decode result of Nothing -> throwM $ ParsingFailure $ "Betfair behaved in unexpected way. Received raw string: " <> (T.pack $ show result) Just (JsonRPC (Left exc)) -> throwM exc Just (JsonRPC (Right x)) -> return x betfairConnection :: Credentials -> MVar (Either SomeException ()) -> MVar Work -> IO () -- the pattern is lazy because if it's bottom, we catch it a too early betfairConnection ~credentials@(Credentials{..}) semaphore work_mvar = flip catch (\(e :: SomeException) -> putMVar semaphore (Left e)) $ do -- establish SSL-enabled HTTP manager ssl_context <- context contextSetPrivateKeyFile ssl_context _certificatePrivateKeyFile contextSetCertificateFile ssl_context _certificateCertificateFile withManager (opensslManagerSettings $ return ssl_context) $ \m -> do session_key <- obtainSessionKey credentials m putMVar semaphore (Right ()) -- we are ready to process API calls -- so signal that to `openBetfair`. mask $ \restore -> do keep_alive_tid <- forkIO $ restore $ forever $ do -- wait 10 hours -- we use a loop because Int may be too -- small to hold the number of -- microseconds needed. replicateM_ (3600*10) $ threadDelay 1000000 keepAlive session_key credentials m finally (restore $ workLoop session_key credentials m work_mvar) (killThread keep_alive_tid) workLoop :: SessionKey -> Credentials -> Manager -> MVar Work -> IO () workLoop session_key credentials m work_mvar = forever $ do work <- takeMVar work_mvar case work of Work url sending result_mvar -> do flip onException (tryPutMVar result_mvar $ Left $ toException BetfairIsClosed) $ do -- --- IMPORTANT API LIMIT DO NOT TOUCH --- -- workRateLimit -- make sure we don't make too many API calls per -- second or we incur Betfair charges request <- betfairRequest session_key credentials sending url withHTTP request m $ \response -> do body <- readBodyLimited 50000000 response putMVar result_mvar (Right body) betfairRequest :: SessionKey -> Credentials -> JsonRPCQuery -> Url -> IO PH.Request betfairRequest session_key (Credentials{..}) query url = do req <- parseUrl url return $ req { method = "POST" , requestBody = RequestBodyBS $ BL.toStrict $ encode query , requestHeaders = requestHeaders req <> [ ("X-Application", T.encodeUtf8 _apiKey) , ("X-Authentication", T.encodeUtf8 session_key) , ("content-type", "application/json") ] } keepAlive :: SessionKey -> Credentials -> Manager -> IO () keepAlive session_key (Credentials{..}) m = do req' <- parseUrl "https://identitysso.betfair.com/api/keepAlive" let req = req' { requestHeaders = requestHeaders req' <> [ ("Accept", "application/json") , ("X-Application", T.encodeUtf8 _apiKey) , ("X-Authentication", T.encodeUtf8 session_key) ] } withHTTP req m $ \response -> do -- maybe it works, maybe it does not? I'm not sure what would be the -- sensible thing to do if keep-alive request fails. User probably -- still can continue to use the API as long as the session key is -- valid (which it will be for 12 hours). _ <- readBodyLimited 100000 response return () obtainSessionKey :: Credentials -> Manager -> IO SessionKey obtainSessionKey (Credentials{..}) m = do -- request to obtain a session key req' <- parseUrl "https://identitysso.betfair.com/api/certlogin" let req = urlEncodedBody [("username", T.encodeUtf8 _username) ,("password", T.encodeUtf8 _password)] $ req' { requestHeaders = requestHeaders req' <> [ ("X-Application", T.encodeUtf8 _apiKey) ] } withHTTP req m $ \response -> do b <- readBodyLimited 1000000 response case decode b of Nothing -> throwM $ ParsingFailure "session key" Just login_response -> case login_response of LoginFailed reason -> throwM $ LoginFailure reason LoginSuccessful session_key -> return session_key readBodyLimited :: Int -> Response (Producer ByteString IO ()) -> IO BL.ByteString readBodyLimited max_bytes response = do accumRef <- newIORef mempty runEffect $ responseBody response >-> accumulator accumRef 0 BL.toLazyByteString <$> readIORef accumRef where accumulator accumRef builder_length = do block <- await accum <- liftIO $ readIORef accumRef when (B.length block + builder_length > max_bytes) $ liftIO $ throwM TooMuchHTTPData liftIO $ writeIORef accumRef (accum <> BL.byteString block) accumulator accumRef (builder_length + B.length block) -- | The type of exceptions that can happen with Betfair. data BetfairException = TooMuchHTTPData -- ^ A HTTP response sent improbable amount of data. -- You are unlikely to see this unless Betfair -- is having a serious malfunction. | ParsingFailure Text -- ^ This exception is thrown if some data returned from Betfair -- failed to parse as expected JSON. It can mean a bug in our library -- or changes in the Betfair API that his library has not taken into -- account. The string has human-readable information. | LoginFailure Text -- ^ Login failed. The human-readable string tells why. | BetfairIsClosed -- ^ The `Betfair` handle you tried to use is closed. deriving ( Eq, Ord, Show, Read, Typeable ) instance Exception BetfairException data CloseBetfair = CloseBetfair deriving ( Eq, Ord, Show, Read, Typeable, Enum ) instance Exception CloseBetfair -- | Close a `Betfair` connection promptly. -- -- Does nothing if it was already closed. Trying to use `Betfair` after this -- call will result in a user error. closeBetfair :: MonadIO m => Betfair -> m () closeBetfair (Betfair mvar) = liftIO $ modifyMVar_ mvar $ \case Nothing -> return Nothing Just handle -> closeBetfairHandle handle >> return Nothing closeBetfairHandle :: BetfairHandle -> IO () closeBetfairHandle (_betfairThread -> tid) = throwTo tid CloseBetfair -- | Perform a request to `Betfair`. request :: forall a b m. (MonadIO m, Network.Betfair.Internal.Request a b) => a -> Betfair -> m b request req bf = liftIO $ work bf (requestUrl (Proxy :: Proxy a)) (JsonRPCQuery { methodName = requestMethod (Proxy :: Proxy a) , params = toJSON req })