pixiv-0.1.0: src/Web/Pixiv/Types/PixivT.hs
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE UndecidableInstances #-}
-- | Copyright: (c) 2021 The closed eye of love
-- SPDX-License-Identifier: BSD-3-Clause
-- Maintainer: Poscat <poscat@mail.poscat.moe>, berberman <berberman@yandex.com>
-- Stability: alpha
-- Portability: portable
-- The core monad of this library. 'PixivT' maintains a pixiv login state,
-- and provides an environment to perform computations created by servant.
module Web.Pixiv.Types.PixivT
( -- * ClientT monad transformer
ClientT (..),
runClientT,
mkDefaultClientEnv,
-- * MonadPixiv class
MonadPixiv (..),
PixivState (..),
-- * PixivT monad transformer
PixivT (..),
liftC,
runPixivT,
runPixivT',
-- * Token
TokenState (..),
computeTokenState,
-- * Utilities
getAccessToken,
getAccessTokenWithAccpetLanguage,
)
where
import Control.Concurrent.MVar
import Control.Monad.Base (MonadBase)
import Control.Monad.Catch
import Control.Monad.Except
import Control.Monad.Reader
import Control.Monad.Trans.Control (MonadBaseControl)
import Data.Function ((&))
import Data.Text (Text)
import Data.Time
import GHC.Generics (Generic)
import GHC.Show (showCommaSpace)
import Network.HTTP.Client (Manager)
import Network.HTTP.Client.TLS (newTlsManager)
import Servant.Client
import Servant.Client.Core
import Servant.Client.Internal.HttpClient
import Web.Pixiv.Auth
-- | Transformer version of 'ClientM', changing the base 'IO' to @m@.
newtype ClientT m a = ClientT
{ unClientT :: ReaderT ClientEnv (ExceptT ClientError m) a
}
deriving newtype
( Functor,
Applicative,
Monad,
MonadIO,
MonadThrow,
MonadCatch,
MonadReader ClientEnv,
MonadError ClientError,
MonadBase b,
MonadBaseControl b
)
instance MonadTrans ClientT where
lift = ClientT . lift . lift
-- | Executes a computation in the client monad.
runClientT :: ClientEnv -> ClientT m a -> m (Either ClientError a)
runClientT env m =
m
& unClientT
& flip runReaderT env
& runExceptT
instance MonadIO m => RunClient (ClientT m) where
throwClientError = throwError
runRequestAcceptStatus status req = do
env <- ask
let m = performRequest status req
res <- liftIO $ runClientM m env
liftEither res
-- | Given 'Manager', creates a 'ClientEnv' using <https://app-api.pixiv.net> as base url.
mkDefaultClientEnv :: Manager -> IO ClientEnv
mkDefaultClientEnv manager = do
baseUrl <- parseBaseUrl "https://app-api.pixiv.net"
pure $ mkClientEnv manager baseUrl
-- | Pixiv auth state.
data TokenState = TokenState
{ -- | Token to access pixiv api.
accessToken :: Token,
-- | Token to obtain new 'accessToken' without giving username and password.
refreshToken :: Token,
-- | Time stamp when 'accessToken' becomes invalid.
expirationTime :: UTCTime,
manager :: Manager
}
deriving stock (Generic)
instance Show TokenState where
showsPrec d TokenState {..} =
showParen (d >= 11) $
showString "TokenState {"
. showString "accessToken = "
. shows accessToken
. showCommaSpace
. showString "refreshToken = "
. shows refreshToken
. showCommaSpace
. showString "expirationTime = "
. shows expirationTime
. showString "}"
-- | State stored in 'MonadPixiv'.
data PixivState = PixivState
{ tokenState :: TokenState,
acceptLanguage :: Maybe Text
}
deriving stock (Generic, Show)
-- | A thread safe implementation of 'MonadPixiv'.
newtype PixivT m a = PixivT
{ unPixivT :: ReaderT (MVar PixivState) (ClientT m) a
}
deriving newtype
( Functor,
Applicative,
Monad,
MonadIO,
MonadThrow,
MonadCatch,
MonadReader (MVar PixivState),
MonadError ClientError
)
deriving stock (Generic)
instance MonadTrans PixivT where
lift = PixivT . lift . lift
deriving newtype instance MonadBase IO m => MonadBase IO (PixivT m)
deriving newtype instance MonadBaseControl IO m => MonadBaseControl IO (PixivT m)
-- | Lifts a computation in 'ClientT' to 'PixivT'.
liftC :: Monad m => ClientT m a -> PixivT m a
liftC = PixivT . lift
instance MonadIO m => RunClient (PixivT m) where
throwClientError = throwError
runRequestAcceptStatus status req =
liftC $ runRequestAcceptStatus status req
-- | The mtl-style class of pixiv monad.
class (RunClient m, MonadIO m) => MonadPixiv m where
-- | Reads the stored 'PixivState', when used in a multithreaded setting, this should block
-- all other thread from reading the 'PixivState' until 'putPixivState' is called.
takePixivState :: m PixivState
-- | Writes a new 'PixivState'.
putPixivState :: PixivState -> m ()
-- | Reads the stored 'PixivState', without blocking other threads which want to read this state.
--
-- Don't confuse with 'takePixivState', please refer to 'readMVar'.
readPixivState :: m PixivState
instance
{-# OVERLAPPABLE #-}
( MonadPixiv m,
MonadTrans f,
MonadIO (f m),
RunClient (f m)
) =>
MonadPixiv (f m)
where
takePixivState = lift takePixivState
putPixivState = lift . putPixivState
readPixivState = lift readPixivState
instance MonadIO m => MonadPixiv (PixivT m) where
takePixivState = ask >>= liftIO . takeMVar
putPixivState s = do
ref <- ask
liftIO $ putMVar ref s
readPixivState = ask >>= liftIO . readMVar
-- | Interprets the 'PixivT' effect, with a supplied 'Manager'.
runPixivT :: MonadIO m => Manager -> Credential -> PixivT m a -> m (Either ClientError a)
runPixivT manager credential m = do
t <- liftIO getCurrentTime
s <- liftIO $ computeTokenState manager credential t
clientEnv <- liftIO $ mkDefaultClientEnv manager
ref <- liftIO . newMVar $ PixivState s Nothing
m
& unPixivT
& flip runReaderT ref
& runClientT clientEnv
-- | Like 'runPixivT', but creates a new 'Manager' everytime.
runPixivT' :: MonadIO m => Credential -> PixivT m a -> m (Either ClientError a)
runPixivT' credential m = do
manager <- liftIO newTlsManager
runPixivT manager credential m
-- | Computes the 'TokenState'.
--
-- This function calls 'auth'' to perform authentication.
computeTokenState ::
Manager ->
-- | Could be username with password or 'refreshToken'.
Credential ->
-- | Current time.
UTCTime ->
IO TokenState
computeTokenState manager credential time = do
OAuth2Token {..} <- liftIO $ auth' manager credential
let offset = oa_expiresIn `div` 5 * 4
diff = secondsToNominalDiffTime $ toEnum offset
accessToken = oa_accessToken
refreshToken = oa_refreshToken
expirationTime = addUTCTime diff time
pure TokenState {..}
-- | Retrieves the 'accessToken' from pixiv monad.
--
-- If the token is overdue, it will call 'computeTokenState' to refresh.
getAccessToken :: MonadPixiv m => m Token
getAccessToken = do
s@PixivState {tokenState = TokenState {..}} <- takePixivState
t <- liftIO getCurrentTime
if t < expirationTime
then do
putPixivState s
pure accessToken
else do
let credential = RefreshToken refreshToken
ts <- liftIO $ computeTokenState manager credential t
putPixivState s {tokenState = ts}
pure accessToken
-- | Retrieves the 'acceptLanguage' from pixiv monad.
getAccpetLanguage :: MonadPixiv m => m (Maybe Text)
getAccpetLanguage = acceptLanguage <$> readPixivState
-- | Retrieves the 'accessToken' and 'acceptLanguage' in one go.
getAccessTokenWithAccpetLanguage :: MonadPixiv m => m (Token, Maybe Text)
getAccessTokenWithAccpetLanguage = (,) <$> getAccessToken <*> getAccpetLanguage