{-# LANGUAGE FlexibleContexts #-} {- | Module : Web.RBB.Blog Description : Very experimental Blog-serving facilties Copyright : (c) Sebastian Witte License : BSD3 Maintainer : woozletoff@gmail.com Stability : experimental -} module Web.RBB.Blog ( withBlog , blogEntries , Blog , getBlogConfig ) where import Control.Concurrent import Control.Concurrent.STM import Control.Monad.Reader import Control.Monad.Trans.Except import Data.IxSet (toList) import Data.List (sortBy) import qualified Data.Map as Map import Data.Time import System.Exit (exitFailure) import System.IO import Web.RBB.Blog.Query import Web.RBB.Config import Web.RBB.Converter (renderEntries) import Web.RBB.Crawler import Web.RBB.Types.Blog hiding (Blog) import qualified Web.RBB.Types.Blog as Internal import Web.RBB.Types.CachedEntry import Web.RBB.Types.Entry import Web.RBB.Util -- | A value of this type contains all the data needed for the blog module to -- operate. newtype Blog m = Blog (Maybe (TVar (Internal.Blog m))) -- TODO saep 2014-11-09 remove the Maybe wrapper? -- | Retrieve the 'BlogConfig' from the 'Blog' value. Due to the resuorce -- managmeent that the 'Blog' data type encapsulates, this function only works -- inside an 'IO' monad. getBlogConfig :: (Functor io, MonadIO io) => Blog m -> io (Maybe (BlogConfig m)) getBlogConfig (Blog Nothing) = return Nothing getBlogConfig (Blog (Just blog)) = Just . view blogConfig <$> liftIO (readTVarIO blog) -- | Create a 'Blog' object by providing a 'BlogConfig' value. -- This function also starts threads which will handle the resource management -- with some configurable settings that can be defined in the 'BlogConfig'. withBlog :: BlogConfig m -> (Blog m -> IO ()) -> IO () withBlog cfg action = do mb <- runExceptT $ initBlog cfg case mb of Left err -> do hPutStrLn stderr err exitFailure Right b -> do tb <- atomically $ newTVar b _ <- forkIO $ manageEntryCache tb (b^.blogCacheChannel) action $ Blog (Just tb) -- | Retrieve an 'IxSet' of blog 'Entry' values. If blogEntries :: (Functor io, MonadIO io, Monad m) => Blog m -- ^ Blog configuration -> EntryQuery -- ^ Sorting order of the entries -> Maybe (IxSet Entry -> IxSet Entry) -- ^ Query function -> io (m Html) blogEntries (Blog Nothing) _ _ = return $ return mempty blogEntries (Blog (Just tb)) eq qfun = do _ <- liftIO . forkIO $ manageEntryUpdates tb b <- liftIO $ readTVarIO tb let es = toList . fromMaybe id qfun $ b^.entries renderEntries b $ sortBy (eqSortBy eq) es -- | On a site rendering request, test whether the entry repository should -- check for updates. manageEntryUpdates :: TVar (Internal.Blog m) -> IO () manageEntryUpdates tb = do b <- liftIO $ readTVarIO tb let luc = b^.lastUpdateCheck now <- liftIO getCurrentTime let interval = max 1 . fromInteger . updateInterval $ b^.blogConfig shouldCheckForUpdate <- liftIO . atomically $ updateUpdateTime now luc interval when shouldCheckForUpdate $ do blog <- liftIO $ readTVarIO tb u <- runExceptT $ updateBlog blog case u of Left err -> hPutStrLn stderr err Right blog' -> liftIO . atomically $ writeTVar tb blog' where updateUpdateTime :: UTCTime -> UTCTime -> NominalDiffTime-> STM Bool updateUpdateTime now luc interval | diffUTCTime now luc > interval * 60 {- seconds -} = do modifyTVar tb $ lastUpdateCheck .~ now return True | otherwise = return False manageEntryCache :: TVar (Internal.Blog m) -> TChan (Integer, CachedEntry) -> IO () manageEntryCache tb tc = forever $ do (i,h) <- atomically $ readTChan tc atomically . modifyTVar tb $ blogEntryCache %~ Map.insert i h