{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE RankNTypes #-} module CDP.Endpoints where import Data.Maybe import Data.List import Data.Proxy import qualified Network.URI as Uri import qualified Network.HTTP.Simple as Http import qualified Data.Aeson as A import qualified Data.ByteString.Lazy as BS import Data.Aeson (FromJSON (..), ToJSON (..), (.:), (.:?), (.=), (.!=), (.:!)) import qualified Data.Text as T import Control.Exception import CDP.Internal.Utils import qualified CDP.Definition type URL = T.Text type TargetId = T.Text data EPBrowserVersion = EPBrowserVersion data EPAllTargets = EPAllTargets data EPCurrentProtocol = EPCurrentProtocol data EPOpenNewTab = EPOpenNewTab { unOpenNewTab :: URL } data EPActivateTarget = EPActivateTarget { unActivateTarget :: TargetId } data EPCloseTarget = EPCloseTarget { unCloseTarget :: TargetId } data EPFrontend = EPFrontend data SomeEndpoint where SomeEndpoint :: Endpoint ep => ep -> SomeEndpoint fromSomeEndpoint :: (forall ep. Endpoint ep => ep -> r) -> SomeEndpoint -> r fromSomeEndpoint f (SomeEndpoint ep) = f ep -- | Sends a request with the given parameters to the corresponding endpoint endpoint :: Endpoint ep => Config -> ep -> IO (EndpointResponse ep) endpoint = getEndpoint . hostPort -- | Creates a session with a new tab connectToTab :: Config -> URL -> IO TargetInfo connectToTab cfg url = do targetInfo <- endpoint cfg $ EPOpenNewTab url endpoint cfg $ EPActivateTarget $ tiId targetInfo pure targetInfo class Endpoint ep where type EndpointResponse ep :: * getEndpoint :: (String, Int) -> ep -> IO (EndpointResponse ep) epDecode :: Proxy ep -> BS.ByteString -> Either String (EndpointResponse ep) instance Endpoint EPBrowserVersion where type EndpointResponse EPBrowserVersion = BrowserVersion getEndpoint hostPort _ = performRequest (Proxy :: Proxy EPBrowserVersion) $ getRequest hostPort ["json", "version"] Nothing epDecode = const A.eitherDecode instance Endpoint EPAllTargets where type EndpointResponse EPAllTargets = [TargetInfo] getEndpoint hostPort _ = performRequest (Proxy :: Proxy EPAllTargets) $ getRequest hostPort ["json", "list"] Nothing epDecode = const A.eitherDecode instance Endpoint EPCurrentProtocol where type EndpointResponse EPCurrentProtocol = CDP.Definition.TopLevel getEndpoint hostPort _ = performRequest (Proxy :: Proxy EPCurrentProtocol) $ getRequest hostPort ["json", "protocol"] Nothing epDecode = const A.eitherDecode instance Endpoint EPOpenNewTab where type EndpointResponse EPOpenNewTab = TargetInfo getEndpoint hostPort (EPOpenNewTab url) = performRequest (Proxy :: Proxy EPOpenNewTab) $ getRequest hostPort ["json", "new"] (Just url) epDecode = const A.eitherDecode instance Endpoint EPActivateTarget where type EndpointResponse EPActivateTarget = () getEndpoint hostPort (EPActivateTarget id) = performRequest (Proxy :: Proxy EPActivateTarget) $ getRequest hostPort ["json", "activate", id] Nothing epDecode = const . const $ Right () instance Endpoint EPCloseTarget where type EndpointResponse EPCloseTarget = () getEndpoint hostPort (EPCloseTarget id) = performRequest (Proxy :: Proxy EPCloseTarget) $ getRequest hostPort ["json", "close", id] Nothing epDecode = const . const $ Right () instance Endpoint EPFrontend where type EndpointResponse EPFrontend = BS.ByteString getEndpoint hostPort EPFrontend = performRequest (Proxy :: Proxy EPFrontend) $ getRequest hostPort ["devtools", "inspector.html"] Nothing epDecode = const Right data BrowserVersion = BrowserVersion { bvBrowser :: T.Text , bvProtocolVersion :: T.Text , bvUserAgent :: T.Text , bvV8Version :: T.Text , bvVebKitVersion :: T.Text , bvWebSocketDebuggerUrl :: T.Text } deriving (Show, Eq) instance FromJSON BrowserVersion where parseJSON = A.withObject "BrowserVersion" $ \v -> BrowserVersion <$> v .: "Browser" <*> v .: "Protocol-Version" <*> v .: "User-Agent" <*> v .: "V8-Version" <*> v .: "WebKit-Version" <*> v .: "webSocketDebuggerUrl" data TargetInfo = TargetInfo { tiDescription :: T.Text , tiDevtoolsFrontendUrl :: T.Text , tiId :: T.Text , tiTitle :: T.Text , tiType :: T.Text , tiUrl :: T.Text , tiWebSocketDebuggerUrl :: T.Text } deriving Show instance FromJSON TargetInfo where parseJSON = A.withObject "TargetInfo" $ \v -> TargetInfo <$> v .: "description" <*> v .: "devtoolsFrontendUrl" <*> v .: "id" <*> v .: "title" <*> v .: "type" <*> v .: "url" <*> v .: "webSocketDebuggerUrl" browserAddress :: (String, Int) -> IO (String, Int, String) browserAddress hostPort = fromMaybe (throw . ERRParse $ "invalid URI when connecting to browser") . parseUri . T.unpack . bvWebSocketDebuggerUrl <$> getEndpoint hostPort EPBrowserVersion getRequest :: (String, Int) -> [T.Text] -> Maybe T.Text -> Http.Request getRequest (host, port) path mbParam = Http.parseRequest_ . T.unpack $ r where r = mconcat ["GET ", T.pack host, ":", T.pack (show port), "/", T.intercalate "/" path , maybe "" ("?" <>) mbParam ] performRequest :: Endpoint ep => Proxy ep -> Http.Request -> IO (EndpointResponse ep) performRequest p req = do body <- Http.getResponseBody <$> Http.httpLBS req either (throwIO . ERRParse) pure $ epDecode p body parseUri :: String -> Maybe (String, Int, String) parseUri uri = do u <- Uri.parseURI $ uri auth <- Uri.uriAuthority u let port = case Uri.uriPort auth of (':':str) -> read str _ -> 80 pure (Uri.uriRegName auth, port, Uri.uriPath u)