{-# LANGUAGE ConstrainedClassMethods #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DuplicateRecordFields #-}
module OpenTok.Client
( Client(Client, _apiKey, _secret)
, ClientError(statusCode, message)
, Path
, post
, postWithBody
, get
, del
)
where
import Prelude ( )
import Prelude.Compat
import Control.Arrow ( left )
import Control.Lens.Combinators
import Control.Lens.Operators
import Control.Monad.Time
import Control.Monad.Trans.Except
import Crypto.JWT
import Data.Either.Combinators ( mapLeft )
import Data.List ( isInfixOf )
import Data.Semigroup ( (<>) )
import Data.UUID ( toText )
import Data.UUID.V4
import Data.Aeson ( decode
, encode
, eitherDecode
, FromJSON
, ToJSON
)
import Data.Aeson.Types
import qualified Data.ByteString.Char8 as C8
( pack )
import Data.ByteString.Lazy ( toStrict )
import Data.HashMap.Strict as HM
import Data.Time.Clock
import GHC.Generics ( Generic )
import Network.HTTP.Client hiding ( responseStatus )
import Network.HTTP.Client.TLS ( tlsManagerSettings )
import Network.HTTP.Simple ( getResponseStatusCode )
import Network.HTTP.Types.Header ( RequestHeaders )
import OpenTok.Util
type Path = String
data APIError = APIError {
_code :: Int,
_status :: Maybe String,
_message :: String
} deriving (Generic, Show)
instance FromJSON APIError where
parseJSON = genericParseJSON $ defaultOptions { omitNothingFields = True, fieldLabelModifier = drop 1 }
data ClientError = ClientError {
statusCode :: Int,
message :: String
} deriving (Generic, Show)
jwtError :: ClientError
jwtError = ClientError 0 "Failed to create JWT"
decodeError :: Int -> ClientError
decodeError sc = ClientError sc "Failed to decode API response"
expireTime :: UTCTime -> UTCTime
expireTime t = epochToUTC $ utcToEpoch t + 60
data Client = Client {
_apiKey :: String,
_secret :: String
}
mkClaims :: String -> IO ClaimsSet
mkClaims projectKey = do
now <- currentTime
uuid <- nextRandom
let now' = epochToUTC $ utcToEpoch now
let later = expireTime now
pure
$ emptyClaimsSet
& claimIss .~ preview stringOrUri (projectKey :: String)
& claimIat ?~ NumericDate now'
& claimExp ?~ NumericDate later
& claimJti ?~ toText uuid
& unregisteredClaims %~ HM.insert "ist" "project"
signJWT :: JWK -> ClaimsSet -> IO (Either JWTError SignedJWT)
signJWT key claims =
runExceptT $ signClaims key (newJWSHeader ((), HS256)) claims
createJWT :: Client -> IO (Either ClientError SignedJWT)
createJWT client = do
claims <- mkClaims (_apiKey client)
let key = fromOctets (C8.pack $ _secret client)
eitherSigned <- signJWT key claims
pure $ mapLeft (const jwtError) eitherSigned
buildHeaders :: SignedJWT -> RequestHeaders
buildHeaders jwt =
[
( "Content-Type", "application/json")
, ("Accept" , "application/json")
, ("X-OPENTOK-AUTH", toStrict $ encodeCompact jwt)
]
buildV1Headers :: SignedJWT -> RequestHeaders
buildV1Headers jwt = drop 1 $ buildHeaders jwt
execute :: (FromJSON a) => Request -> IO (Either ClientError a)
execute req = do
manager <- newManager tlsManagerSettings
response <- httpLbs req manager
let body = responseBody response
let sc = getResponseStatusCode response
case sc of
200 -> pure $ left (\m -> (decodeError sc) { message = m }) (eitherDecode body)
_ -> pure $ Left $ maybe (decodeError sc)
(ClientError sc . _message)
(decode body :: Maybe APIError)
deleteResource :: Request -> IO (Either ClientError String)
deleteResource req = do
manager <- newManager tlsManagerSettings
response <- httpLbs req manager
let sc = getResponseStatusCode response
case sc of
204 -> pure $ Right "Ok"
_ -> pure $ Left $ ClientError sc "Failed to delete resource"
buildRequest :: Path -> SignedJWT -> IO Request
buildRequest p jwt = do
initialRequest <- parseRequest $ "https://api.opentok.com" <> p
let buildFn = if "v2" `isInfixOf` p then buildHeaders else buildV1Headers
pure $ initialRequest { method = "POST", requestHeaders = buildFn jwt }
post :: (FromJSON a) => Client -> Path -> IO (Either ClientError a)
post client p = do
eitherJWT <- createJWT client
case eitherJWT of
Left e -> pure $ Left e
Right jwt -> do
request <- buildRequest p jwt
execute request
postWithBody :: (ToJSON a, FromJSON b) => Client -> Path -> a -> IO (Either ClientError b)
postWithBody client p b = do
eitherJWT <- createJWT client
case eitherJWT of
Left e -> pure $ Left $ e
Right jwt -> do
request <- buildRequest p jwt
execute $ request { requestBody = RequestBodyLBS $ encode b }
get :: (FromJSON a) => Client -> Path -> IO (Either ClientError a)
get client p = do
eitherJWT <- createJWT client
case eitherJWT of
Left e -> pure $ Left $ e
Right jwt -> do
request <- buildRequest p jwt
execute $ request { method = "GET" }
del :: Client -> Path -> IO (Either ClientError String)
del client p = do
eitherJWT <- createJWT client
case eitherJWT of
Left e -> pure $ Left $ e
Right jwt -> do
request <- buildRequest p jwt
deleteResource $ request { method = "DELETE" }