packages feed

imm 0.6.0.0 → 0.6.0.1

raw patch · 13 files changed

+176/−184 lines, 13 filesdep +transformersPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependencies added: transformers

API changes (from Hackage documentation)

- Imm.Config: class ConfigReader m
- Imm.Config: instance [overlap ok] Monad m => ConfigReader (ReaderT Config m)
- Imm.Config: instance [overlap ok] MonadBase IO m => DatabaseWriter (ReaderT Config (ErrorT ImmError m))
- Imm.Config: localConfig :: ConfigReader m => (Config -> Config) -> m a -> m a
- Imm.Config: readConfig :: ConfigReader m => Simple Lens Config a -> m a
- Imm.Maildir: add :: (MonadBase IO m, MonadError ImmError m) => FilePath -> Mail -> m ()
- Imm.Maildir: create :: (MonadBase IO m, MonadError ImmError m) => FilePath -> m ()
- Imm.OPML: read :: String -> Maybe [(String, [String])]
- Imm.Options: Help :: Action
- Imm.Options: Import :: Action
- Imm.Options: Recompile :: Action
- Imm.Options: Run :: Action -> Action
- Imm.Options: ShowVersion :: Action
- Imm.Options: action :: Lens' CliOptions Action
- Imm.Options: class OptionsReader m
- Imm.Options: data Action
- Imm.Options: data CliOptions
- Imm.Options: dataDirectory :: Lens' CliOptions (Maybe FilePath)
- Imm.Options: dyreMode :: Lens' CliOptions Mode
- Imm.Options: feedsList :: Lens' CliOptions [URI]
- Imm.Options: instance Default Action
- Imm.Options: instance Default CliOptions
- Imm.Options: instance Eq Action
- Imm.Options: instance Eq CliOptions
- Imm.Options: instance Monad m => OptionsReader (ReaderT CliOptions m)
- Imm.Options: instance OptionsReader ((->) CliOptions)
- Imm.Options: instance Show Action
- Imm.Options: instance Show CliOptions
- Imm.Options: readOptions :: OptionsReader m => Simple Lens CliOptions a -> m a
- Imm.Options: run :: MonadBase IO m => ReaderT CliOptions m a -> m a
- Imm.Options: usage :: String
+ Imm: read :: String -> Maybe [(String, [String])]
+ Imm.Config: instance [overlap ok] (MonadBase IO m, MonadError ImmError m) => MaildirWriter (ReaderT Config m)
+ Imm.Config: instance [overlap ok] (MonadError ImmError m, MonadBase IO m) => DatabaseWriter (ReaderT Config m)
+ Imm.Core: dispatch :: (Config -> Config) -> Action -> FeedList -> IO ()
+ Imm.Maildir: class MaildirWriter m
+ Imm.Maildir: init :: MaildirWriter m => m ()
+ Imm.Maildir: instance (MonadBase IO m, MonadError ImmError m) => MaildirWriter (ReaderT Maildir m)
+ Imm.Maildir: type Maildir = FilePath
+ Imm.Maildir: write :: MaildirWriter m => Mail -> m ()
- Imm.Config: maildir :: Lens' Config FilePath
+ Imm.Config: maildir :: Lens' Config Maildir
- Imm.Core: check :: (MonadBaseControl IO m, FeedParser m, ConfigReader m, DatabaseReader m, Decoder m, MonadError ImmError m) => FeedList -> m ()
+ Imm.Core: check :: (Config -> Config) -> FeedConfig -> IO ()
- Imm.Core: markAsRead :: (MonadBase IO m, ConfigReader m, DatabaseState m, MonadError ImmError m) => FeedConfig -> m ()
+ Imm.Core: markAsRead :: (Config -> Config) -> FeedConfig -> IO ()
- Imm.Core: markAsUnread :: (MonadBase IO m, ConfigReader m, DatabaseState m, MonadError ImmError m) => FeedConfig -> m ()
+ Imm.Core: markAsUnread :: (Config -> Config) -> FeedConfig -> IO ()
- Imm.Core: showStatus :: (MonadBase IO m, ConfigReader m, DatabaseReader m, MonadError ImmError m) => FeedConfig -> m ()
+ Imm.Core: showStatus :: (Config -> Config) -> FeedConfig -> IO ()
- Imm.Core: update :: (MonadBaseControl IO m, ConfigReader m, DatabaseState m, MonadError ImmError m, FeedParser m, MailFormatter m, Decoder m) => FeedList -> m ()
+ Imm.Core: update :: (Config -> Config) -> FeedConfig -> IO ()
- Imm.Database: class MonadError ImmError m => DatabaseWriter m
+ Imm.Database: class DatabaseWriter m
- Imm.Error: withError :: (Error e, Show e, MonadBase IO m) => ErrorT e m () -> m ()
+ Imm.Error: withError :: (Error e, Show e, MonadBase IO m) => String -> ErrorT e m () -> m ()
- Imm.Feed: markAsRead :: (MonadBase IO m, MonadError ImmError m, DatabaseState m) => URI -> m ()
+ Imm.Feed: markAsRead :: (MonadBase IO m, MonadError ImmError m, DatabaseWriter m) => URI -> m ()

Files

Imm/Boot.hs view
@@ -6,21 +6,23 @@ import Imm.Config import Imm.Database import Imm.Dyre as Dyre-import Imm.Error import qualified Imm.Feed as Feed-import Imm.Options (Action(..), OptionsReader(..))+import Imm.Options (Action(..)) import qualified Imm.Options as Options import Imm.Util  import Control.Lens hiding (Action, (??))-import Control.Monad.Error hiding(when)-import Control.Monad.Reader hiding(when)+import Control.Monad.Error hiding(mapM_, when)+-- import Control.Monad.Reader hiding(mapM_, when)+import Control.Monad.Trans.Maybe +import Data.Foldable import Data.Version  import Network.URI as N  import Paths_imm+import Prelude hiding (mapM_)  import System.Log.Logger import System.Exit@@ -31,30 +33,41 @@  -- | Main function to call in the configuration file. imm :: [ConfigFeed] -> IO ()-imm feedsFromConfig = Options.run $ readOptions Options.action >>= dispatch1 feedsFromConfig+imm feedsFromConfig = void . runMaybeT $ do+    options <- Options.get+    let dataDir          = view Options.dataDirectory options+        dyreMode         = view Options.dyreMode      options+        feedsFromOptions = view Options.feedsList     options+        logLevel         = view Options.logLevel      options +    action <- handleSpecialActions $ view Options.action        options -dispatch1 :: [ConfigFeed] -> Options.Action -> ReaderT Options.CliOptions IO ()-dispatch1 _ Help        = io $ putStrLn Options.usage >> exitSuccess-dispatch1 _ ShowVersion = io $ putStrLn (showVersion version) >> exitSuccess-dispatch1 _ Recompile   = io $ Dyre.recompile >>= maybe exitSuccess (\e -> putStrLn e >> exitFailure)-dispatch1 _ Import      = io getContents >>= Core.importOPML >> io exitSuccess-dispatch1 feedsFromConfig (Run action) = do-    dyreMode         <- readOptions Options.dyreMode-    feedsFromOptions <- readOptions Options.feedsList-    dataDir          <- readOptions Options.dataDirectory+    io . updateGlobalLogger rootLoggerName $ setLevel logLevel+    io . debugM "imm.options" $ "Commandline options: " ++ show options      io $ Dyre.wrap dyreMode realMain (action, dataDir, feedsFromOptions, feedsFromConfig)  -dispatch2 :: Feed.Action -> Core.FeedList -> ReaderT Config (ErrorT ImmError IO) ()-dispatch2 Feed.Check        feeds = Core.check feeds-dispatch2 Feed.ShowStatus   feeds = mapM_ Core.showStatus feeds-dispatch2 Feed.MarkAsRead   feeds = mapM_ Core.markAsRead feeds-dispatch2 Feed.MarkAsUnread feeds = mapM_ Core.markAsUnread feeds-dispatch2 Feed.Update       feeds = Core.update feeds+handleSpecialActions :: Options.Action -> MaybeT IO Feed.Action+handleSpecialActions Help         = (io $ putStrLn Options.usage) >> mzero+handleSpecialActions ShowVersion  = (io . putStrLn $ showVersion version) >> mzero+handleSpecialActions Recompile    = (io $ Dyre.recompile >>= mapM_ putStrLn) >> mzero+handleSpecialActions Import       = io getContents >>= Core.importOPML >> mzero+handleSpecialActions (Run action) = return action  +realMain :: (Feed.Action, Maybe FilePath, [URI], [ConfigFeed]) -> IO ()+realMain (action, dataDir, feedsFromOptions, feedsFromConfig) = do+    unless (null errors)  . errorM   "imm.boot" $ unlines errors+    when   (null feedsOK) $ warningM "imm.boot"   "Nothing to process. Exiting..." >> exitFailure+    -- io . debugM "imm.boot" . unlines $ "Feeds to be processed:":(map (show . snd) feedsOK)++    Core.dispatch baseConfig action feedsOK+  where+    (errors, feedsOK) = validateFeeds feedsFromConfig feedsFromOptions+    baseConfig        = maybe id (set (fileDatabase . directory)) dataDir++ validateFeeds :: [ConfigFeed] -> [URI] -> ([String], Core.FeedList) validateFeeds feedsFromConfig feedsFromOptions = (errors ++ errors', null feedsFromOptions ? feedsOK ?? feedsOK')   where@@ -62,13 +75,3 @@     validateFromOptions uri   = maybe (Left ("URI from commandline option has no configuration entry: " ++ show uri)) Right . listToMaybe . filter ((== uri) . snd) $ feedsOK     (errors,  feedsOK)        = partitionEithers $ map validateFromConfig  feedsFromConfig     (errors', feedsOK')       = partitionEithers $ map validateFromOptions feedsFromOptions---realMain :: (Feed.Action, Maybe FilePath, [URI], [ConfigFeed]) -> IO ()-realMain (action, dataDir, feedsFromOptions, feedsFromConfig) = do-    let (errors, feedsOK) = validateFeeds feedsFromConfig feedsFromOptions-    unless (null errors)  . errorM   "imm.boot" $ unlines errors-    when   (null feedsOK) $ warningM "imm.boot"   "Nothing to process. Exiting..." >> exitFailure-    -- io . debugM "imm.boot" . unlines $ "Feeds to be processed:":(map (show . snd) feedsOK)--    withError . withConfig (maybe id (set (fileDatabase . directory)) dataDir) $ dispatch2 action feedsOK
Imm/Config.hs view
@@ -12,7 +12,6 @@     formatSubject,     formatBody,     decoder,-    ConfigReader(..),     withConfig, -- * Misc     addFeeds,@@ -23,6 +22,7 @@ import Imm.Error import Imm.Feed (FeedParser) import qualified Imm.Feed as F+import Imm.Maildir (Maildir, MaildirWriter(..)) import Imm.HTTP (Decoder(..)) import qualified Imm.Mail as Mail import Imm.Util@@ -38,6 +38,8 @@ import Data.Time.RFC2822 import Data.Time.RFC3339 +import Prelude hiding(init)+ import Text.Feed.Query as F -- import Text.Feed.Types as F @@ -67,13 +69,13 @@  -- | The only exported constructor is through 'Default' class. data Config = Config {-    _maildir        :: FilePath,       -- ^ Where mails will be written-    _fileDatabase   :: FileDatabase,   -- ^ Database configuration, used to store resilient information (basically: last update time)+    _maildir        :: Maildir,                    -- ^ Where mails will be written+    _fileDatabase   :: FileDatabase,               -- ^ Database configuration, used to store resilient information (basically: last update time)     _dateParsers    :: [String -> Maybe UTCTime],  -- ^ List of date parsing functions, will be tried sequentially until one succeeds-    _formatFrom     :: FromFormat,     -- ^ Called to write the From: header of feed mails-    _formatSubject  :: SubjectFormat,  -- ^ Called to write the Subject: header of feed mails-    _formatBody     :: BodyFormat,     -- ^ Called to write the body of feed mails (sic!)-    _decoder        :: String          -- ^ 'Converter' name used to decode the HTTP response from a feed URI+    _formatFrom     :: FromFormat,                 -- ^ Called to write the From: header of feed mails+    _formatSubject  :: SubjectFormat,              -- ^ Called to write the Subject: header of feed mails+    _formatBody     :: BodyFormat,                 -- ^ Called to write the body of feed mails (sic!)+    _decoder        :: String                      -- ^ 'Converter' name used to decode the HTTP response from a feed URI }  makeLenses ''Config@@ -111,26 +113,24 @@ instance (MonadBase IO m) => DatabaseReader (ReaderT Config m) where     getLastCheck = withReaderT (view fileDatabase) . getLastCheck -instance (MonadBase IO m) => DatabaseWriter (ReaderT Config (ErrorT ImmError m)) where+instance (MonadError ImmError m, MonadBase IO m) => DatabaseWriter (ReaderT Config m) where     storeLastCheck uri = withReaderT (view fileDatabase) . storeLastCheck uri     forget             = withReaderT (view fileDatabase) . forget +instance (MonadBase IO m, MonadError ImmError m) => MaildirWriter (ReaderT Config m) where+    init       = do+        theMaildir <- asks $ view maildir+        lift $ runReaderT init theMaildir+    write mail = do+        theMaildir <- asks $ view maildir+        lift $ runReaderT (write mail) theMaildir+ instance (Monad m) => Mail.MailFormatter (ReaderT Config m) where     formatFrom    = asks $ unFromFormat    . view formatFrom     formatSubject = asks $ unSubjectFormat . view formatSubject     formatBody    = asks $ unBodyFormat    . view formatBody  --- | 'MonadReader' for 'Config'-class ConfigReader m where-    readConfig  :: Simple Lens Config a -> m a-    localConfig :: (Config -> Config) -> m a -> m a--instance (Monad m) => ConfigReader (ReaderT Config m) where-    readConfig l = return . view l =<< ask-    localConfig  = local-- withConfig :: (MonadBase IO m) => (Config -> Config) -> ReaderT Config m a -> m a withConfig f g = do     theConfig <- f <$> io def@@ -138,15 +138,34 @@ -- }}}  --- | Return the Haskell code to write in the configuration file to add a feed.+-- | Return the Haskell code to write in the configuration file to add feeds. addFeeds :: (MonadBase IO m) => [(String, [String])] -> m ()-addFeeds feeds = forM_ feeds addFeedsGroup+addFeeds feeds = do+    io . putStrLn . unlines $+        "import Imm":+        "import Control.Lens":+        "import System.FilePath":+        "":+        "main :: IO ()":+        "main = imm myFeeds":+        "":+        "maildirRoot = \"/home/<user>/feeds\"   -- TODO: fill <user>":+        "":+        ("myFeeds = concat $ " ++ intercalate ":" (map (map toLower . concat . words . fst) feeds) ++ ":[]"):+        [] +    forM_ feeds addFeedsGroup+ addFeedsGroup :: (MonadBase IO m) => (String, [String]) -> m () addFeedsGroup (groupTitle, uris) = io $ do     -- guard (not $ null uris)-    putStrLn $ "-- Group " ++ groupTitle-    putStrLn $ map toLower (concat . words $ groupTitle) ++ " = ["-    putStrLn . ("    " ++) . intercalate ",\n    " $ map show uris-    putStrLn "]"+    putStr . unlines $+        ("-- Group " ++ groupTitle):+        (groupID ++ "Config = set maildir (maildirRoot </> \"" ++ groupID ++ "\")"):+        (groupID ++ "       = zip (repeat " ++ groupID ++ "Config) $"):+        []+    putStr . unlines $ map (\u -> "    " ++ show u ++ ":") uris+    putStrLn "    []"     putStrLn ""+  where+    groupID = map toLower . concat . words $ groupTitle
Imm/Core.hs view
@@ -4,6 +4,7 @@     FeedConfig,     FeedList, -- * Actions+    dispatch,     importOPML,     check,     showStatus,@@ -18,7 +19,7 @@ import Imm.Error import Imm.Feed (FeedParser(..)) import qualified Imm.Feed as Feed-import qualified Imm.HTTP as HTTP+import Imm.Maildir (MaildirWriter(..)) import qualified Imm.Maildir as Maildir import Imm.Mail (MailFormatter(..)) import qualified Imm.Mail as Mail@@ -29,7 +30,6 @@ import Control.Monad hiding(forM_, mapM_) import Control.Monad.Error hiding(forM_, mapM_) -- import Control.Monad.Reader hiding(forM_, mapM_)-import Control.Monad.Trans.Control  import Data.Foldable hiding(foldr) import Data.Time as T@@ -48,40 +48,41 @@ -- }}}  -importOPML :: (MonadBase IO m, MonadPlus m) => String -> m ()-importOPML = mapM_ addFeeds . OPML.read+dispatch :: (Config -> Config) -> Feed.Action -> FeedList -> IO ()+dispatch baseConfig Feed.Check        feeds = void $ mapConcurrently (check baseConfig) feeds+dispatch baseConfig Feed.ShowStatus   feeds = mapM_ (showStatus baseConfig) feeds+dispatch baseConfig Feed.MarkAsRead   feeds = mapM_ (markAsRead baseConfig) feeds+dispatch baseConfig Feed.MarkAsUnread feeds = mapM_ (markAsUnread baseConfig) feeds+dispatch baseConfig Feed.Update       feeds = void $ mapConcurrently (update baseConfig) feeds  -check :: (MonadBaseControl IO m, FeedParser m, ConfigReader m, DatabaseReader m, HTTP.Decoder m, MonadError ImmError m) => FeedList -> m ()-check feeds = void . liftBaseWith $ \runInIO -> mapConcurrently (runInIO . checkFeed) feeds--checkFeed :: (MonadBase IO m, FeedParser m, ConfigReader m, DatabaseReader m, HTTP.Decoder m, MonadError ImmError m) => FeedConfig -> m ()-checkFeed (f, feedID) = localConfig f . localError "imm.core" $ Feed.download feedID >>= Feed.check+importOPML :: (MonadBase IO m, MonadPlus m) => String -> m ()+importOPML = mapM_ addFeeds . OPML.read  -showStatus :: (MonadBase IO m, ConfigReader m, DatabaseReader m, MonadError ImmError m) => FeedConfig -> m ()-showStatus (f, feedID) = localConfig f . localError "imm.core" $ (io . noticeM "imm.core" =<< Feed.showStatus feedID)+check :: (Config -> Config) -> FeedConfig -> IO ()+check baseConfig (f, feedID) = withError "imm.core". withConfig (f . baseConfig) $ Feed.download feedID >>= Feed.check  -markAsRead :: (MonadBase IO m, ConfigReader m, DatabaseState m, MonadError ImmError m) => FeedConfig -> m ()-markAsRead (f, feedID) = localConfig f . localError "imm.core" $ Feed.markAsRead feedID+showStatus :: (Config -> Config) -> FeedConfig -> IO ()+showStatus baseConfig (f, feedID) = withConfig (f . baseConfig) $ (io . noticeM "imm.core" =<< Feed.showStatus feedID)  -markAsUnread :: (MonadBase IO m, ConfigReader m, DatabaseState m, MonadError ImmError m) => FeedConfig -> m ()-markAsUnread (f, feedID) = localConfig f . localError "imm.core" $ Feed.markAsUnread feedID+markAsRead :: (Config -> Config) -> FeedConfig -> IO ()+markAsRead baseConfig (f, feedID) = withError "imm.core" . withConfig (f . baseConfig) $ Feed.markAsRead feedID  -update :: (MonadBaseControl IO m, ConfigReader m, DatabaseState m, MonadError ImmError m, FeedParser m, MailFormatter m, HTTP.Decoder m) => FeedList -> m ()-update feeds = void . liftBaseWith $ \runInIO -> mapConcurrently (runInIO . updateFeed) feeds+markAsUnread :: (Config -> Config) -> FeedConfig -> IO ()+markAsUnread baseConfig (f, feedID) = withError "imm.core" . withConfig (f . baseConfig) $ Feed.markAsUnread feedID   -- | Write mails for each new item, and update the last check time in state file.-updateFeed :: (Applicative m, ConfigReader m, DatabaseState m, FeedParser m, MailFormatter m, MonadBase IO m, HTTP.Decoder m, MonadError ImmError m) => FeedConfig -> m ()-updateFeed (f, feedID) = localConfig f . localError "imm.core" $ do+update :: (Config -> Config) -> FeedConfig -> IO ()+update baseConfig (f, feedID) = withError "imm.core" . withConfig (f . baseConfig) $ do     -- io . noticeM "imm.core" $ "Updating: " ++ show feedID     (uri, feed) <- Feed.download feedID -    Maildir.create =<< readConfig maildir+    Maildir.init      io . debugM "imm.core" $ Feed.describe feed @@ -93,10 +94,7 @@     Feed.markAsRead uri  -updateItem :: (Applicative m, ConfigReader m, FeedParser m, MailFormatter m, MonadBase IO m, MonadError ImmError m) => (Item, Feed) -> m ()+updateItem :: (Applicative m, FeedParser m, MaildirWriter m, MailFormatter m, MonadBase IO m, MonadError ImmError m) => (Item, Feed) -> m () updateItem (item, feed) = do     timeZone <- io getCurrentTimeZone-    dir <- readConfig maildir--    io . debugM "imm.core" $ "Adding following item to maildir [" ++ dir ++ "]:\n" ++ Feed.describeItem item-    Maildir.add dir =<< Mail.build timeZone (item, feed)+    Maildir.write =<< Mail.build timeZone (item, feed)
Imm/Database.hs view
@@ -40,8 +40,7 @@ instance (Error e, DatabaseReader m) => DatabaseReader (ErrorT e m) where     getLastCheck = getLastCheck --class (MonadError ImmError m) => DatabaseWriter m where+class DatabaseWriter m where     -- | Write the last update time in the data file.     storeLastCheck :: FeedID -> UTCTime -> m ()     -- | Remove state file as if no update was ever done.
Imm/Dyre.hs view
@@ -34,12 +34,12 @@ showPaths :: MonadBase IO m => m String showPaths = io $ do     (a, b, c, d, e) <- getPaths $ parameters nullMain-    return . unlines $ [-        "Current binary:  " ++ a,-        "Custom binary:   " ++ b,-        "Config file:     " ++ c,-        "Cache directory: " ++ d,-        "Lib directory:   " ++ e, []]+    return . unlines $+        ("Current binary:  " ++ a):+        ("Custom binary:   " ++ b):+        ("Config file:     " ++ c):+        ("Cache directory: " ++ d):+        ("Lib directory:   " ++ e):[]  -- | Dynamic reconfiguration settings parameters :: (a -> IO ()) -> Params (Either String a)
Imm/Error.hs view
@@ -65,8 +65,8 @@     strMsg = OtherError  -withError :: (Error e, Show e, MonadBase IO m) => ErrorT e m () -> m ()-withError = runErrorT >=> either (io . print) return+withError :: (Error e, Show e, MonadBase IO m) => String -> ErrorT e m () -> m ()+withError category = runErrorT >=> either (io . errorM category . show) return  localError :: (MonadBase IO m, MonadError ImmError m) => String -> m () -> m () localError category f = f `catchError` (io . errorM category . show)
Imm/Feed.hs view
@@ -76,25 +76,27 @@     feed <- parse . TL.unpack =<< HTTP.get uri     return (uri, feed) --- |+-- | Count the list of unread items for given feed. check :: (FeedParser m, DatabaseReader m, MonadBase IO m, MonadError ImmError m) => ImmFeed -> m () check (feedID, feed) = do     lastCheck       <- getLastCheck feedID-    (errors, dates) <- partitionEithers <$> forM (feedItems feed) (\item -> (return . Right =<< getDate item) `catchError` (return . Left))+    (errors, dates) <- partitionEithers <$> mapM (runErrorT . getDate) (feedItems feed)+    let newItems     = filter (> lastCheck) dates+     unless (null errors) . io . errorM "imm.feed" . unlines $ map show errors-    let newItems = filter (> lastCheck) dates     io . noticeM "imm.feed" $ show (length newItems) ++ " new item(s) for <" ++ show feedID ++ ">" - -- | Simply set the last check time to now.-markAsRead :: (MonadBase IO m, MonadError ImmError m, DatabaseState m) => URI -> m ()-markAsRead uri = io getCurrentTime >>= storeLastCheck uri >> (io . debugM "imm.feed" $ "Feed " ++ show uri ++ " marked as read.")+markAsRead :: (MonadBase IO m, MonadError ImmError m, DatabaseWriter m) => URI -> m ()+markAsRead uri = do+    io getCurrentTime >>= storeLastCheck uri+    io . noticeM "imm.feed" $ "Feed <" ++ show uri ++ "> marked as read."  -- | Simply remove the state file. markAsUnread ::  (MonadBase IO m, MonadError ImmError m, DatabaseState m) => URI -> m () markAsUnread uri = do     forget uri-    io . noticeM "imm.feed" $ "Feed " ++ show uri ++ " marked as unread."+    io . noticeM "imm.feed" $ "Feed <" ++ show uri ++ "> marked as unread."  -- | Return a 'String' describing the last update for a given feed. showStatus :: (DatabaseReader m, MonadBase IO m) => URI -> m String@@ -108,8 +110,8 @@ getItemContent :: Item -> String getItemContent (AtomItem i) = length theContent < length theSummary ? theSummary ?? theContent   where-    theContent = maybe "" extractHtml $ Atom.entryContent i-    theSummary = maybe "No content" Atom.txtToString $ Atom.entrySummary i+    theContent = fromMaybe "" $ (extractHtml <$> Atom.entryContent i)+    theSummary = fromMaybe "No content" $ (Atom.txtToString <$> Atom.entrySummary i) getItemContent (RSSItem  i) = length theContent < length theDescription ? theDescription ?? theContent   where     theContent     = dropWhile isSpace . concatMap concat . map (map cdData . onlyText . elContent) . RSS.rssItemOther $ i
Imm/HTTP.hs view
@@ -30,34 +30,8 @@  instance (Error e, Decoder m) => Decoder (ErrorT e m) where     converter = lift converter---- | HTTP client--- data Client = Client {-    -- _manager       :: Manager,-    -- _state         :: BrowserState}---- makeLenses ''Client---- run :: (MonadBase IO m) => ReaderT (IORef Client) m a -> m a--- run f = do-    -- theInput <- newEmptyMVar-    -- threadID <- fork worker-    -- manager' <- io $ newManager def-    -- ref      <- io . newIORef $ Client manager' (defaultState manager')-    -- result   <- runReaderT f ref-    -- io $ closeManager manager'-    -- return result---- worker = do-    -- (theOrder, theResultMVar) <- takeMVar order-    -- putMVar theResultMVar result-    -- result <- work theOrder- -- }}} --- getRaw = do-    -- putMVar- -- | Perform an HTTP GET request and return the response body as raw 'ByteString' getRaw :: (MonadBase IO m, MonadError ImmError m) => URI -> m BL.ByteString getRaw uri = do@@ -67,7 +41,7 @@  -- | Same as 'getRaw' with additional decoding get :: (Decoder m, MonadBase IO m, MonadError ImmError m) => URI -> m TL.Text-get uri = decode =<< getRaw uri+get uri = getRaw uri >>= decode  -- | Monad-agnostic version of 'withManager' withManager' :: (MonadError ImmError m, MonadBase IO m) => (Manager -> ResourceT IO b) -> m b
Imm/Mail.hs view
@@ -43,15 +43,15 @@         _returnPath         = "<imm@noreply>"}  instance Show Mail where-    show mail = unlines [-        "Return-Path: " ++ view returnPath mail,-        maybe "" (("Date: " ++) . showRFC2822) . view date $ mail,-        "From: " ++ view from mail,-        "Subject: " ++ view subject mail,-        "Content-Type: " ++ view mime mail ++ "; charset=" ++ view charset mail,-        "Content-Disposition: " ++ view contentDisposition mail,-        "",-        view body mail]+    show mail = unlines $+        ("Return-Path: " ++ view returnPath mail):+        (maybe "" (("Date: " ++) . showRFC2822) $ view date mail):+        ("From: " ++ view from mail):+        ("Subject: " ++ view subject mail):+        ("Content-Type: " ++ view mime mail ++ "; charset=" ++ view charset mail):+        ("Content-Disposition: " ++ view contentDisposition mail):+        "":+        (view body mail):[]   type Format = (Item, Feed) -> String@@ -68,5 +68,7 @@     from'    <- formatFrom    <*> return (item, feed)     subject' <- formatSubject <*> return (item, feed)     body'    <- formatBody    <*> return (item, feed)-    date'    <- return . either (const Nothing) (Just . utcToZonedTime timeZone) =<< runErrorT (getDate item)+    date'    <- runErrorT' (utcToZonedTime timeZone <$> getDate item)     return . set date date' . set from from' . set subject subject' . set body body' $ def+  where+    runErrorT' = (return . either (const Nothing) Just) <=< runErrorT
Imm/Maildir.hs view
@@ -6,6 +6,7 @@ import Imm.Util  import Control.Monad.Error+import Control.Monad.Reader  import qualified Data.Text.Lazy.IO as T import qualified Data.Text.Lazy as TL@@ -15,23 +16,33 @@  import System.Directory import System.FilePath+import System.Log.Logger import System.Random -- }}} --- | Build a maildir with subdirectories cur, new and tmp.-create :: (MonadBase IO m, MonadError ImmError m) => FilePath -> m ()-create directory = do-    try $ createDirectoryIfMissing True directory-    try $ createDirectoryIfMissing True (directory </> "cur")-    try $ createDirectoryIfMissing True (directory </> "new")-    try $ createDirectoryIfMissing True (directory </> "tmp")+type Maildir = FilePath --- | Add a mail to the maildir-add :: (MonadBase IO m, MonadError ImmError m) => FilePath -> Mail -> m ()-add directory mail = do-    fileName <- io getUniqueName-    try $ T.writeFile (directory </> "new" </> fileName) (TL.pack $ show mail)+class MaildirWriter m where+    -- | Build a maildir with subdirectories cur, new and tmp.+    init :: m ()+    -- | Add a mail to the maildir+    write  :: Mail -> m () +instance (MonadBase IO m, MonadError ImmError m) => MaildirWriter (ReaderT Maildir m) where+    init = do+        theMaildir <- ask+        io . debugM "imm.maildir" $ "Creating maildir [" ++ theMaildir ++ "]"+        try . mapM_ (createDirectoryIfMissing True) $+            theMaildir:+            (theMaildir </> "cur"):+            (theMaildir </> "new"):+            (theMaildir </> "tmp"):[]+    write mail = do+        fileName   <- io getUniqueName+        theMaildir <- ask+        io . debugM "imm.maildir" $ "Writing new mail in maildir [" ++ theMaildir ++ "]"+        try $ T.writeFile (theMaildir </> "new" </> fileName) (TL.pack $ show mail)+ -- | Return an allegedly unique filename; useful to add new mail files in a maildir. getUniqueName :: MonadBase IO m => m String getUniqueName = io $ do@@ -39,4 +50,4 @@     hostname <- getHostName     rand     <- show <$> (getStdRandom $ randomR (1,100000) :: IO Int) -    return . concat $ [time, ".", rand, ".", hostname]+    return . concat $ time:".":rand:".":hostname:[]
Imm/Options.hs view
@@ -1,14 +1,13 @@ {-# LANGUAGE TemplateHaskell #-}--- | Commandline options tools. Designed to be imported as @qualified@. module Imm.Options (     CliOptions,     action,     dyreMode,     feedsList,     dataDirectory,-    OptionsReader(..),+    logLevel,     Action(..),-    run,+    get,     usage, ) where @@ -30,7 +29,6 @@ import System.Environment -- import System.Environment.XDG.BaseDir import System.Log as Log-import System.Log.Logger -- }}}  -- {{{ Types@@ -48,8 +46,7 @@     _dyreMode       :: Dyre.Mode,     _dataDirectory  :: Maybe FilePath,     _feedsList      :: [URI],-    _logLevel       :: Log.Priority,-    _dyreDebug      :: Bool}+    _logLevel       :: Log.Priority}     deriving(Eq)  makeLenses ''CliOptions@@ -60,8 +57,7 @@         return . ("RECONFIGURATION_MODE=" ++) . show $ view dyreMode opts,         null (view feedsList opts) ? Nothing ?? Just ("FEED_URI=[" ++ (unwords . map show $ view feedsList opts) ++ "]"),         return . ("DATA_DIR=" ++) =<< view dataDirectory opts,-        return . ("LOG_LEVEL=" ++) . show $ view logLevel opts,-        view dyreDebug opts ? Just "DYRE_DEBUG" ?? Nothing]+        return $ "LOG_LEVEL=" ++ show (view logLevel opts)]  instance Default CliOptions where     def = CliOptions {@@ -69,26 +65,7 @@         _dyreMode      = def,         _logLevel      = Log.INFO,         _dataDirectory = Nothing,-        _feedsList     = [],-        _dyreDebug     = False}---- | 'MonadReader' for 'CliOptions'-class OptionsReader m where-    readOptions :: Simple Lens CliOptions a -> m a--instance (Monad m) => OptionsReader (ReaderT CliOptions m) where-    readOptions l = return . view l =<< ask--instance OptionsReader ((->) CliOptions) where-    readOptions l = view l---- | Parse commandline options, set the corresponding log level.-run :: (MonadBase IO m) => ReaderT CliOptions m a -> m a-run f = do-    opts <- get-    io . updateGlobalLogger rootLoggerName . setLevel $ view logLevel opts-    io . debugM "imm.options" $ "Commandline options: " ++ show opts-    runReaderT f opts+        _feedsList     = []} -- }}}  description :: [OptDescr (CliOptions -> CliOptions)]@@ -116,15 +93,21 @@  -- | Usage text (printed when using 'Help' action) usage :: String-usage = usageInfo "Usage: imm [OPTIONS] [URI]\n\nConvert items from RSS/Atom feeds to maildir entries. If one or more URI(s) are given, they will be processed instead of the feeds list from configuration\n" description+usage = flip usageInfo description . unlines $+    "Usage: imm [OPTIONS] [URI]":+    "":+    "Convert items from RSS/Atom feeds to maildir entries.":+    "If one or more URI(s) are given, they will be processed instead of the feeds list from configuration.":[]  -- | Get and parse commandline options get :: (MonadBase IO m) => m CliOptions get = io $ do-    options <- getOpt' Permute description <$> getArgs-    case options of+    parsedArgs <- getOpt' Permute description <$> getArgs+    case parsedArgs of         (opts, input, _, []) -> do-            let (errors, valids) = partitionEithers $ map (\uri -> maybe (Left $ "Invalid URI given in commandline: " ++ uri) Right $ N.parseURI uri) input+            let (errors, valids) = partitionEithers $ map parseURI' input             unless (null errors) $ io . putStrLn $ unlines errors             return $ set feedsList valids  (foldl (flip id) def opts)         (_, _, _, _)         -> return def+  where+    parseURI' uri = maybe (Left $ "Invalid URI given in commandline: " ++ uri) Right $ N.parseURI uri
Imm/Util.hs view
@@ -13,7 +13,7 @@ import Data.Default as X import Data.Either as X import Data.Functor as X-import Data.List as X hiding(foldl, sum)+import Data.List as X hiding(foldl, init, sum) import Data.Maybe as X  import System.FilePath
imm.cabal view
@@ -1,5 +1,5 @@ Name:                imm-Version:             0.6.0.0+Version:             0.6.0.1 Synopsis:            Retrieve RSS/Atom feeds and write one mail per new item in a maildir. Description:         Cf README --Homepage:@@ -31,11 +31,11 @@         Imm.HTTP,         Imm.Mail,         Imm.Maildir,-        Imm.OPML,-        Imm.Options,         Imm.Util     Other-modules:         Imm.Dyre,+        Imm.OPML,+        Imm.Options,         Paths_imm     Build-depends:         async,@@ -63,6 +63,7 @@         text,         text-icu,         transformers-base,+        transformers,         time,         timerep >= 1.0.3,         tls,