{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
module Network.Mail.Mailgun.API where
import Control.Lens
import Control.Monad.Catch
import Control.Monad.Reader.Class
import Control.Monad.Trans
import qualified Data.Aeson as JS
import Data.Aeson.Lens
import Data.Foldable
import Data.Machine
import Network.Mail.Mailgun.Config
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.Encoding as TE
import Network.Wreq
import Network.Wreq.Types (Postable)
import qualified Network.Wreq as HTTP
import Text.Printf
data UnparsableResponse
= UnparsableResponse JS.Value
deriving (Show)
makePrisms ''UnparsableResponse
instance Exception UnparsableResponse
data MailgunApiError
= RequestTooLarge
| MailgunSideError
| MailgunNotFound
| UnknownResponseError Int
deriving (Show)
makePrisms ''MailgunApiError
instance Exception MailgunApiError
data MGRequest
= MGGet
{ _reqPath :: DomainName -> String
, _reqParams :: [(Text, Text)]
}
| MGDelete
{ _reqPath :: DomainName -> String
, _reqParams :: [(Text, Text)]
}
| forall b. Postable b => MGPost
{ _reqPath :: DomainName -> String
, _reqParams :: [(Text, Text)]
, _reqBody :: b
}
makeLenses ''MGRequest
yesNo :: Text -> Bool -> HTTP.Part
yesNo t True = partText t "yes"
yesNo t False = partText t "no"
wreqOptions :: MailgunConfig -> Options
wreqOptions = reader $ \c ->
defaults & auth ?~ basicAuth (TE.encodeUtf8 "api") (c^.mailgunApiKey)
call :: forall c m r
. (HasMailgunConfig c, MonadIO m, MonadThrow m, MonadReader c m)
=> MGRequest
-> (JS.Value -> Maybe r)
-> m r
call rq respHandle = do
c <- view mailgunConfig
let o = (c^.to wreqOptions) & params .~ (rq^.reqParams) & checkResponse .~ (Just $ \_ _ -> pure ())
let url = mconcat ["https://", c^.mailgunApiDomain, (rq^.reqPath) (c^.mailgunDomain)]
resp <- liftIO $ case rq of
MGGet {} ->
getWith o url
MGDelete {} ->
deleteWith o url
MGPost {_reqBody=bdy} ->
postWith o url bdy
case resp^.responseStatus.statusCode of
404 -> throwM MailgunNotFound
413 -> throwM RequestTooLarge
sts | sts `elem` [500, 502, 503, 504] -> throwM MailgunSideError
200 -> do
vr <- asValue resp
case respHandle (vr^.responseBody) of
Nothing -> throwM $ UnparsableResponse (vr^.responseBody)
Just r -> pure r
sts -> throwM $ UnknownResponseError sts
getStream :: forall t c m r s
. (HasMailgunConfig c, MonadIO m, MonadThrow m, MonadReader c m)
=> s
-> (s -> (t, MGRequest))
-> (t -> JS.Value -> Maybe (Maybe s, [r]))
-> SourceT m r
getStream seed rqMkr respHandle = construct (go seed)
where
go r = do
let (c, rq) = rqMkr r
(ms, res) <- lift $ call rq (respHandle c)
for_ res yield
for_ ms go
paginatedStream :: forall c m r
. (HasMailgunConfig c, MonadIO m, MonadThrow m, MonadReader c m)
=> MGRequest -> (JS.Value -> Maybe [r]) -> SourceT m r
paginatedStream rq respHandle = preplan $ do
dmn <- lift $ view mailgunApiDomain
let upre = T.pack $ printf "https://%s" dmn
pure $ getStream Nothing
(\case
Nothing -> ((), rq)
Just s ->
((), MGGet (const . maybe (error "no url pre") T.unpack .
T.stripPrefix upre $ s) []))
(\() j -> let mr = respHandle j
in fmap (\case
[] -> (Nothing, [])
r -> (j^?key "paging".key "next"._JSON, r)) mr)