telegraph-0.1.0: src/Web/Telegraph/API.hs
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE UndecidableInstances #-}
-- | The telegraph API.
-- Every function that runs in 'MonadTelegraph' might throw a 'TelegraphError'.
module Web.Telegraph.API
( -- ** Types
Telegraph (..),
MonadTelegraph (..),
AccountInfo (..),
-- ** Interpreting 'MonadTelegraph'
TelegraphT (..),
runTelegraph,
runTelegraph',
-- ** Type Synonyms
HasHttpCap,
-- ** Account related APIs
editAccountInfo,
getAccountInfo,
revokeAccessToken,
createPage,
editPage,
getPageList,
-- ** Account independent APIs
createAccount,
getAccountInfo',
getPage,
getTotalViews,
-- ** Image uploading API
uploadImageFromFile,
uploadImageFromFiles,
ImgStream (..),
uploadImageStreaming,
uploadImagesStreaming,
uploadParts,
)
where
import Conduit
( ConduitT,
sourceHandle,
)
import Control.Concurrent
import Control.Exception (throwIO)
import Control.Monad.Base
import Control.Monad.Catch
import Control.Monad.Cont
import Control.Monad.Reader
import Control.Monad.Trans.Control
import Data.Aeson (eitherDecode, encode, object, (.=))
import Data.ByteString (ByteString)
import Data.Function ((&))
import Data.Maybe
import Data.Text (Text, pack, unpack)
import Deriving.Aeson
import Deriving.Aeson.Stock
import Network.HTTP.Client.Conduit
import Network.HTTP.Client.MultipartFormData
import System.IO
import Web.Telegraph.Types
import Prelude as P
type HasHttpCap env m = (MonadIO m, HasHttpManager env, MonadReader env m)
class MonadThrow m => MonadTelegraph m where
takeTelegraph :: m Telegraph
readTelegraph :: m Telegraph
putTelegraph :: Telegraph -> m ()
newtype TelegraphT m a = TelegraphT {runTelegraphT :: ReaderT (MVar Telegraph) m a}
deriving newtype
( Functor,
Applicative,
Monad,
MonadTrans,
MonadIO,
MonadThrow,
MonadCatch,
MonadMask,
MonadBase b,
MonadBaseControl b
)
instance MonadReader r m => MonadReader r (TelegraphT m) where
ask = lift ask
local f m = do
ref <- TelegraphT ask
lift $ local f $ flip runReaderT ref $ runTelegraphT m
instance (MonadThrow m, MonadIO m) => MonadTelegraph (TelegraphT m) where
takeTelegraph = TelegraphT ask >>= liftIO . takeMVar
readTelegraph = TelegraphT ask >>= liftIO . readMVar
putTelegraph t = TelegraphT ask >>= \ref -> liftIO $ putMVar ref t
instance
{-# OVERLAPPABLE #-}
( MonadTelegraph m,
MonadTrans f,
MonadThrow (f m)
) =>
MonadTelegraph (f m)
where
takeTelegraph = lift takeTelegraph
readTelegraph = lift readTelegraph
putTelegraph = lift . putTelegraph
data Telegraph = Telegraph
{ accessToken :: Text,
shortName :: Text,
authorName :: Text,
authorUrl :: Text
}
deriving (Show, Eq, Generic)
data AccountInfo = AccountInfo
{ shortName :: Text,
authorName :: Text,
authorUrl :: Text
}
deriving (Show, Eq, Generic)
deriving (FromJSON, ToJSON) via Snake AccountInfo
-- | Use this method to create a new Telegraph account
createAccount :: HasHttpCap env m => AccountInfo -> m (Result Account)
createAccount !a = postAeson "https://api.telegra.ph/createAccount" a
-- | Use this method to update information about this Telegraph account
editAccountInfo :: (HasHttpCap env m, MonadTelegraph m, MonadMask m) => AccountInfo -> m ()
editAccountInfo AccountInfo {..} =
bracketOnError
takeTelegraph
putTelegraph
$ \t@Telegraph {accessToken} -> do
let o =
object
[ "access_token" .= accessToken,
"short_name" .= shortName,
"author_name" .= authorName,
"author_url" .= authorUrl
]
r <- postAeson "https://api.telegra.ph/editAccountInfo" o
case r of
Error e -> do
putTelegraph t
throwM $ APICallFailure e
Result Account {} -> do
let t' = Telegraph {..}
putTelegraph t'
-- | Use this method to get information about this Telegraph account
getAccountInfo :: (HasHttpCap env m, MonadTelegraph m) => m Account
getAccountInfo = do
Telegraph {accessToken} <- readTelegraph
r <- getAccountInfo' accessToken
case r of
Error e -> throwM $ APICallFailure e
Result a -> pure a
getAccountInfo' :: HasHttpCap env m => Text -> m (Result Account)
getAccountInfo' accessToken = postAeson "https://api.telegra.ph/getAccountInfo" o
where
fields :: [Text]
fields = ["short_name", "author_name", "author_url", "auth_url", "page_count"]
o =
object
[ "access_token" .= accessToken,
"fields" .= fields
]
-- | Use this method to revoke access_token and generate a new one
revokeAccessToken :: (HasHttpCap env m, MonadTelegraph m, MonadMask m) => m Account
revokeAccessToken =
bracketOnError
takeTelegraph
putTelegraph
$ \Telegraph {..} -> do
let o = object ["access_token" .= accessToken]
r <- postAeson "https://api.telegra.ph/revokeAccessToken" o
case r of
Error e -> throwM $ APICallFailure e
Result a@Account {accessToken = accessToken'} -> do
let t' = Telegraph {accessToken = fromJust accessToken', ..}
putTelegraph t'
pure a
data CreatePage = CreatePage
{ accessToken :: Text,
title :: Text,
authorName :: Maybe Text,
authorUrl :: Maybe Text,
content :: [Node],
returnContent :: Bool
}
deriving (Show, Eq, Generic)
deriving (FromJSON, ToJSON) via Snake CreatePage
-- | Use this method to create a new Telegraph page
createPage ::
(HasHttpCap env m, MonadTelegraph m) =>
-- | title
Text ->
-- | content
[Node] ->
m Page
createPage title content = do
Telegraph {..} <- readTelegraph
let o =
object
[ "access_token" .= accessToken,
"title" .= title,
"author_name" .= authorName,
"author_url" .= authorUrl,
"content" .= content
]
r <- postAeson "https://api.telegra.ph/createPage" o
case r of
Error e -> throwM $ APICallFailure e
Result p -> pure p
-- | Use this method to edit an existing Telegraph page
editPage ::
(HasHttpCap env m, MonadTelegraph m) =>
-- | path
Text ->
-- | title
Text ->
-- | content
[Node] ->
m Page
editPage path title content = do
Telegraph {..} <- readTelegraph
let o =
object
[ "access_token" .= accessToken,
"path" .= path,
"title" .= title,
"author_name" .= authorName,
"author_url" .= authorUrl,
"content" .= content
]
r <- postAeson "https://api.telegra.ph/editPage" o
case r of
Error e -> throwM $ APICallFailure e
Result p -> pure p
-- | Use this method to get a Telegraph page
getPage :: HasHttpCap env m => Text -> m (Result Page)
getPage path = do
let o =
object
[ "path" .= path,
"return_content" .= True
]
postAeson "https://api.telegra.ph/getPage" o
-- | Use this method to get a list of pages belonging to this Telegraph account
getPageList ::
(HasHttpCap env m, MonadTelegraph m) =>
-- | offset
Int ->
-- | limit (0 - 200)
Int ->
m PageList
getPageList offset limit = do
Telegraph {..} <- readTelegraph
let o =
object
[ "access_token" .= accessToken,
"offset" .= offset,
"limit" .= limit
]
r <- postAeson "https://api.telegra.ph/getPageList" o
case r of
Error e -> throwM $ APICallFailure e
Result p -> pure p
-- | Use this method to get the total number of views for a Telegraph article
getTotalViews :: HasHttpCap env m => Text -> m (Result PageViews)
getTotalViews path = postAeson "https://api.telegra.ph/getViews" o
where
o = object ["path" .= path]
--------------------------------------------------
-- Upload API
uploadParts :: HasHttpCap env m => [PartM m] -> m UploadResult
uploadParts parts = do
let initReq = parseRequest_ "POST https://telegra.ph/upload"
boundary <- liftIO webkitBoundary
req <- formDataBodyWithBoundary boundary parts initReq
resp <- httpLbs req
case eitherDecode (responseBody resp) of
Left e -> P.error ("impossible: json decode failure: " ++ e)
Right r -> pure r
-- | Upload a image from a filepath to Telegraph
uploadImageFromFile :: (HasHttpCap env m, MonadMask m) => FilePath -> m UploadResult
uploadImageFromFile fp =
evalContT $ do
src <- withSourceFile fp
let body = requestBodySourceChunked src
part = partFileRequestBody "file" fp body
lift $ uploadParts [part]
-- | Upload a list of images to Telegraph. The resulting list of images will be in the same order
uploadImageFromFiles :: (HasHttpCap env m, MonadMask m) => [FilePath] -> m UploadResult
uploadImageFromFiles fps =
evalContT $ do
srcs <- traverse withSourceFile fps
let bodies = map requestBodySourceChunked srcs
parts = zipWith (\fp -> partFileRequestBody (pack fp) fp) fps bodies
lift $ uploadParts parts
data ImgStream = ImgStream
{ -- | an image stream needs a filename
name :: Text,
stream :: forall i n. MonadIO n => ConduitT i ByteString n ()
}
imgStream2Part :: Applicative m => ImgStream -> PartM m
imgStream2Part ImgStream {..} = partFileRequestBody name (unpack name) body
where
body = requestBodySourceChunked stream
-- | Upload a image stream to Telegraph
uploadImageStreaming :: HasHttpCap env m => ImgStream -> m UploadResult
uploadImageStreaming imgs = uploadParts [imgStream2Part imgs]
-- | Upload a list of image streams to Telegraph. The resulting list of images
uploadImagesStreaming :: HasHttpCap env m => [ImgStream] -> m UploadResult
uploadImagesStreaming imgss = uploadParts $ map imgStream2Part imgss
--------------------------------------------------
-- Utils
postAeson :: (ToJSON a, FromJSON b, HasHttpCap env m) => String -> a -> m b
postAeson url c = do
let req =
(parseRequest_ url)
{ method = "POST",
requestBody = RequestBodyLBS $ encode c,
requestHeaders =
[ ("content-type", "application/json"),
("accept", "application/json")
]
}
resp <- httpLbs req
case eitherDecode (responseBody resp) of
Left e -> P.error ("impossible: json decode failure: " ++ e)
Right r -> pure r
-- | interprets 'TelegraphT' using the access token of an existing account
runTelegraph :: HasHttpCap env m => Text -> TelegraphT m a -> m a
runTelegraph accessToken m = do
r <- getAccountInfo' accessToken
case r of
Error e -> liftIO $ throwIO $ APICallFailure e
Result Account {shortName, authorName, authorUrl} -> do
let t = Telegraph {..}
ref <- liftIO $ newMVar t
m
& runTelegraphT
& flip runReaderT ref
-- | Create a new account and interprets 'TelegraphT' using that account
runTelegraph' :: HasHttpCap env m => AccountInfo -> TelegraphT m a -> m a
runTelegraph' acc m = do
r <- createAccount acc
case r of
Error e -> liftIO $ throwIO $ APICallFailure e
Result Account {shortName, authorName, authorUrl, accessToken = accessToken'} -> do
let t = Telegraph {accessToken = fromJust accessToken', ..}
ref <- liftIO $ newMVar t
m
& runTelegraphT
& flip runReaderT ref
evalContT :: Applicative m => ContT r m r -> m r
evalContT m = runContT m pure
{-# INLINE evalContT #-}
withSourceFile :: (MonadMask m, MonadIO m, MonadIO n) => FilePath -> ContT r m (ConduitT i ByteString n ())
withSourceFile fp = ContT $ \k ->
bracket
(liftIO $ openBinaryFile fp ReadMode)
(liftIO . hClose)
(k . sourceHandle)