hsforce-0.1.0.0: src/HSForce/Client.hs
{-# 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