{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE TemplateHaskell #-} module HSForce.Client where import Network.HTTP.Conduit import Network.URI import Network.URI.Encode as URI import System.IO import System.Environment import Data.Aeson as JSON import Data.Aeson.TH (deriveJSON, defaultOptions, Options(..)) import Data.ByteString.Char8 as B8 import Data.ByteString.Lazy.Char8 as BL8 import Data.Proxy as DP import Data.Maybe import Data.Text as T import Data.HashMap.Strict as M import Data.List as L import Text.HTML.TagSoup.Entity import Text.XML.HaXml import Text.XML.HaXml.Posn import Text.XML.HaXml.Util import Text.XML.HaXml.Xtract.Parse import Text.Regex.Posix import GHC.Generics import HSForce.Util import Control.Applicative data SFClient = SFClient { clientAccessToken :: String, clientInstanceUrl :: String, clientApiVersion:: String, clientDebug :: Bool } deriving Show data LoginRequest = LoginRequest{ sfUsername :: Maybe String, sfPassword :: Maybe String, sfEndpoint :: Maybe String, sfVersion :: Maybe String, sfClientID :: Maybe String, sfClientSecret :: Maybe String } deriving Show defaultLoginRequest :: IO LoginRequest defaultLoginRequest = do sfUsername <- lookupEnv "SALESFORCE_USERNAME" sfPassword <- lookupEnv "SALESFORCE_PASSWORD" endpoint <- lookupEnv "SALESFORCE_ENDPOINT" version <- lookupEnv "SALESFORCE_VERSION" sfClientID <- lookupEnv "SALESFORCE_CLINET_ID" sfClientSecret <- lookupEnv "SALESFORCE_CLIENT_SECRET" return (LoginRequest{ sfUsername, sfPassword, sfEndpoint = endpoint <|> Just "login.salesforce.com", sfVersion = version <|> Just "v44.0", sfClientID, sfClientSecret }) login :: LoginRequest -> IO (SFClient) login lr = do if isOAuth lr then restLogin lr else soapLogin lr soapLogin :: LoginRequest -> IO (SFClient) soapLogin LoginRequest{sfUsername, sfPassword, sfEndpoint, sfVersion} = do let username = fromJust sfUsername password = fromJust sfPassword endpoint = fromJust sfEndpoint clientApiVersion = fromJust sfVersion let body = "<?xml version=\"1.0\" encoding=\"utf-8\"?> \ \<env:Envelope xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:env=\"http://schemas.xmlsoap.org/soap/envelope/\"> \ \<env:Body> \ \<n1:login xmlns:n1=\"urn:partner.soap.sforce.com\"> \ \<n1:username>{username}</n1:username> \ \<n1:password>{password}</n1:password> \ \</n1:login> \ \</env:Body> \ \</env:Envelope>" :: String initReq <- parseRequest $ "https://" ++ endpoint ++ "/services/Soap/u/" ++ clientApiVersion manager <- newManager tlsManagerSettings let requestBody = L.foldl (\body (bind,value) -> HSForce.Util.replace bind (escapeXML value) body) body [("{username}", username), ("{password}", password)] let req = initReq { method = "POST", requestHeaders = [("Content-Type", "text/xml"), ("SOAPAction", "''")], requestBody = RequestBodyBS $ B8.pack requestBody } response <- httpLbs req manager let Document _ _ root _ = xmlParse "" $ (BL8.unpack (responseBody response)) cont = CElem root noPos result = xtract id "/soapenv:Envelope/soapenv:Body/loginResponse/result" cont !! 0 clientAccessToken = tagText result "/result/sessionId" clientServerUrl = tagText result "/result/serverUrl" matches = clientServerUrl =~ ("^(https://[^/]*)/.*" :: String) :: [[String]] return SFClient{clientAccessToken, clientApiVersion, clientInstanceUrl = matches !! 0 !! 1, clientDebug = False} restLogin :: LoginRequest -> IO (SFClient) restLogin LoginRequest{sfUsername, sfPassword, sfEndpoint, sfVersion, sfClientID, sfClientSecret} = do let username = fromJust sfUsername password = fromJust sfPassword endpoint = fromJust sfEndpoint clientApiVersion = fromJust sfVersion clientId = fromJust sfClientID clientSecret = fromJust sfClientSecret initReq <- parseRequest $ "https://" ++ endpoint ++ "/services/oauth2/token" manager <- newManager tlsManagerSettings let params = [ ("grant_type", "password"), ("client_id", clientId), ("client_secret", clientSecret), ("username", username), ("password", password) ] requestBody = L.tail $ L.foldl (\body (k,v) -> body ++ "&" ++ k ++ "=" ++ URI.encode v) "" params req = initReq { method = "POST", requestHeaders = [("Content-Type", "application/x-www-form-urlencoded")], requestBody = RequestBodyBS $ B8.pack requestBody } response <- httpLbs req manager let tokenObject = fromJust (JSON.decode $ responseBody response :: Maybe Object) clientAccessToken = T.unpack . getText . fromJust $ M.lookup "access_token" tokenObject clientInstanceUrl = T.unpack . getText . fromJust $ M.lookup "instance_url" tokenObject return SFClient{clientAccessToken, clientApiVersion, clientInstanceUrl, clientDebug = False} isOAuth :: LoginRequest -> Bool isOAuth LoginRequest{sfClientID = Just _, sfClientSecret = Just _} = True isOAuth _ = False requestGet :: SFClient -> String -> IO (Response BL8.ByteString) requestGet = requestWithoutBody "GET" requestDelete :: SFClient -> String -> IO (Response BL8.ByteString) requestDelete = requestWithoutBody "DELETE" requestWithoutBody :: B8.ByteString -> SFClient -> String -> IO (Response BL8.ByteString) requestWithoutBody method client path = do initReq <- parseRequest $ clientInstanceUrl client ++ path manager <- newManager tlsManagerSettings let req = initReq { method = method, requestHeaders = [("Authorization", B8.pack $ "Bearer " ++ (clientAccessToken client))] } printDebug client req response <- httpLbs req manager printDebug client response return (response) requestWithBody :: B8.ByteString -> SFClient -> String -> String -> IO (Response BL8.ByteString) requestWithBody method client path body = do initReq <- parseRequest $ clientInstanceUrl client ++ path manager <- newManager tlsManagerSettings let req = initReq { method = method, requestHeaders = [ ("Content-Type", "application/json"), ("Authorization", B8.pack $ "Bearer " ++ (clientAccessToken client)) ], requestBody = RequestBodyBS $ B8.pack body } printDebug client req response <- httpLbs req manager printDebug client response return (response) requestPost :: SFClient -> String -> String -> IO (Response BL8.ByteString) requestPost = requestWithBody "POST" requestPatch :: SFClient -> String -> String -> IO (Response BL8.ByteString) requestPatch = requestWithBody "PATCH" printDebug :: (Show a) => SFClient -> a -> IO () printDebug client var = do if clientDebug client then print var else pure () dataPath :: SFClient -> String dataPath client = do "/services/data/" ++ (clientApiVersion client) getText :: Value -> Text getText (String a) = a deriveJSON defaultOptions { fieldLabelModifier = defaultJsonLabelFilter "client" } ''SFClient