packages feed

imm 1.1.0.0 → 1.2.0.0

raw patch · 17 files changed

+220/−107 lines, 17 filesdep +xml-typesdep ~rss-conduitdep ~xml-conduitPVP ok

version bump matches the API change (PVP)

Dependencies added: xml-types

Dependency ranges changed: rss-conduit, xml-conduit

API changes (from Hackage documentation)

- Imm.Prelude: (*:*) :: (Functor f, Functor g) => (a -> f a) -> (b -> g b) -> (a, b) -> Product f g (a, b)
- Imm.Prelude: (>:) :: a -> b -> (a, b)
+ Imm.Feed: prettyElement :: FeedElement -> Doc
+ Imm.Prelude: (*:) :: (Functor f, Functor g) => (a -> f a) -> (b -> g b) -> (a, b) -> Product f g (a, b)
+ Imm.Prelude: (+:) :: a -> b -> (a, b)
+ Imm.XML: CoXmlParserF :: (URI -> LByteString -> m (Either SomeException Feed, a)) -> CoXmlParserF m a
+ Imm.XML: ParseXml :: URI -> LByteString -> (Either SomeException Feed -> next) -> XmlParserF next
+ Imm.XML: [parseXmlH] :: CoXmlParserF m a -> URI -> LByteString -> m (Either SomeException Feed, a)
+ Imm.XML: data XmlParserF next
+ Imm.XML: instance GHC.Base.Functor Imm.XML.XmlParserF
+ Imm.XML: instance GHC.Base.Functor m => GHC.Base.Functor (Imm.XML.CoXmlParserF m)
+ Imm.XML: instance GHC.Base.Monad m => Imm.Prelude.PairingM (Imm.XML.CoXmlParserF m) Imm.XML.XmlParserF m
+ Imm.XML: newtype CoXmlParserF m a
+ Imm.XML: parseXml :: (MonadFree f m, XmlParserF :<: f, MonadThrow m) => URI -> LByteString -> m Feed
+ Imm.XML.Simple: defaultPreProcess :: Monad m => PreProcess m
+ Imm.XML.Simple: mkCoXmlParser :: (MonadIO m, MonadCatch m) => PreProcess m -> CoXmlParserF m (PreProcess m)
+ Imm.XML.Simple: type PreProcess m = URI -> Conduit Event m Event
- Imm.Boot: imm :: (a -> CoHttpClientF IO a, a) -> (b -> CoDatabaseF' IO b, b) -> (c -> CoLoggerF IO c, c) -> (d -> CoHooksF IO d, d) -> IO ()
+ Imm.Boot: imm :: (a -> CoHttpClientF IO a, a) -> (b -> CoDatabaseF' IO b, b) -> (c -> CoLoggerF IO c, c) -> (d -> CoHooksF IO d, d) -> (e -> CoXmlParserF IO e, e) -> IO ()
- Imm.Core: check :: (MonadIO m, MonadCatch m, LoggerF :<: f, Functor f, MonadFree f m, DatabaseF' :<: f, HttpClientF :<: f) => [FeedID] -> m ()
+ Imm.Core: check :: (MonadIO m, MonadCatch m, LoggerF :<: f, MonadFree f m, DatabaseF' :<: f, HttpClientF :<: f, XmlParserF :<: f) => [FeedID] -> m ()
- Imm.Core: importOPML :: (MonadIO m, LoggerF :<: f, Functor f, MonadFree f m, DatabaseF' :<: f, MonadCatch m) => m ()
+ Imm.Core: importOPML :: (MonadIO m, LoggerF :<: f, MonadFree f m, DatabaseF' :<: f, MonadCatch m) => m ()
- Imm.Core: run :: (MonadIO m, MonadCatch m, HooksF :<: f, LoggerF :<: f, Functor f, MonadFree f m, DatabaseF' :<: f, HttpClientF :<: f) => [FeedID] -> m ()
+ Imm.Core: run :: (MonadIO m, MonadCatch m, HooksF :<: f, LoggerF :<: f, MonadFree f m, DatabaseF' :<: f, HttpClientF :<: f, XmlParserF :<: f) => [FeedID] -> m ()
- Imm.Core: showFeed :: (MonadIO m, LoggerF :<: f, MonadThrow m, Functor f, MonadFree f m, DatabaseF' :<: f) => [FeedID] -> m ()
+ Imm.Core: showFeed :: (MonadIO m, LoggerF :<: f, MonadThrow m, MonadFree f m, DatabaseF' :<: f) => [FeedID] -> m ()
- Imm.Core: subscribe :: (LoggerF :<: f, Functor f, MonadFree f m, DatabaseF' :<: f, MonadCatch m) => URI -> Maybe Text -> m ()
+ Imm.Core: subscribe :: (LoggerF :<: f, MonadFree f m, DatabaseF' :<: f, MonadCatch m) => URI -> Maybe Text -> m ()
- Imm.Database: commit :: (MonadThrow m, Functor f, MonadFree f m, DatabaseF t :<: f, LoggerF :<: f) => t -> m ()
+ Imm.Database: commit :: (MonadThrow m, MonadFree f m, DatabaseF t :<: f, LoggerF :<: f) => t -> m ()
- Imm.Database: delete :: (MonadThrow m, Functor f, MonadFree f m, LoggerF :<: f, DatabaseF t :<: f) => t -> Key t -> m ()
+ Imm.Database: delete :: (MonadThrow m, MonadFree f m, LoggerF :<: f, DatabaseF t :<: f) => t -> Key t -> m ()
- Imm.Database: deleteList :: (MonadThrow m, Functor f, MonadFree f m, LoggerF :<: f, DatabaseF t :<: f) => t -> [Key t] -> m ()
+ Imm.Database: deleteList :: (MonadThrow m, MonadFree f m, LoggerF :<: f, DatabaseF t :<: f) => t -> [Key t] -> m ()
- Imm.Database: describeDatabase :: (Functor f, MonadFree f m, DatabaseF t :<: f) => t -> m Doc
+ Imm.Database: describeDatabase :: (MonadFree f m, DatabaseF t :<: f) => t -> m Doc
- Imm.Database: fetch :: (Functor f, MonadFree f m, DatabaseF t :<: f, Table t, MonadThrow m) => t -> Key t -> m (Entry t)
+ Imm.Database: fetch :: (MonadFree f m, DatabaseF t :<: f, Table t, MonadThrow m) => t -> Key t -> m (Entry t)
- Imm.Database: fetchAll :: (MonadThrow m, Functor f, MonadFree f m, DatabaseF t :<: f) => t -> m (Map (Key t) (Entry t))
+ Imm.Database: fetchAll :: (MonadThrow m, MonadFree f m, DatabaseF t :<: f) => t -> m (Map (Key t) (Entry t))
- Imm.Database: fetchList :: (Functor f, MonadFree f m, DatabaseF t :<: f, MonadThrow m) => t -> [Key t] -> m (Map (Key t) (Entry t))
+ Imm.Database: fetchList :: (MonadFree f m, DatabaseF t :<: f, MonadThrow m) => t -> [Key t] -> m (Map (Key t) (Entry t))
- Imm.Database: insert :: (MonadThrow m, Functor f, MonadFree f m, LoggerF :<: f, DatabaseF t :<: f) => t -> Key t -> Entry t -> m ()
+ Imm.Database: insert :: (MonadThrow m, MonadFree f m, LoggerF :<: f, DatabaseF t :<: f) => t -> Key t -> Entry t -> m ()
- Imm.Database: insertList :: (MonadThrow m, Functor f, MonadFree f m, LoggerF :<: f, DatabaseF t :<: f) => t -> [(Key t, Entry t)] -> m ()
+ Imm.Database: insertList :: (MonadThrow m, MonadFree f m, LoggerF :<: f, DatabaseF t :<: f) => t -> [(Key t, Entry t)] -> m ()
- Imm.Database: purge :: (MonadThrow m, Functor f, MonadFree f m, DatabaseF t :<: f, LoggerF :<: f) => t -> m ()
+ Imm.Database: purge :: (MonadThrow m, MonadFree f m, DatabaseF t :<: f, LoggerF :<: f) => t -> m ()
- Imm.Database: update :: (Functor f, MonadFree f m, DatabaseF t :<: f, MonadThrow m) => t -> Key t -> (Entry t -> Entry t) -> m ()
+ Imm.Database: update :: (MonadFree f m, DatabaseF t :<: f, MonadThrow m) => t -> Key t -> (Entry t -> Entry t) -> m ()
- Imm.Database.FeedTable: addReadHash :: (DatabaseF' :<: f, Functor f, MonadFree f m, MonadThrow m, LoggerF :<: f) => FeedID -> Int -> m ()
+ Imm.Database.FeedTable: addReadHash :: (DatabaseF' :<: f, MonadFree f m, MonadThrow m, LoggerF :<: f) => FeedID -> Int -> m ()
- Imm.Database.FeedTable: getStatus :: (DatabaseF' :<: f, Functor f, MonadFree f m, MonadCatch m) => FeedID -> m FeedStatus
+ Imm.Database.FeedTable: getStatus :: (DatabaseF' :<: f, MonadFree f m, MonadCatch m) => FeedID -> m FeedStatus
- Imm.Database.FeedTable: markAsRead :: (MonadIO m, DatabaseF' :<: f, Functor f, MonadFree f m, MonadThrow m, LoggerF :<: f) => FeedID -> m ()
+ Imm.Database.FeedTable: markAsRead :: (MonadIO m, DatabaseF' :<: f, MonadFree f m, MonadThrow m, LoggerF :<: f) => FeedID -> m ()
- Imm.Database.FeedTable: markAsUnread :: (DatabaseF' :<: f, Functor f, MonadFree f m, MonadThrow m, LoggerF :<: f) => FeedID -> m ()
+ Imm.Database.FeedTable: markAsUnread :: (DatabaseF' :<: f, MonadFree f m, MonadThrow m, LoggerF :<: f) => FeedID -> m ()
- Imm.Database.FeedTable: register :: (MonadThrow m, LoggerF :<: f, DatabaseF' :<: f, Functor f, MonadFree f m) => FeedID -> Text -> m ()
+ Imm.Database.FeedTable: register :: (MonadThrow m, LoggerF :<: f, DatabaseF' :<: f, MonadFree f m) => FeedID -> Text -> m ()
- Imm.HTTP: get :: (MonadFree f m, Functor f, HttpClientF :<: f, LoggerF :<: f, MonadThrow m) => URI -> m LByteString
+ Imm.HTTP: get :: (MonadFree f m, HttpClientF :<: f, LoggerF :<: f, MonadThrow m) => URI -> m LByteString
- Imm.Hooks: onNewElement :: (Functor f, MonadFree f m, LoggerF :<: f, HooksF :<: f) => Feed -> FeedElement -> m ()
+ Imm.Hooks: onNewElement :: (MonadFree f m, LoggerF :<: f, HooksF :<: f) => Feed -> FeedElement -> m ()
- Imm.Logger: flushLogs :: (Functor f, MonadFree f m, LoggerF :<: f) => m ()
+ Imm.Logger: flushLogs :: (MonadFree f m, LoggerF :<: f) => m ()
- Imm.Logger: getLogLevel :: (Functor f, MonadFree f m, LoggerF :<: f) => m LogLevel
+ Imm.Logger: getLogLevel :: (MonadFree f m, LoggerF :<: f) => m LogLevel
- Imm.Logger: log :: (Functor f, MonadFree f m, LoggerF :<: f) => LogLevel -> Doc -> m ()
+ Imm.Logger: log :: (MonadFree f m, LoggerF :<: f) => LogLevel -> Doc -> m ()
- Imm.Logger: logDebug :: (Functor f, MonadFree f m, LoggerF :<: f) => Doc -> m ()
+ Imm.Logger: logDebug :: (MonadFree f m, LoggerF :<: f) => Doc -> m ()
- Imm.Logger: logError :: (Functor f, MonadFree f m, LoggerF :<: f) => Doc -> m ()
+ Imm.Logger: logError :: (MonadFree f m, LoggerF :<: f) => Doc -> m ()
- Imm.Logger: logInfo :: (Functor f, MonadFree f m, LoggerF :<: f) => Doc -> m ()
+ Imm.Logger: logInfo :: (MonadFree f m, LoggerF :<: f) => Doc -> m ()
- Imm.Logger: logWarning :: (Functor f, MonadFree f m, LoggerF :<: f) => Doc -> m ()
+ Imm.Logger: logWarning :: (MonadFree f m, LoggerF :<: f) => Doc -> m ()
- Imm.Logger: setColorizeLogs :: (Functor f, MonadFree f m, LoggerF :<: f) => Bool -> m ()
+ Imm.Logger: setColorizeLogs :: (MonadFree f m, LoggerF :<: f) => Bool -> m ()
- Imm.Logger: setLogLevel :: (Functor f, MonadFree f m, LoggerF :<: f) => LogLevel -> m ()
+ Imm.Logger: setLogLevel :: (MonadFree f m, LoggerF :<: f) => LogLevel -> m ()
- Imm.Prelude: infixr 0 *:*
+ Imm.Prelude: infixr 0 *:

Files

imm.cabal view
@@ -1,9 +1,9 @@ name:                imm-version:             1.1.0.0+version:             1.2.0.0 synopsis:            Execute arbitrary actions for each unread element of RSS/Atom feeds description:         Cf README file homepage:            https://github.com/k0ral/imm-license:             OtherLicense+license:             PublicDomain license-file:        LICENSE author:              kamaradclimber, koral maintainer:          koral <koral@mailoo.org>@@ -33,6 +33,8 @@     Imm.Logger     Imm.Logger.Simple     Imm.Prelude+    Imm.XML+    Imm.XML.Simple   other-modules:     Imm.Aeson     Imm.Dyre@@ -40,7 +42,7 @@     Imm.Options     Imm.Pretty     Paths_imm-  build-depends: aeson, atom-conduit >= 0.4, base == 4.*, blaze-html, blaze-markup, bytestring, case-insensitive, chunked-data >= 0.3.0, comonad, conduit, conduit-combinators, connection, containers, directory >= 1.2.3.0, dyre, fast-logger, filepath, free, hashable, HaskellNet, HaskellNet-SSL >= 0.3.3.0, http-client >= 0.4.30, http-client-tls, http-types, mime-mail, monoid-subclasses, mono-traversable >= 1.0.0, network, opml-conduit >= 0.6, optparse-applicative, rainbow, rainbox, rss-conduit >= 0.3, safe-exceptions, tagged, text, transformers, time, timerep >= 2.0.0.0, tls, uri-bytestring, xml, xml-conduit, ansi-wl-pprint+  build-depends: aeson, ansi-wl-pprint, atom-conduit >= 0.4, base == 4.*, blaze-html, blaze-markup, bytestring, case-insensitive, chunked-data >= 0.3.0, comonad, conduit, conduit-combinators, connection, containers, directory >= 1.2.3.0, dyre, fast-logger, filepath, free, hashable, HaskellNet, HaskellNet-SSL >= 0.3.3.0, http-client >= 0.4.30, http-client-tls, http-types, mime-mail, monoid-subclasses, mono-traversable >= 1.0.0, network, opml-conduit >= 0.6, optparse-applicative, rainbow, rainbox, rss-conduit >= 0.3.1, safe-exceptions, tagged, text, transformers, time, timerep >= 2.0.0.0, tls, uri-bytestring, xml, xml-conduit >= 1.5, xml-types   -- Build-tools:   hs-source-dirs: src/lib   ghc-options: -Wall -fno-warn-unused-do-bind
src/bin/Executable.hs view
@@ -5,19 +5,19 @@ -- {{{ Imports import           Imm import           Imm.Database.JsonFile-import qualified Imm.Hooks.WriteFile   as WriteFile import           Imm.HTTP.Simple import           Imm.Logger.Simple import           Imm.Prelude+import           Imm.XML.Simple  import           System.Exit -- }}} --- mkDummyCoHooks :: (MonadIO m, MonadThrow m) => () -> CoHooksF m ()--- mkDummyCoHooks _ = CoHooksF coOnNewElement where---   coOnNewElement _ _ = do---     io $ putStrLn "No hook defined."---     throwM $ ExitFailure 1+mkDummyCoHooks :: (MonadIO m, MonadThrow m) => () -> CoHooksF m ()+mkDummyCoHooks _ = CoHooksF coOnNewElement where+  coOnNewElement _ _ = do+    io $ putStrLn "No hook defined."+    throwM $ ExitFailure 1   main :: IO ()@@ -26,4 +26,4 @@   manager <- defaultManager   database <- defaultDatabase -  imm (mkCoHttpClient, manager) (mkCoDatabase, database) (mkCoLogger, logger) (WriteFile.mkCoHooks, WriteFile.defaultSettings "/home/koral/feeds")+  imm (mkCoHttpClient, manager) (mkCoDatabase, database) (mkCoLogger, logger) (mkDummyCoHooks, ()) (mkCoXmlParser, defaultPreProcess)
src/lib/Imm/Boot.hs view
@@ -37,6 +37,7 @@ import Imm.Options as Options hiding(logLevel) import Imm.Prelude import Imm.Pretty+import Imm.XML  import Control.Comonad.Cofree import Control.Monad.Trans.Free@@ -58,6 +59,7 @@ -- > import           Imm.Hooks.SendMail -- > import           Imm.HTTP.Simple -- > import           Imm.Logger.Simple+-- > import           Imm.XML.Simple -- > -- > main :: IO () -- > main = do@@ -65,7 +67,7 @@ -- >   manager  <- defaultManager -- >   database <- defaultDatabase -- >--- >   imm (mkCoHttpClient, manager) (mkCoDatabase, database) (mkCoLogger, logger) (mkCoHooks, sendmail)+-- >   imm (mkCoHttpClient, manager) (mkCoDatabase, database) (mkCoLogger, logger) (mkCoHooks, sendmail) (mkCoXmlParser, defaultPreProcess) -- > -- > sendmail :: SendMailSettings -- > sendmail = SendMailSettings smtpServer formatMail@@ -82,14 +84,15 @@ -- >   (Just $ Authentication PLAIN "user" "password") -- >   (StartTls "smtp.host" defaultSettingsSMTPSTARTTLS) imm :: (a -> CoHttpClientF IO a, a)  -- ^ HTTP client interpreter (cf "Imm.HTTP")-    -> (b -> CoDatabaseF' IO b, b)  -- ^ Database interpreter (cf "Imm.Database")-    -> (c -> CoLoggerF IO c, c)     -- ^ Logger interpreter (cf "Imm.Logger")-    -> (d -> CoHooksF IO d, d)      -- ^ Hooks interpreter (cf "Imm.Hooks")+    -> (b -> CoDatabaseF' IO b, b)   -- ^ Database interpreter (cf "Imm.Database")+    -> (c -> CoLoggerF IO c, c)      -- ^ Logger interpreter (cf "Imm.Logger")+    -> (d -> CoHooksF IO d, d)       -- ^ Hooks interpreter (cf "Imm.Hooks")+    -> (e -> CoXmlParserF IO e, e)   -- ^ XML parsing interpreter (cf "Imm.XML")     -> IO ()-imm coHttpClient coDatabase coLogger coHooks = void $ do+imm coHttpClient coDatabase coLogger coHooks coXmlParser = void $ do   options <- parseOptions   Dyre.wrap (optionDyreMode options) realMain (optionCommand options, optionLogLevel options, optionColorizeLogs options, coiter next start)-  where (next, start) = mkCoImm coHttpClient coDatabase coLogger coHooks+  where (next, start) = mkCoImm coHttpClient coDatabase coLogger coHooks coXmlParser  realMain :: (MonadIO m, PairingM (CoImmF m) ImmF m, MonadCatch m)          => (Command, LogLevel, Bool, Cofree (CoImmF m) a) -> m ()@@ -117,14 +120,22 @@  -- * DSL/interpreter model -type CoImmF m = Product (CoHttpClientF m) (Product (CoDatabaseF' m) (Product (CoLoggerF m) (CoHooksF m)))-type ImmF = Sum HttpClientF (Sum DatabaseF' (Sum LoggerF HooksF))+type CoImmF m = Product (CoHttpClientF m)+  (Product (CoDatabaseF' m)+   (Product (CoLoggerF m)+    (Product (CoHooksF m) (CoXmlParserF m)+    )))+type ImmF = Sum HttpClientF (Sum DatabaseF' (Sum LoggerF (Sum HooksF XmlParserF)))  mkCoImm :: (Functor m)-        => (a -> CoHttpClientF m a, a) -> (b -> CoDatabaseF' m b, b) -> (c -> CoLoggerF m c, c) -> (d -> CoHooksF m d, d)-        -> ((a ::: b ::: c ::: d) -> CoImmF m (a ::: b ::: c ::: d), a ::: b ::: c ::: d)-mkCoImm (coHttpClient, a) (coDatabase, b) (coLogger, c) (coHooks, d) =-  (coHttpClient *:* coDatabase *:* coLogger *:* coHooks, a >: b >: c >: d)+        => (a -> CoHttpClientF m a, a)+        -> (b -> CoDatabaseF' m b, b)+        -> (c -> CoLoggerF m c, c)+        -> (d -> CoHooksF m d, d)+        -> (e -> CoXmlParserF m e, e)+        -> ((a ::: b ::: c ::: d ::: e) -> CoImmF m (a ::: b ::: c ::: d ::: e), a ::: b ::: c ::: d ::: e)+mkCoImm (coHttpClient, a) (coDatabase, b) (coLogger, c) (coHooks, d) (coXmlParser, e) =+  (coHttpClient *: coDatabase *: coLogger *: coHooks *: coXmlParser, a +: b +: c +: d +: e)   -- * Util@@ -144,7 +155,7 @@   unless (null x || x == ("Y" :: Text)) $ throwM InterruptedException  -resolveTarget :: (MonadIO m, MonadThrow m, Functor f, MonadFree f m, DatabaseF' :<: f)+resolveTarget :: (MonadIO m, MonadThrow m, MonadFree f m, DatabaseF' :<: f)               => SafeGuard -> Maybe Core.FeedRef -> m [FeedID] resolveTarget s Nothing = do   result <- keys <$> Database.fetchAll FeedTable
src/lib/Imm/Core.hs view
@@ -29,6 +29,7 @@ import Imm.Logger import Imm.Prelude import Imm.Pretty+import Imm.XML  -- import Control.Concurrent.Async.Lifted (Async, async, mapConcurrently, waitAny) -- import Control.Concurrent.Async.Pool@@ -51,12 +52,8 @@  import           System.Info -import Text.Atom.Conduit.Parse-import Text.Atom.Types import Text.OPML.Conduit.Parse import Text.OPML.Types as OPML-import Text.RSS.Conduit.Parse-import Text.RSS.Types import Text.XML as XML () import Text.XML.Stream.Parse as XML @@ -70,7 +67,7 @@   putStrLn $ "compiled by " ++ compilerName ++ "-" ++ showVersion compilerVersion  -- | Print database status for given feed(s)-showFeed :: (MonadIO m, LoggerF :<: f, MonadThrow m, Functor f, MonadFree f m, DatabaseF' :<: f)+showFeed :: (MonadIO m, LoggerF :<: f, MonadThrow m, MonadFree f m, DatabaseF' :<: f)          => [FeedID] -> m () showFeed feedIDs = do   feeds <- Database.fetchList FeedTable feedIDs@@ -78,38 +75,41 @@   if null feeds then logWarning "No subscription" else putBox $ entryTableToBox feeds  -- | Register the given feed URI in database-subscribe :: (LoggerF :<: f, Functor f, MonadFree f m, DatabaseF' :<: f, MonadCatch m)+subscribe :: (LoggerF :<: f, MonadFree f m, DatabaseF' :<: f, MonadCatch m)           => URI -> Maybe Text -> m () subscribe uri category = Database.register (FeedID uri) $ fromMaybe "default" category  -- | Check for unread elements without processing them-check :: (MonadIO m, MonadCatch m, LoggerF :<: f, Functor f, MonadFree f m, DatabaseF' :<: f, HttpClientF :<: f)+check :: (MonadIO m, MonadCatch m, LoggerF :<: f, MonadFree f m, DatabaseF' :<: f, HttpClientF :<: f, XmlParserF :<: f)       => [FeedID] -> m () check feedIDs = do-  results <- forM (zip ([1..] :: [Int]) feedIDs) $ \(i, feedID) -> do+  results <- for (zip ([1..] :: [Int]) feedIDs) $ \(i, feedID) -> do     logInfo $ brackets (fill width (bold $ cyan $ pretty i) <+> "/" <+> pretty total) <+> "Checking" <+> magenta (pretty feedID) <> "..."     try $ checkOne feedID +  flushLogs+   putBox $ statusTableToBox $ mapFromList $ zip feedIDs results++  let (failures, successes) = partitionEithers $ zipWith (\a -> bimap (a,) (a,)) feedIDs results+  unless (null failures) $ logError $ bold (pretty $ length failures) <+> "feeds in error"+  forM_ failures $ \(feedID, e) ->+    logError $ indent 2 (pretty feedID <++> indent 2 (pretty $ displayException e))+   where width = length (show total :: String)         total = length feedIDs -checkOne :: (MonadIO m, MonadCatch m, LoggerF :<: f, Functor f, MonadFree f m, DatabaseF' :<: f, HttpClientF :<: f)+checkOne :: (MonadIO m, MonadCatch m, LoggerF :<: f, MonadFree f m, DatabaseF' :<: f, HttpClientF :<: f, XmlParserF :<: f)          => FeedID -> m Int-checkOne feedID@(FeedID uri) = do-  body <- HTTP.get uri-  feed <- runConduit $ parseLBS def body =$= force "Invalid feed" ((fmap Left <$> atomFeed) `orE` (fmap Right <$> rssDocument))-+checkOne feedID = do+  feed <- getFeed feedID   case feed of-    Left _ -> logDebug $ "Parsed Atom feed: " <> pretty feedID-    Right _ -> logDebug $ "Parsed RSS feed: " <> pretty feedID+    Atom _ -> logDebug $ "Parsed Atom feed: " <> pretty feedID+    Rss _ -> logDebug $ "Parsed RSS feed: " <> pretty feedID -  let dates = either-              (map entryUpdated . feedEntries)-              (mapMaybe itemPubDate . channelItems)-              feed+  let dates = mapMaybe getDate $ getElements feed -  logDebug $ vsep $ either (map prettyEntry . feedEntries) (map prettyItem . channelItems) feed+  logDebug $ vsep $ map prettyElement $ getElements feed   status <- Database.getStatus feedID    return $ length $ filter (unread status) dates@@ -117,10 +117,10 @@         unread _ _                = True  -run :: (MonadIO m, MonadCatch m, HooksF :<: f, LoggerF :<: f, Functor f, MonadFree f m, DatabaseF' :<: f, HttpClientF :<: f)+run :: (MonadIO m, MonadCatch m, HooksF :<: f, LoggerF :<: f, MonadFree f m, DatabaseF' :<: f, HttpClientF :<: f, XmlParserF :<: f)     => [FeedID] -> m () run feedIDs = do-  results <- forM (zip ([1..] :: [Int]) feedIDs) $ \(i, feedID) -> do+  results <- for (zip ([1..] :: [Int]) feedIDs) $ \(i, feedID) -> do     logInfo $ brackets (fill width (bold $ cyan $ pretty i) <+> "/" <+> pretty total) <+> "Processing" <+> magenta (pretty feedID) <> "..."     result <- tryAny $ runOne feedID     return $ bimap (feedID,) (feedID,) result@@ -136,14 +136,13 @@   where width = length (show total :: String)         total = length feedIDs -runOne :: (MonadIO m, MonadCatch m, HooksF :<: f, LoggerF :<: f, Functor f, MonadFree f m, DatabaseF' :<: f, HttpClientF :<: f)+runOne :: (MonadIO m, MonadCatch m, HooksF :<: f, LoggerF :<: f, MonadFree f m, DatabaseF' :<: f, HttpClientF :<: f, XmlParserF :<: f)     => FeedID -> m ()-runOne feedID@(FeedID uri) = do-  body <- HTTP.get uri-  feed <- runConduit $ parseLBS def body =$= force "Invalid feed" ((fmap Atom <$> atomFeed) `orE` (fmap Rss <$> rssDocument))+runOne feedID = do+  feed <- getFeed feedID   unreadElements <- filterM (fmap not . isRead feedID) $ getElements feed -  unless (null unreadElements) $ logInfo $ indent 2 $ green (pretty $ length unreadElements) <+> "unread element(s)"+  unless (null unreadElements) $ logInfo $ indent 2 $ green (pretty $ length unreadElements) <+> "new element(s)"    forM_ unreadElements $ \element -> do     onNewElement feed element@@ -152,7 +151,7 @@   Database.markAsRead feedID  -isRead :: (Functor f, MonadCatch m, DatabaseF' :<: f, MonadFree f m) => FeedID -> FeedElement -> m Bool+isRead :: (MonadCatch m, DatabaseF' :<: f, MonadFree f m) => FeedID -> FeedElement -> m Bool isRead feedID element = do   DatabaseEntry _ _ readHashes lastCheck <- Database.fetch FeedTable feedID   let matchHash = not $ null $ (setFromList (getHashes element) :: Set Int) `intersection` readHashes@@ -163,18 +162,23 @@   return $ matchHash || matchDate  -- | 'subscribe' to all feeds described by the OPML document provided in input (stdin)-importOPML :: (MonadIO m, LoggerF :<: f, Functor f, MonadFree f m, DatabaseF' :<: f, MonadCatch m) => m ()+importOPML :: (MonadIO m, LoggerF :<: f, MonadFree f m, DatabaseF' :<: f, MonadCatch m) => m () importOPML = do   opml <- runConduit $ Conduit.stdin =$= XML.parseBytes def =$= force "Invalid OPML" parseOpml   forM_ (opmlOutlines opml) $ importOPML' mempty -importOPML' :: (MonadIO m, LoggerF :<: f, Functor f, MonadFree f m, DatabaseF' :<: f, MonadCatch m)+importOPML' :: (MonadIO m, LoggerF :<: f, MonadFree f m, DatabaseF' :<: f, MonadCatch m)             => Maybe Text -> Tree OpmlOutline -> m () importOPML' _ (Node (OpmlOutlineGeneric b _) sub) = mapM_ (importOPML' (Just . toNullable $ OPML.text b)) sub importOPML' c (Node (OpmlOutlineSubscription _ s) _) = subscribe (xmlUri s) c importOPML' _ _ = return ()  +getFeed :: (MonadIO m, MonadCatch m, MonadFree f m, HttpClientF :<: f, LoggerF :<: f, XmlParserF :<: f)+        => FeedID -> m Feed+getFeed (FeedID uri) = HTTP.get uri >>= parseXml uri++ -- * Boxes  putBox :: (Orientation a, MonadIO m) => Box a -> m ()@@ -202,6 +206,6 @@ statusTableToBox t = tableByColumns $ Rainbox.intersperse sep $ fromList [col1, col2, col3] where   result = sortBy (comparing fst) $ Map.toList t   col1 = fromList $ cell "# UNREAD" : map (cell . either (const "?") show . snd) result-  col2 = fromList $ cell "STATUS" : map (cell . either (fromString . displayException) (const "OK") . snd) result+  col2 = fromList $ cell "STATUS" : map (cell . either (const "ERROR") (const "OK") . snd) result   col3 = fromList $ cell "FEED" : map (cell . show . pretty . fst) result   sep = fromList [separator mempty 2]
src/lib/Imm/Database.hs view
@@ -111,62 +111,62 @@  -- * Primitives -describeDatabase :: (Functor f, MonadFree f m, DatabaseF t :<: f)+describeDatabase :: (MonadFree f m, DatabaseF t :<: f)                  => t -> m Doc describeDatabase t = liftF . inj $ Describe t id -fetch :: (Functor f, MonadFree f m, DatabaseF t :<: f, Table t, MonadThrow m)+fetch :: (MonadFree f m, DatabaseF t :<: f, Table t, MonadThrow m)       => t -> Key t -> m (Entry t) fetch t k = do   results <- liftF . inj $ FetchList t [k] id   result <- lookup k <$> liftE results   maybe (throwM $ NotFound t [k]) return result -fetchList :: (Functor f, MonadFree f m, DatabaseF t :<: f, MonadThrow m)+fetchList :: (MonadFree f m, DatabaseF t :<: f, MonadThrow m)           => t -> [Key t] -> m (Map (Key t) (Entry t)) fetchList t k = do   result <- liftF . inj $ FetchList t k id   liftE result -fetchAll :: (MonadThrow m, Functor f, MonadFree f m, DatabaseF t :<: f) => t -> m (Map (Key t) (Entry t))+fetchAll :: (MonadThrow m, MonadFree f m, DatabaseF t :<: f) => t -> m (Map (Key t) (Entry t)) fetchAll t = do   result <- liftF . inj $ FetchAll t id   liftE result -update :: (Functor f, MonadFree f m, DatabaseF t :<: f, MonadThrow m)+update :: (MonadFree f m, DatabaseF t :<: f, MonadThrow m)        => t -> Key t -> (Entry t -> Entry t) -> m () update t k f = do   result <- liftF . inj $ Update t k f id   liftE result -insert :: (MonadThrow m, Functor f, MonadFree f m, LoggerF :<: f, DatabaseF t :<: f)+insert :: (MonadThrow m, MonadFree f m, LoggerF :<: f, DatabaseF t :<: f)        => t -> Key t -> Entry t -> m () insert t k v = insertList t [(k, v)] -insertList :: (MonadThrow m, Functor f, MonadFree f m, LoggerF :<: f, DatabaseF t :<: f)+insertList :: (MonadThrow m, MonadFree f m, LoggerF :<: f, DatabaseF t :<: f)            => t -> [(Key t, Entry t)] -> m () insertList t i = do   logInfo $ "Inserting " <> yellow (pretty $ length i) <> " entries..."   result <- liftF . inj $ InsertList t i id   liftE result -delete :: (MonadThrow m, Functor f, MonadFree f m, LoggerF :<: f, DatabaseF t :<: f) => t -> Key t -> m ()+delete :: (MonadThrow m, MonadFree f m, LoggerF :<: f, DatabaseF t :<: f) => t -> Key t -> m () delete t k = deleteList t [k] -deleteList :: (MonadThrow m, Functor f, MonadFree f m, LoggerF :<: f, DatabaseF t :<: f)+deleteList :: (MonadThrow m, MonadFree f m, LoggerF :<: f, DatabaseF t :<: f)            => t -> [Key t] -> m () deleteList t k = do   logInfo $ "Deleting " <> yellow (pretty $ length k) <> " entries..."   result <- liftF . inj $ DeleteList t k id   liftE result -purge :: (MonadThrow m, Functor f, MonadFree f m, DatabaseF t :<: f, LoggerF :<: f) => t -> m ()+purge :: (MonadThrow m, MonadFree f m, DatabaseF t :<: f, LoggerF :<: f) => t -> m () purge t = do   logInfo "Purging database..."   result <- liftF . inj $ Purge t id   liftE result -commit :: (MonadThrow m, Functor f, MonadFree f m, DatabaseF t :<: f, LoggerF :<: f) => t -> m ()+commit :: (MonadThrow m, MonadFree f m, DatabaseF t :<: f, LoggerF :<: f) => t -> m () commit t = do   logDebug "Committing database transaction..."   result <- liftF . inj $ Commit t id
src/lib/Imm/Database/FeedTable.hs view
@@ -95,19 +95,19 @@  -- * Primitives -register :: (MonadThrow m, LoggerF :<: f, DatabaseF' :<: f, Functor f, MonadFree f m)+register :: (MonadThrow m, LoggerF :<: f, DatabaseF' :<: f, MonadFree f m)           => FeedID -> Text -> m () register feedID category = do   logInfo $ "Registering feed " <> magenta (pretty feedID) <> "..."   insert FeedTable feedID $ newDatabaseEntry feedID category -getStatus :: (DatabaseF' :<: f, Functor f, MonadFree f m, MonadCatch m)+getStatus :: (DatabaseF' :<: f, MonadFree f m, MonadCatch m)           => FeedID -> m FeedStatus getStatus feedID = handleAny (\_ -> return Unknown) $ do   result <- fmap Just (fetch FeedTable feedID) `catchAny` (\_ -> return Nothing)   return $ maybe New LastUpdate $ entryLastCheck =<< result -addReadHash :: (DatabaseF' :<: f, Functor f, MonadFree f m, MonadThrow m, LoggerF :<: f)+addReadHash :: (DatabaseF' :<: f, MonadFree f m, MonadThrow m, LoggerF :<: f)                => FeedID -> Int -> m () addReadHash feedID hash = do   logDebug $ "Adding read hash: " <> pretty hash <> "..."@@ -115,7 +115,7 @@   where f a = a { entryReadHashes = insertSet hash $ entryReadHashes a }  -- | Set the last check time to now-markAsRead :: (MonadIO m, DatabaseF' :<: f, Functor f, MonadFree f m, MonadThrow m, LoggerF :<: f)+markAsRead :: (MonadIO m, DatabaseF' :<: f, MonadFree f m, MonadThrow m, LoggerF :<: f)            => FeedID -> m () markAsRead feedID = do   logDebug $ "Marking feed as read: " <> pretty feedID <> "..."@@ -124,7 +124,7 @@   where f time a = a { entryLastCheck = Just time }  -- | Unset feed's last update and remove all read hashes-markAsUnread ::  (DatabaseF' :<: f, Functor f, MonadFree f m, MonadThrow m, LoggerF :<: f)+markAsUnread ::  (DatabaseF' :<: f, MonadFree f m, MonadThrow m, LoggerF :<: f)              => FeedID -> m () markAsUnread feedID = do   logInfo $ "Marking feed as unread: " <> show (pretty feedID) <> "..."
src/lib/Imm/Database/JsonFile.hs view
@@ -39,9 +39,9 @@ mkJsonFileDatabase :: (Table t) => FilePath -> JsonFileDatabase t mkJsonFileDatabase file = JsonFileDatabase file mempty Empty --- | Default database is stored in @$XDG_DATA_HOME\/imm\/feeds.json@+-- | Default database is stored in @$XDG_CONFIG_HOME\/imm\/feeds.json@ defaultDatabase :: Table t => IO (JsonFileDatabase t)-defaultDatabase = mkJsonFileDatabase <$> getXdgDirectory XdgData "imm/feeds.json"+defaultDatabase = mkJsonFileDatabase <$> getXdgDirectory XdgConfig "imm/feeds.json"   data JsonException = UnableDecode
src/lib/Imm/Feed.hs view
@@ -7,7 +7,6 @@ import           Imm.Pretty  import           Data.Hashable-import           Data.NonNull import           Data.Time  import           Text.Atom.Types@@ -57,3 +56,10 @@   <> [hash $ itemTitle item]   <> [hash $ itemDescription item] getHashes (AtomElement entry) = [hash $ entryId entry, (hash :: String -> Int) $ show $ prettyAtomText $ entryTitle entry]+++-- * Misc++prettyElement :: FeedElement -> Doc+prettyElement (RssElement item)   = prettyItem item+prettyElement (AtomElement entry) = prettyEntry entry
src/lib/Imm/HTTP.hs view
@@ -40,7 +40,7 @@ -- * Primitives  -- | Perform an HTTP GET request-get :: (MonadFree f m, Functor f, HttpClientF :<: f, LoggerF :<: f, MonadThrow m)+get :: (MonadFree f m, HttpClientF :<: f, LoggerF :<: f, MonadThrow m)     => URI -> m LByteString get uri = do   logDebug $ "Fetching " <> prettyURI uri
src/lib/Imm/Hooks.hs view
@@ -37,7 +37,7 @@  -- * Primitives -onNewElement :: (Functor f, MonadFree f m, LoggerF :<: f, HooksF :<: f) => Feed -> FeedElement -> m ()+onNewElement :: (MonadFree f m, LoggerF :<: f, HooksF :<: f) => Feed -> FeedElement -> m () onNewElement feed element = do   logDebug $ "Unread element:" <+> textual (getTitle element)   liftF . inj $ OnNewElement feed element ()
src/lib/Imm/Hooks/WriteFile.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE FlexibleContexts  #-} {-# LANGUAGE NamedFieldPuns    #-} {-# LANGUAGE NoImplicitPrelude #-} {-# LANGUAGE OverloadedStrings #-}@@ -11,7 +12,9 @@ import           Imm.Prelude import           Imm.Pretty -import           Data.Monoid.Textual           hiding (map)+import           Control.Arrow++import           Data.Monoid.Textual           hiding (elem, map) import qualified Data.Text.Lazy                as Text import           Data.Time @@ -19,9 +22,10 @@ import           System.FilePath  import           Text.Atom.Types-import qualified Text.Blaze                    as Blaze import           Text.Blaze.Html.Renderer.Text-import           Text.Blaze.Html5              as H hiding (map)+import           Text.Blaze.Html5              (Html, docTypeHtml,+                                                preEscapedToHtml, (!))+import qualified Text.Blaze.Html5              as H hiding (map) import           Text.Blaze.Html5.Attributes   as H (charset, href) import           Text.RSS.Types @@ -57,9 +61,12 @@ defaultFilePath :: FilePath -> Feed -> FeedElement -> FilePath defaultFilePath root feed element = makeValid $ root </> feedTitle </> fileName <.> "html" where   date = maybe "" (formatTime defaultTimeLocale "%F-") $ getDate element-  fileName = date <> convertText (sanitizePath $ getTitle element)-  feedTitle = convertText $ sanitizePath $ getFeedTitle feed-  sanitizePath = intercalate "-" . split isPathSeparator+  fileName = date <> sanitize (convertText $ getTitle element)+  feedTitle = sanitize $ convertText $ getFeedTitle feed+  sanitize = replaceIf isPathSeparator '-' >>> replaceAny ".?!#" '_'+  replaceAny :: [Char] -> Char -> String -> String+  replaceAny list = replaceIf (`elem` list)+  replaceIf f b = map (\c -> if f c then b else c)  -- | Generate an HTML page, with a title, a header and an article that contains the feed element defaultFileContent :: Feed -> FeedElement -> ByteString@@ -67,9 +74,9 @@   H.head $ do     H.meta ! H.charset "utf-8"     H.title $ convertText $ getFeedTitle feed <> " | " <> getTitle element-  body $ do+  H.body $ do     H.h1 $ convertText $ getFeedTitle feed-    article $ do+    H.article $ do       defaultHeader feed element       defaultBody feed element @@ -78,29 +85,29 @@  -- | Generate an HTML @<header>@ for a given feed element defaultHeader :: Feed -> FeedElement -> Html-defaultHeader _ element@(RssElement item) = header $ do+defaultHeader _ element@(RssElement item) = H.header $ do   H.h2 $ maybe id (\uri -> H.a ! H.href uri) link $ convertText $ getTitle element-  unless (null author) $ address $ "Published by " >> convertText author-  forM_ (itemPubDate item) $ \date -> p $ " on " >> time (convertDoc $ prettyTime date)+  unless (null author) $ H.address $ "Published by " >> convertText author+  forM_ (itemPubDate item) $ \date -> H.p $ " on " >> H.time (convertDoc $ prettyTime date)   where link = withRssURI (convertDoc . prettyURI) <$> itemLink item         author = itemAuthor item-defaultHeader _ element@(AtomElement entry) = header $ do+defaultHeader _ element@(AtomElement entry) = H.header $ do   H.h2 $ convertText $ getTitle element-  address $ do+  H.address $ do     "Published by "     forM_ (entryAuthors entry) $ \author -> do       convertDoc $ prettyPerson author       ", "-  p $ "on " >> time (convertDoc $ prettyTime $ entryUpdated entry)+  H.p $ "on " >> H.time (convertDoc $ prettyTime $ entryUpdated entry)  -- | Generate the HTML content for a given feed element defaultBody :: Feed -> FeedElement -> Html-defaultBody _ (RssElement item) = p $ preEscapedToHtml $ itemDescription item+defaultBody _ (RssElement item) = H.p $ preEscapedToHtml $ itemDescription item defaultBody _ (AtomElement entry) = do-  unless (null links) $ p $ do+  unless (null links) $ H.p $ do     "Related links:"     H.ul $ forM_ links $ \uri -> H.li (H.a ! H.href (convertAtomURI uri) $ convertAtomURI uri)-  p $ preEscapedToHtml $ fromMaybe "<empty>" $ content <|> summary+  H.p $ preEscapedToHtml $ fromMaybe "<empty>" $ content <|> summary   where links   = map linkHref $ entryLinks entry         content = show . prettyAtomContent <$> entryContent entry :: Maybe Text         summary = show . prettyAtomText <$> entrySummary entry :: Maybe Text
src/lib/Imm/Logger.hs view
@@ -64,24 +64,24 @@  -- * Primitives -log :: (Functor f, MonadFree f m, LoggerF :<: f) => LogLevel -> Doc -> m ()+log :: (MonadFree f m, LoggerF :<: f) => LogLevel -> Doc -> m () log level message = liftF . inj $ Log level message () -getLogLevel :: (Functor f, MonadFree f m, LoggerF :<: f) => m LogLevel+getLogLevel :: (MonadFree f m, LoggerF :<: f) => m LogLevel getLogLevel = liftF . inj $ GetLevel id -setLogLevel :: (Functor f, MonadFree f m, LoggerF :<: f) => LogLevel -> m ()+setLogLevel :: (MonadFree f m, LoggerF :<: f) => LogLevel -> m () setLogLevel level = liftF . inj $ SetLevel level () -setColorizeLogs :: (Functor f, MonadFree f m, LoggerF :<: f) => Bool -> m ()+setColorizeLogs :: (MonadFree f m, LoggerF :<: f) => Bool -> m () setColorizeLogs colorize = liftF . inj $ SetColorize colorize () -flushLogs :: (Functor f, MonadFree f m, LoggerF :<: f) => m ()+flushLogs :: (MonadFree f m, LoggerF :<: f) => m () flushLogs = liftF . inj $ Flush ()  -- * Helpers -logDebug, logInfo, logWarning, logError :: (Functor f, MonadFree f m, LoggerF :<: f) => Doc -> m ()+logDebug, logInfo, logWarning, logError :: (MonadFree f m, LoggerF :<: f) => Doc -> m () logDebug = log Debug logInfo = log Info logWarning = log Warning
src/lib/Imm/Options.hs view
@@ -68,7 +68,7 @@ --         ++ catMaybes [("CONFIG=" ++) <$> opts^.configurationLabel_]  parseOptions :: (MonadIO m) => m CliOptions-parseOptions = io $ customExecParser (defaultPrefs {- noBacktrack -} ) (info parser $ progDesc "Fetch elements from RSS/Atom feeds and execute arbitrary actions for each of them.")+parseOptions = io $ customExecParser (prefs noBacktrack) (info parser $ progDesc "Convert items from RSS/Atom feeds to mails.")   where parser = helper <*> optional dyreMasterBinary *> optional dyreDebug *> cliOptions  
src/lib/Imm/Prelude.hs view
@@ -45,7 +45,7 @@ import           Data.Tagged import qualified Data.Text                       as T (Text ()) import qualified Data.Text.Lazy                  as LT (Text ())-import           Data.Traversable                as X (forM)+import           Data.Traversable                as X (for, forM) import           Data.Typeable                   as X  import qualified GHC.Show                        as Show@@ -81,13 +81,13 @@ infixr 0 :::  -- | Right-associative tuple data-constructor-(>:) :: a -> b -> (a,b)-(>:) a b = (a, b)-infixr 0 >:+(+:) :: a -> b -> (a,b)+(+:) a b = (a, b)+infixr 0 +: -(*:*) :: (Functor f, Functor g) => (a -> f a) -> (b -> g b) -> (a, b) -> Product f g (a, b)-(*:*) f g (a,b) = Pair ((,b) <$> f a) ((a,) <$> g b)-infixr 0 *:*+(*:) :: (Functor f, Functor g) => (a -> f a) -> (b -> g b) -> (a, b) -> Product f g (a, b)+(*:) f g (a,b) = Pair ((,b) <$> f a) ((a,) <$> g b)+infixr 0 *:   data HLeft
src/lib/Imm/Pretty.hs view
@@ -16,7 +16,8 @@ import           Text.Atom.Types              as Atom -- import           Text.OPML.Types              as OPML hiding (text) -- import qualified Text.OPML.Types              as OPML-import           Text.PrettyPrint.ANSI.Leijen as X hiding ((<$>), (</>), (<>))+import           Text.PrettyPrint.ANSI.Leijen as X hiding (sep, width, (<$>),+                                                    (</>), (<>)) import           Text.RSS.Types               as RSS  import           URI.ByteString
+ src/lib/Imm/XML.hs view
@@ -0,0 +1,45 @@+{-# LANGUAGE DeriveFunctor         #-}+{-# LANGUAGE FlexibleContexts      #-}+{-# LANGUAGE FlexibleInstances     #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE NoImplicitPrelude     #-}+{-# LANGUAGE TypeOperators         #-}+-- | DSL/interpreter model for parsing XML into a 'Feed'+module Imm.XML where++-- {{{ Imports+import           Imm.Error+import           Imm.Feed+import           Imm.Prelude++import           Control.Monad.Trans.Free++import           URI.ByteString+-- }}}++-- * Types++-- | XML parsing DSL+data XmlParserF next+  = ParseXml URI LByteString (Either SomeException Feed -> next)+  deriving(Functor)++-- | XML parsing interpreter+newtype CoXmlParserF m a = CoXmlParserF+  { parseXmlH :: URI -> LByteString -> m (Either SomeException Feed, a)+  } deriving(Functor)++instance Monad m => PairingM (CoXmlParserF m) XmlParserF m where+  -- pairM :: (a -> b -> m r) -> f a -> g b -> m r+  pairM f (CoXmlParserF p) (ParseXml uri bytestring next) = do+    (result, a) <- p uri bytestring+    f a $ next result++-- * Primitives++-- | Parse XML into a 'Feed'+parseXml :: (MonadFree f m, XmlParserF :<: f, MonadThrow m)+         => URI -> LByteString -> m Feed+parseXml uri bytestring = do+  result <- liftF . inj $ ParseXml uri bytestring id+  liftE result
+ src/lib/Imm/XML/Simple.hs view
@@ -0,0 +1,37 @@+{-# LANGUAGE NoImplicitPrelude #-}+{-# LANGUAGE OverloadedStrings #-}+-- | Simple interpreter to parse XML into 'Feed', based on 'Conduit'.+module Imm.XML.Simple where++-- {{{ Imports+import           Imm.Feed+import           Imm.Prelude+import           Imm.XML++import           Control.Monad+import           Control.Monad.Fix++import           Data.Conduit+import           Data.XML.Types++import           Text.Atom.Conduit.Parse+import           Text.RSS.Conduit.Parse+import           Text.RSS1.Conduit.Parse+import           Text.XML.Stream.Parse++import           URI.ByteString+-- }}}++-- | A 'Conduit' to alter the raw XML before feeding it to the parser, depending on the feed 'URI'+type PreProcess m = URI -> Conduit Event m Event++-- | Interpreter for 'XmlParserF'+mkCoXmlParser :: (MonadIO m, MonadCatch m) => PreProcess m -> CoXmlParserF m (PreProcess m)+mkCoXmlParser preProcess = CoXmlParserF coParse where+  coParse uri bytestring = handleAny (\e -> return (Left e, preProcess)) $ do+    result <- runConduit $ parseLBS def bytestring =$= preProcess uri =$= force "Invalid feed" ((fmap Atom <$> atomFeed) `orE` (fmap Rss <$> rssDocument) `orE` (fmap Rss <$> rss1Document))+    return (Right result, preProcess)++-- | Default pre-process always forwards all 'Event's+defaultPreProcess :: Monad m => PreProcess m+defaultPreProcess _ = fix $ \loop -> await >>= maybe (return ()) (yield >=> const loop)