FeedMonad/FeedMonad/src/FeedMonad.hs

119 lines
4.1 KiB
Haskell
Raw Normal View History

{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TupleSections #-}
2021-11-15 20:49:51 +02:00
{-# LANGUAGE LambdaCase #-}
2021-11-11 19:04:48 +02:00
module FeedMonad where
import Control.Exception (bracket)
import Control.Monad.App (App, runApp)
import Control.Monad.HTTP (execute, fetch)
import Control.Monad.Reader (asks)
2021-11-15 20:49:51 +02:00
import Control.Monad.Trans (liftIO, MonadIO)
import Data.Acid (AcidState(closeAcidState), openLocalState, query, update)
import Data.Category (Category)
import Data.Environment
2021-11-15 20:49:51 +02:00
import Data.Foldable (for_, toList, traverse_)
2021-11-11 19:04:48 +02:00
import Data.Text (Text)
import Database
( CountEntries(CountEntries)
, FeedId(FeedId)
, SaveEntries(SaveEntries)
, emptyFeedMonadState, UnseenEntries (UnseenEntries), EntryId (EntryId)
)
import Feed.Parser (parseEntries)
2021-11-11 19:04:48 +02:00
import Middleware (Middleware)
2021-11-11 22:08:46 +02:00
import Network.HTTP.Client.TLS (newTlsManager)
import Numeric.Natural (Natural)
2021-11-11 22:40:47 +02:00
import Text.Feed.Import (parseFeedSource)
import Data.Entry (Entry(entryURL))
import qualified Data.Map.Strict as M
2021-11-15 20:49:51 +02:00
import qualified Data.Text.IO as TI
import Trace
import qualified Data.Text as T
import Data.Functor.Contravariant ((>$<))
import Text.Printf (printf)
import Data.Either (partitionEithers)
2021-11-11 19:04:48 +02:00
newtype Minutes = Minutes Natural
data FeedMonad = FeedMonad
2021-11-11 23:00:50 +02:00
{ feeds :: [Category FeedId]
2021-11-11 19:04:48 +02:00
-- ^ The forest of urls for the feeds. It's a forest because of the categories
, filters :: Middleware
-- ^ The middleware. Modifies the scoring, tags and content
, refreshTime :: Minutes
-- ^ How often to refresh the feeds
, secretToken :: Text
-- ^ Used for authenticating the UI. This is a single user app
-- served over http, so we can get around with hardcoded
-- authentication token
}
2021-11-11 20:55:29 +02:00
defaultConfig :: FeedMonad
defaultConfig = FeedMonad
{ feeds = []
, filters = id
, refreshTime = Minutes 30
, secretToken = "i am a secret"
}
2021-11-15 20:49:51 +02:00
data TraceMsg
= UpdateFeed FeedId UpdateMsg
| UpdateFeeds
2021-11-15 20:49:51 +02:00
data UpdateMsg
= Start
| NewEntries Int
| Failures [Text]
formatTraceMsg :: TraceMsg -> Maybe Text
formatTraceMsg (UpdateFeed fi Start) = Just $ T.pack $ printf "Updating feed %s" (show fi)
formatTraceMsg (UpdateFeed fi (NewEntries n)) = Just $ T.pack $ printf "Feed (%s) has %d new entries" (show fi) n
formatTraceMsg (UpdateFeed _ (Failures [])) = Nothing
formatTraceMsg (UpdateFeed fi (Failures failures)) = Just $ T.pack $ printf "Feed (%s) has %d failures: %s" (show fi) (length failures) (show failures)
formatTraceMsg UpdateFeeds = Just $ T.pack $ printf "Updating feeds"
logTrace :: MonadIO m => Trace m (Maybe Text)
logTrace = Trace $ \case
Nothing -> pure ()
Just msg -> liftIO . TI.putStrLn $ msg
updateFeeds :: Trace App TraceMsg -> FeedMonad -> App ()
updateFeeds trace f = do
runTrace trace UpdateFeeds
for_ (feeds f) $
traverse_ (\fid -> updateFeed (UpdateFeed fid >$< trace) fid)
where
updateFeed :: Trace App UpdateMsg -> FeedId -> App ()
updateFeed t fid = do
let FeedId u = fid
mgr <- asks environmentManager
st <- asks environmentAcidState
runTrace t Start
(failures, entries) <- liftIO (partitionEithers . maybe [] parseEntries . parseFeedSource <$> liftIO (execute mgr (fetch u)))
runTrace t (Failures failures)
finalEntries <- liftIO (foldMap (\e -> M.singleton (EntryId $ entryURL e) e) <$> traverse (execute mgr . filters f pure) entries)
newEntries <- liftIO (query st (UnseenEntries fid (M.keysSet finalEntries)))
runTrace t (NewEntries (length newEntries))
liftIO (update st (SaveEntries fid (foldMap (\eid -> toList $ M.lookup eid finalEntries) newEntries)))
queryCategory :: FeedMonad -> App [Category (FeedId, Int)]
queryCategory = traverse (traverse q) . feeds
2021-11-11 23:00:50 +02:00
where
q :: FeedId -> App (FeedId, Int)
q fid = do
st <- asks environmentAcidState
(fid, ) <$> liftIO (query st (CountEntries fid))
2021-11-11 23:00:50 +02:00
2021-11-11 19:04:48 +02:00
defaultMain :: FeedMonad -> IO ()
2021-11-15 20:49:51 +02:00
defaultMain f = do
let trace = formatTraceMsg >$< logTrace
2021-11-11 22:08:46 +02:00
bracket (openLocalState emptyFeedMonadState) closeAcidState $ \st -> do
mgr <- newTlsManager
runApp (Environment mgr st) $ do
2021-11-15 20:49:51 +02:00
updateFeeds trace f
cat <- queryCategory f
liftIO $ print cat