packages feed

imm 0.2.0.0 → 0.3.0.0

raw patch · 11 files changed

+387/−235 lines, 11 filesdep +conddep −errorsdep −transformersPVP ok

version bump matches the API change (PVP)

Dependencies added: cond

Dependencies removed: errors, transformers

API changes (from Hackage documentation)

- Imm.Core: downloadFeed :: URI -> EitherT ImmError IO ImmFeed
- Imm.Core: downloadRaw :: URI -> EitherT ConnError IO Text
- Imm.Core: processFeed :: Settings -> FeedSettings -> ImmFeed -> EitherT ImmError IO ()
- Imm.Core: processFeedGroup :: Settings -> FeedGroup -> EitherT ImmError IO ()
- Imm.Core: processItem :: Settings -> FeedSettings -> Item -> Script ()
- Imm.Core: realMain :: [FeedGroup] -> (Settings, CliOptions) -> IO ()
- Imm.Mail: (><) :: a -> (a -> b) -> b
- Imm.Mail: buildMailBody :: Item -> Text
- Imm.Mail: defaultMail :: Mail
- Imm.Mail: extractHtml :: EntryContent -> String
- Imm.Mail: getItemContent :: Item -> Text
- Imm.Mail: getItemLinkNM :: Item -> String
- Imm.Mail: itemToMail :: TimeZone -> Item -> Mail
- Imm.Mail: paragraphy :: String -> String
- Imm.Types: mContent :: Mail -> Text
- Imm.Types: mError :: Settings -> Maybe String
- Imm.Types: mMailDirectory :: FeedSettings -> PortableFilePath
- Imm.Types: mParameter :: CliOptions -> Maybe String
- Imm.Util: decodeIfNeeded :: String -> String
- Imm.Util: escapeFileName :: Char -> String
+ Imm.Boot: realMain :: Either String (Settings, CliOptions) -> IO ()
+ Imm.Config: contentBuilder :: Item -> Text
+ Imm.Feed: extractHtml :: EntryContent -> String
+ Imm.Feed: getItemContent :: Item -> Text
+ Imm.Feed: getItemDate :: MonadError ImmError m => Item -> m UTCTime
+ Imm.Feed: getItemLinkNM :: Item -> String
+ Imm.Feed: getStateFile :: URI -> FilePath
+ Imm.Feed: paragraphy :: String -> String
+ Imm.Feed: toFileName :: Char -> String
+ Imm.Mail: bare :: Mail
+ Imm.Mail: build :: MonadReader Settings m => TimeZone -> (Item, Feed) -> m Mail
+ Imm.Mail: buildBody :: MonadReader Settings m => (Item, Feed) -> m Text
+ Imm.Mail: buildFrom :: MonadReader Settings m => (Item, Feed) -> m String
+ Imm.Mail: buildSubject :: MonadReader Settings m => (Item, Feed) -> m Text
+ Imm.Main: checkFeedGroup :: (MonadReader Settings m, MonadIO m) => FeedGroup -> m ()
+ Imm.Main: checkStateDirectory :: (MonadReader Settings m, MonadIO m, MonadError ImmError m) => m ()
+ Imm.Main: downloadFeed :: (MonadIO m, MonadError ImmError m) => URI -> m ImmFeed
+ Imm.Main: downloadRaw :: (MonadIO m, MonadError ImmError m) => URI -> m ByteString
+ Imm.Main: main :: (MonadReader Settings m, MonadIO m, MonadError ImmError m) => m ()
+ Imm.Main: printFeedGroupStatus :: (MonadReader Settings m, MonadIO m) => FeedGroup -> m ()
+ Imm.Main: printFeedStatus :: (MonadReader Settings m, MonadIO m) => String -> m ()
+ Imm.Main: processFeed :: (MonadReader Settings m, MonadIO m, MonadError ImmError m) => FeedSettings -> ImmFeed -> m ()
+ Imm.Main: processFeedGroup :: (MonadIO m, MonadReader Settings m, MonadError ImmError m) => FeedGroup -> m ()
+ Imm.Main: processItem :: (MonadReader Settings m, MonadIO m, MonadError ImmError m) => FeedSettings -> (Item, Feed) -> m ()
+ Imm.Types: HTTPError :: String -> ImmError
+ Imm.Types: IOE :: IOError -> ImmError
+ Imm.Types: UnicodeError :: UnicodeException -> ImmError
+ Imm.Types: instance Error ImmError
+ Imm.Types: mBody :: Mail -> Text
+ Imm.Types: mBodyBuilder :: Settings -> (Item, Feed) -> Text
+ Imm.Types: mCheck :: CliOptions -> Bool
+ Imm.Types: mDenyReconf :: CliOptions -> Bool
+ Imm.Types: mFeedGroups :: Settings -> [FeedGroup]
+ Imm.Types: mFromBuilder :: Settings -> (Item, Feed) -> String
+ Imm.Types: mList :: CliOptions -> Bool
+ Imm.Types: mMaildir :: FeedSettings -> PortableFilePath
+ Imm.Types: mSubjectBuilder :: Settings -> (Item, Feed) -> Text
+ Imm.Util: decodeUtf8 :: MonadError ImmError m => ByteString -> m Text
+ Imm.Util: parseFeedString :: MonadError ImmError m => String -> m Feed
+ Imm.Util: parseTime :: MonadError ImmError m => String -> m UTCTime
+ Imm.Util: parseURI :: MonadError ImmError m => String -> m URI
+ Imm.Util: request :: Request ByteString -> BrowserAction (HandleStream ByteString) (URI, Response ByteString)
+ Imm.Util: try :: (MonadIO m, MonadError ImmError m) => IO a -> m a
- Imm.Boot: dyreParameters :: [FeedGroup] -> Params (Settings, CliOptions)
+ Imm.Boot: dyreParameters :: Params (Either String (Settings, CliOptions))
- Imm.Boot: imm :: [FeedGroup] -> Settings -> IO ()
+ Imm.Boot: imm :: Settings -> IO ()
- Imm.Boot: showError :: (Settings, a) -> String -> (Settings, a)
+ Imm.Boot: showError :: Either String a -> String -> Either String a
- Imm.Feed: getLastCheck :: Settings -> ImmFeed -> IO UTCTime
+ Imm.Feed: getLastCheck :: (MonadReader Settings m, MonadIO m) => URI -> m UTCTime
- Imm.Feed: storeLastCheck :: Settings -> ImmFeed -> UTCTime -> EitherT ImmError IO ()
+ Imm.Feed: storeLastCheck :: (MonadReader Settings m, MonadIO m, MonadError ImmError m) => URI -> UTCTime -> m ()
- Imm.Maildir: add :: PortableFilePath -> Mail -> Script ()
+ Imm.Maildir: add :: (MonadIO m, MonadError ImmError m) => PortableFilePath -> Mail -> m ()
- Imm.Maildir: getUniqueName :: IO String
+ Imm.Maildir: getUniqueName :: MonadIO m => m String
- Imm.Maildir: init :: PortableFilePath -> EitherT String IO ()
+ Imm.Maildir: init :: (MonadIO m, MonadError ImmError m) => PortableFilePath -> m ()
- Imm.Types: CliOptions :: Maybe String -> Maybe String -> CliOptions
+ Imm.Types: CliOptions :: Bool -> Bool -> Bool -> Maybe String -> CliOptions
- Imm.Types: Settings :: PortableFilePath -> Maybe String -> Settings
+ Imm.Types: Settings :: PortableFilePath -> [FeedGroup] -> ((Item, Feed) -> String) -> ((Item, Feed) -> Text) -> ((Item, Feed) -> Text) -> Settings
- Imm.Util: logNormal, logVerbose :: String -> IO ()
+ Imm.Util: logNormal, logVerbose :: MonadIO m => String -> m ()
- Imm.Util: resolve :: (RefDirs -> a) -> IO a
+ Imm.Util: resolve :: MonadIO m => (RefDirs -> a) -> m a

Files

Imm/Boot.hs view
@@ -1,13 +1,14 @@ module Imm.Boot where  -- {{{ Imports-import Imm.Core+import Imm.Main import Imm.Types  import qualified Config.Dyre as D import Config.Dyre.Paths -import Data.Foldable+import Control.Monad.Error+import Control.Monad.Reader  import System.Console.CmdArgs import System.IO@@ -17,46 +18,56 @@ -- | Available commandline options cliOptions :: CliOptions cliOptions = CliOptions {-    mParameter     = def &= help "option description" &= explicit &= name "p" &= name "parameter" &= typ "type of the argument",-    mMasterBinary  = def &= name "dyre-master-binary" &= explicit+    mCheck         = def &= explicit &= name "c" &= name "check" &= help "Check availability and validity of all feed sources currently configured.",+    mList          = def &= explicit &= name "l" &= name "list"  &= help "List all feed sources currently configured.",+    mDenyReconf    = def &= explicit &= name "deny-reconf"       &= help "Deny recompilation even if the configuration file has changed.",+    mMasterBinary  = def &= explicit &= name "dyre-master-binary" }  getOptions :: IO CliOptions getOptions = cmdArgs $ cliOptions-    &= verbosityArgs [explicit, name "Verbose", name "v"] []+    &= verbosityArgs [explicit, name "verbose", name "v"] []     &= versionArg [ignore]-    &= help "Fetch and send items from RSS/Atom feeds to a custom mail address."+    &= help "Convert items from RSS/Atom feeds to maildir entries."     &= helpArg [explicit, name "help", name "h"]     &= program "imm" -- }}}  -- {{{ Dynamic reconfiguration printDyrePaths :: IO ()-printDyrePaths = getPaths (dyreParameters []) >>= \(a, b, c, d, e) -> (putStrLn . unlines) [-    "Current binary:  " ++ a,-    "Custom binary:   " ++ b,-    "Config file:     " ++ c,-    "Cache directory: " ++ d,-    "Lib directory:   " ++ e, []]+printDyrePaths = do+    (a, b, c, d, e) <- getPaths dyreParameters+    putStrLn . unlines $ [+        "Current binary:  " ++ a,+        "Custom binary:   " ++ b,+        "Config file:     " ++ c,+        "Cache directory: " ++ d,+        "Lib directory:   " ++ e, []] -dyreParameters :: [FeedGroup] -> D.Params (Settings, CliOptions)-dyreParameters feedGroups = D.defaultParams {+dyreParameters :: D.Params (Either String (Settings, CliOptions))+dyreParameters = D.defaultParams {   D.projectName  = "imm",   D.showError    = showError,-  D.realMain     = realMain feedGroups,+  D.realMain     = realMain,   D.ghcOpts      = ["-threaded"],   D.statusOut    = hPutStrLn stderr } -showError :: (Settings, a) -> String -> (Settings, a)-showError (settings, x) message = (settings { mError = Just message }, x)+showError :: Either String a -> String -> Either String a+showError _ = Left -- }}} --- | -imm :: [FeedGroup] -> Settings -> IO ()-imm feedGroups settings = do-    forM_ (mError settings) putStrLn-    whenLoud printDyrePaths-    +-- | Main function to call.+imm :: Settings -> IO ()+imm settings = do     options <- getOptions-    D.wrapMain (dyreParameters feedGroups) (settings, options)+    D.wrapMain dyreParameters (Right (settings, options))++-- Main dispatcher, depending on commandline options+realMain :: Either String (Settings, CliOptions) -> IO ()+realMain (Left e) = putStrLn e+realMain (Right (settings, options))+  | mList  options = mapM_ (flip runReaderT settings . printFeedGroupStatus) $ mFeedGroups settings+  | mCheck options = mapM_ (flip runReaderT settings . checkFeedGroup) $ mFeedGroups settings+  | otherwise      = whenLoud printDyrePaths >> runErrorT (runReaderT main settings) >>= either print return+-- }}}
Imm/Config.hs view
@@ -1,12 +1,27 @@ module Imm.Config where +-- {{{ Imports+import Imm.Feed import Imm.Types +import qualified Data.Text.Lazy as T+ import System.FilePath +import Text.Feed.Query+import Text.Feed.Types+-- }}}+ -- | Default configuration. defaultSettings :: Settings defaultSettings = Settings {     mStateDirectory = (</> "state") . mConfiguration,-    mError          = Nothing+    mFeedGroups     = [],+    mFromBuilder    = \(item, feed) -> maybe (getFeedTitle feed) id $ getItemAuthor item,+    mSubjectBuilder = \(item, _feed) -> T.pack . maybe "Untitled" id $ getItemTitle item,+    mBodyBuilder    = \(item, _feed) -> contentBuilder item }++contentBuilder :: Item -> T.Text+contentBuilder item = +    T.unlines $ map (flip ($) item) [T.pack . getItemLinkNM, getItemContent]
− Imm/Core.hs
@@ -1,99 +0,0 @@-module Imm.Core where---- {{{ Imports-import Imm.Feed-import Imm.Mail-import qualified Imm.Maildir as Maildir-import Imm.Types-import Imm.Util--import Control.Error-import Control.Monad hiding(forM_, mapM_)--import qualified Data.ByteString.Lazy as B---import qualified Data.ByteString.UTF8 as B-import qualified Data.Text.Lazy as T-import Data.Text.Lazy.Encoding-import Data.Foldable---import Data.Functor-import Data.Time--import Network.HTTP hiding(Response)-import Network.Stream-import Network.URI--import Prelude hiding(mapM_)--import Text.Feed.Import-import Text.Feed.Query-import Text.Feed.Types--- }}}---- Entry point-realMain :: [FeedGroup] -> (Settings, CliOptions) -> IO ()-realMain feedGroups (settings, _options) = do-    result <- forM feedGroups $ runEitherT . processFeedGroup settings-    forM_ (lefts result) print--   -processFeedGroup :: Settings -> FeedGroup -> EitherT ImmError IO ()-processFeedGroup globalSettings _feedGroup@(feedSettings, feedURIs) = do-    fmapLT OtherError $ Maildir.init $ mMailDirectory feedSettings-    forM_ feedURIs $ \uri -> do-        uri' <- EitherT . return $ parseURI' uri-        feed <- downloadFeed uri'-        processFeed globalSettings feedSettings feed-  where-    parseURI' uri = note (ParseUriError uri) . parseURI $ uri---processFeed :: Settings -> FeedSettings -> ImmFeed -> EitherT ImmError IO ()-processFeed globalSettings feedSettings (uri, feed) = do-    io . logVerbose $ unlines [-        "Processing feed: " ++ show uri,-        ("Title:  " ++) . getFeedTitle $ feed,-        ("Author: " ++) . maybe "No author" id . getFeedAuthor $ feed,-        ("Home:   " ++) . maybe "No home"   id . getFeedHome $ feed]-    -    lastCheck <- io $ getLastCheck globalSettings (uri, feed)-    forM_ (feedItems feed) $ \item -> do-      date <- EitherT . return $ getItemDate' item-      when (date > lastCheck) $ fmapLT OtherError $ processItem globalSettings feedSettings item-      return ()-    -    currentTime <- io getCurrentTime-    storeLastCheck globalSettings (uri, feed) currentTime-  where-    getItemDate' x = note (ParseItemDateError x) . (parseDate <=< getItemDate) $ x-      -      -processItem :: Settings -> FeedSettings -> Item -> Script ()-processItem _globalSettings feedSettings item = do-    io . logVerbose $ unlines ["",-            "   Item author: " ++ (maybe "" id $ getItemAuthor item),-            "   Item title:  " ++ (maybe "" id $ getItemTitle item),-            "   Item URI:    " ++ (maybe "" id $ getItemLink  item),-            --"   Item Content:    " ++ (Imm.Mail.getItemContent  item),-            "   Item date:   " ++ (maybe "" id $ time)]-    -    timeZone <- io getCurrentTimeZone-    void $ Maildir.add dir . itemToMail timeZone $ item -  where-    time = getItemDate item-    dir  = mMailDirectory feedSettings---downloadRaw :: URI -> EitherT ConnError IO T.Text-downloadRaw uri = do-    io . logVerbose $ "Downloading " ++ show uri-    response <- EitherT $ simpleHTTP (mkRequest GET uri :: Request B.ByteString)-    return . decodeUtf8 . rspBody $ response---downloadFeed :: URI -> EitherT ImmError IO ImmFeed-downloadFeed uri = do-    rawData <- fmapLT CE $ downloadRaw uri-    feed    <- EitherT . return $ parseFeedString' (T.unpack rawData)-    return (uri, feed)-  where-    parseFeedString' x = note (ParseFeedError $ show x) $ parseFeedString x
+ Imm/Executable.hs view
@@ -0,0 +1,8 @@+--module Executable where++import Imm.Boot+import Imm.Config+--import Imm.Types++main :: IO ()+main = imm defaultSettings
Imm/Feed.hs view
@@ -1,46 +1,89 @@+{-# LANGUAGE FlexibleContexts #-} module Imm.Feed where  -- {{{ Imports import Imm.Types import Imm.Util -import Control.Error---import Control.Exception+import Control.Monad.Error+import Control.Monad.Reader -import Data.Time+--import Data.Functor+import qualified Data.Text.Lazy as T+import Data.Time hiding(parseTime) import Data.Time.Clock.POSIX +import Network.URI+ import System.Directory import System.FilePath import System.IO import System.Locale++import Text.Atom.Feed hiding(URI)+import Text.Feed.Query as F+import Text.Feed.Types+import Text.XML.Light.Proc -- }}} +-- {{{ Util+getStateFile :: URI -> FilePath+getStateFile feedUri@URI{ uriAuthority = Just auth } = toFileName =<< ((++ (uriQuery feedUri)) . (++ (uriPath feedUri)) . uriRegName $ auth)+getStateFile feedUri = show feedUri >>= toFileName -getLastCheck :: Settings -> ImmFeed -> IO UTCTime-getLastCheck settings (uri, _feed) = do-    directory <- resolve $ mStateDirectory settings-    -    result <- runEitherT $ do-        content <- fmapLT OtherError . tryIO $ readFile (directory </> fileName)-        EitherT . return $ parseTime' content+toFileName :: Char -> String+toFileName '/' = "."+toFileName '?' = "."+toFileName x = [x]+-- }}} -    either -        (\e -> print e >> return timeZero)-        return-        result++getLastCheck :: (MonadReader Settings m, MonadIO m) => URI -> m UTCTime+getLastCheck feedUri = do+    directory <- asks mStateDirectory >>= resolve+    result    <- runErrorT $ do+        content <- try $ readFile (directory </> fileName)+        parseTime content+        +    either (const $ return timeZero) return result   where-    fileName = show uri >>= escapeFileName-    timeZero = posixSecondsToUTCTime $ 0 -    parseTime' string = note (ParseTimeError string) $ parseTime defaultTimeLocale "%c" string+    fileName = getStateFile feedUri+    timeZero = posixSecondsToUTCTime 0   -storeLastCheck :: Settings -> ImmFeed -> UTCTime -> EitherT ImmError IO ()-storeLastCheck settings (uri, _) date = do-    directory <- io . resolve $ mStateDirectory settings-    let fileName = show uri >>= escapeFileName+storeLastCheck :: (MonadReader Settings m, MonadIO m, MonadError ImmError m) => URI -> UTCTime -> m ()+storeLastCheck feedUri date = do+    directory <- asks mStateDirectory >>= resolve     -    (file, stream) <- fmapLT OtherError . tryIO $ openTempFile directory fileName+    (file, stream) <- try $ openTempFile directory fileName     io $ hPutStrLn stream (formatTime defaultTimeLocale "%c" date)     io $ hClose stream-    fmapLT OtherError . tryIO $ renameFile file (directory </> fileName)+    try $ renameFile file (directory </> fileName)+  where+    fileName = getStateFile feedUri+    +-- {{{ Item utilities+getItemLinkNM :: Item -> String +getItemLinkNM item = maybe "No link found" paragraphy  $ getItemLink item+++getItemContent :: Item -> T.Text+getItemContent (AtomItem e) = T.pack . maybe "No content" extractHtml . entryContent $ e+getItemContent item = T.pack . maybe "Empty" id . getItemDescription $ item++getItemDate :: MonadError ImmError m => Item -> m UTCTime+getItemDate x = maybe (throwError $ ParseItemDateError x) return $ parseDate =<< F.getItemDate x+-- }}}+++extractHtml :: EntryContent -> String+extractHtml (HTMLContent c) = c+extractHtml (XHTMLContent c) = strContent c+extractHtml (TextContent t) = t+extractHtml (MixedContent a b)= show a ++ show b+extractHtml (ExternalContent mediaType uri) = show mediaType ++ show uri+++paragraphy :: String -> String+paragraphy s = "<p>"++s++"</p>"+
Imm/Mail.hs view
@@ -1,26 +1,27 @@+{-# LANGUAGE FlexibleContexts #-} module Imm.Mail where  -- {{{ Imports+--import Imm.Feed import Imm.Types import Imm.Util  import Control.Monad+import Control.Monad.Reader  import qualified Data.Text.Lazy as T import Data.Time import Data.Time.RFC2822 -import Text.Feed.Query+import Text.Feed.Query as F import Text.Feed.Types-import Text.Atom.Feed   as Atom-import Text.XML.Light.Proc -- }}}  -defaultMail :: Mail-defaultMail = Mail {+bare :: Mail+bare = Mail {     mCharset            = "utf-8",-    mContent            = T.pack "",+    mBody               = T.pack "",     mContentDisposition = "inline",     mDate               = Nothing,     mFrom               = "imm",@@ -38,39 +39,32 @@     T.pack $ "Content-Type: " ++ mMIME mail ++ "; charset=" ++ mCharset mail,     T.pack $ "Content-Disposition: " ++ mContentDisposition mail,     T.pack "",-    mContent mail]+    mBody mail]   -itemToMail :: TimeZone -> Item -> Mail-itemToMail timeZone item = defaultMail {-    mDate       = maybe Nothing (Just . utcToZonedTime timeZone) . parseDate <=< getItemDate $ item,-    mFrom       = maybe "Anonymous" id $ getItemAuthor item,-    mSubject    = T.pack $ maybe "Untitled" id $ getItemTitle item,-    mContent    = buildMailBody item}+build :: MonadReader Settings m => TimeZone -> (Item, Feed) -> m Mail+build timeZone (item, feed) = do+    from    <- buildFrom    (item, feed)+    subject <- buildSubject (item, feed)+    body    <- buildBody    (item, feed)+    return bare {mDate = date, mFrom = from, mSubject = subject, mBody = body}+  where+    date = maybe Nothing (Just . utcToZonedTime timeZone) . parseDate <=< F.getItemDate $ item -buildMailBody :: Item -> T.Text-buildMailBody item = -    T.unlines $ map ((><) item) [T.pack . getItemLinkNM, getItemContent] -getItemLinkNM :: Item -> String -getItemLinkNM item = maybe "No link found" paragraphy  $ getItemLink item---- ce "magic operator" semble pas défini dans les libs haskell -> WTF ?-(><) :: a -> (a -> b) -> b-(><) a b = b a--paragraphy :: String -> String-paragraphy s = "<p>"++s++"</p>"+buildFrom :: MonadReader Settings m => (Item, Feed) -> m String+buildFrom (item, feed) = do+    builder <- asks mFromBuilder+    return $ builder (item, feed)  -getItemContent :: Item -> T.Text-getItemContent (AtomItem e) = T.pack . maybe "No content" extractHtml . Atom.entryContent $ e-getItemContent item = T.pack . maybe "Empty" id . getItemDescription $ item+buildSubject :: MonadReader Settings m => (Item, Feed) -> m T.Text+buildSubject (item, feed) = do+    builder <- asks mSubjectBuilder+    return $ builder (item, feed)  -extractHtml :: EntryContent -> String-extractHtml (HTMLContent c) = c-extractHtml (XHTMLContent c) = strContent c-extractHtml (TextContent t) = t-extractHtml (MixedContent a b)= show a ++ show b-extractHtml (ExternalContent mediaType uri) = show mediaType ++ show uri+buildBody :: MonadReader Settings m => (Item, Feed) -> m T.Text+buildBody (item, feed) = do+    builder <- asks mBodyBuilder+    return $ builder (item, feed)
Imm/Maildir.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE FlexibleContexts #-} module Imm.Maildir where  -- {{{ Imports@@ -5,8 +6,8 @@ import Imm.Types import Imm.Util -import Control.Error---import Control.Exception+--import Control.Monad.IO.Class+import Control.Monad.Error  import Data.Functor import qualified Data.Text.Lazy.IO as T@@ -16,30 +17,28 @@  import System.Directory import System.FilePath---import System.IO.Error (ioeGetErrorString) import System.Random -- }}} -init :: PortableFilePath -> EitherT String IO ()+init :: (MonadIO m, MonadError ImmError m) => PortableFilePath -> m () init directory = do     dir <- io $ resolve directory-    tryIO $ createDirectoryIfMissing True dir-    tryIO $ createDirectoryIfMissing True (dir </> "cur")-    tryIO $ createDirectoryIfMissing True (dir </> "new")-    tryIO $ createDirectoryIfMissing True (dir </> "tmp")-    -    io . logVerbose $ "Maildir correctly created at: " ++ dir+    try $ createDirectoryIfMissing True dir+    try $ createDirectoryIfMissing True (dir </> "cur")+    try $ createDirectoryIfMissing True (dir </> "new")+    try $ createDirectoryIfMissing True (dir </> "tmp")+    logVerbose $ "Maildir correctly created at: " ++ dir  -add :: PortableFilePath -> Mail -> Script ()+add :: (MonadIO m, MonadError ImmError m) => PortableFilePath -> Mail -> m () add directory mail = do-    dir      <- io $ resolve directory+    dir      <- resolve directory     fileName <- io getUniqueName-    tryIO $ T.writeFile (dir </> "new" </> fileName) (toText mail)+    try $ T.writeFile (dir </> "new" </> fileName) (toText mail)  -getUniqueName :: IO String -getUniqueName = do+getUniqueName :: MonadIO m => m String +getUniqueName = io $ do     time     <- show <$> getPOSIXTime     hostname <- getHostName     rand     <- show <$> (getStdRandom $ randomR (1,100000) :: IO Int)
Imm/Main.hs view
@@ -1,8 +1,123 @@-module Main where+{-# LANGUAGE FlexibleContexts #-}+module Imm.Main where -import Imm.Boot-import Imm.Config---import Imm.Types+-- {{{ Imports+import Imm.Feed+import qualified Imm.Mail as Mail+import qualified Imm.Maildir as Maildir+import Imm.Types+import Imm.Util -main :: IO ()-main = imm [] defaultSettings+import Control.Conditional hiding(when)+import Control.Monad hiding(forM_, mapM_)+import Control.Monad.Error hiding(forM_, mapM_)+import Control.Monad.Reader hiding(forM_, mapM_)++import qualified Data.ByteString.Lazy as B+import qualified Data.Text.Lazy as T+import Data.Either+import Data.Foldable+--import Data.Functor+import Data.Time+import Data.Time.Clock.POSIX++import Network.Browser hiding(request)+import Network.HTTP+import Network.URI hiding(parseURI)++import Prelude hiding(mapM_)++import System.Directory++--import Text.Feed.Import+import Text.Feed.Query hiding(getItemDate)+import Text.Feed.Types+-- }}}++-- {{{ Quick actions performed for specific commandline options+printFeedGroupStatus :: (MonadReader Settings m, MonadIO m) => FeedGroup -> m ()+printFeedGroupStatus (settings, feeds) = do+    maildir <- resolve $ mMaildir settings+    io . putStrLn $ " => Feed group " ++ maildir+    mapM_ printFeedStatus feeds++printFeedStatus :: (MonadReader Settings m, MonadIO m) => String -> m ()+printFeedStatus feedUri = do+    prefix <- case parseURI feedUri of+        Right uri -> do+          lastCheck <- getLastCheck uri+          return $ (lastCheck == posixSecondsToUTCTime 0) ? "[NEW] " ?? ("[Last update: "++ show lastCheck ++ "]")+        _ -> return "[Not an URI]"+    io . putStrLn $ prefix ++ " " ++ feedUri++   +checkFeedGroup :: (MonadReader Settings m, MonadIO m) => FeedGroup -> m ()+checkFeedGroup (settings, feeds) = return ()+-- }}}++-- | Internal entry point for imm, after boot process+main :: (MonadReader Settings m, MonadIO m, MonadError ImmError m) => m ()+main = do+    checkStateDirectory    +    asks mFeedGroups >>= mapM_ (\x -> processFeedGroup x `catchError` (io . print))++checkStateDirectory :: (MonadReader Settings m, MonadIO m, MonadError ImmError m) => m ()+checkStateDirectory = asks mStateDirectory >>= resolve >>= try . io . createDirectoryIfMissing True+++processFeedGroup :: (MonadIO m, MonadReader Settings m, MonadError ImmError m) => FeedGroup -> m ()+processFeedGroup _feedGroup@(config, feedURIs) = do+    Maildir.init $ mMaildir config+    forM_ feedURIs $ \uri ->+        (parseURI uri >>= downloadFeed >>= processFeed config)+        `catchError` (io . print)+++processFeed :: (MonadReader Settings m, MonadIO m, MonadError ImmError m) => FeedSettings -> ImmFeed -> m ()+processFeed feedSettings (uri, feed) = do+    logVerbose $ unlines [+        "Processing feed: " ++ show uri,+        "Title:  " ++ (getFeedTitle feed),+        "Author: " ++ (maybe "No author" id $ getFeedAuthor feed),+        "Home:   " ++ (maybe "No home"   id $ getFeedHome feed)]+    +    lastCheck <- getLastCheck uri+    forM_ (feedItems feed) $ \item -> +      do+        date <- getItemDate item+        when (date > lastCheck) $ processItem feedSettings (item, feed)+      `catchError` (io . print)++    storeLastCheck uri =<< io getCurrentTime+    +      +processItem :: (MonadReader Settings m, MonadIO m, MonadError ImmError m) => FeedSettings -> (Item, Feed) -> m ()+processItem feedSettings (item, feed) = do+    date <- getItemDate item+    logVerbose $ unlines [+            "   Item author: " ++ (maybe "" id $ getItemAuthor item),+            "   Item title:  " ++ (maybe "" id $ getItemTitle item),+            "   Item URI:    " ++ (maybe "" id $ getItemLink  item),+            -- "   Item Body:   " ++ (Imm.Mail.getItemContent  item),+            "   Item date:   " ++ show date]+    +    timeZone <- io getCurrentTimeZone+    Maildir.add dir =<< Mail.build timeZone (item, feed)+  where+    dir = mMaildir feedSettings+++downloadRaw :: (MonadIO m, MonadError ImmError m) => URI -> m B.ByteString+downloadRaw uri = do+    logVerbose $ "Downloading " ++ show uri+    (_, r) <- io . browse $ do+        setAllowRedirects True+        request (mkRequest GET uri :: Request B.ByteString)+    when (rspCode r == (4,0,4)) $ throwError (HTTPError $ rspReason r)+    return . rspBody $ r+++downloadFeed :: (MonadIO m, MonadError ImmError m) => URI -> m ImmFeed+downloadFeed uri = do+    feed <- parseFeedString . T.unpack =<< decodeUtf8 =<< downloadRaw uri+    return (uri, feed)
Imm/Types.hs view
@@ -1,7 +1,11 @@ {-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-} module Imm.Types where  -- {{{ Imports+import Control.Monad.Error++import Data.Text.Encoding.Error import qualified Data.Text.Lazy as T import Data.Time @@ -9,38 +13,72 @@ import Network.Stream  import System.Console.CmdArgs+import System.IO.Error +import Text.Feed.Query import Text.Feed.Types -- }}}  -- {{{ Error handling-data ImmError = OtherError String | ParseUriError String | ParseTimeError String | ParseItemDateError Item | ParseFeedError String | CE ConnError+-- | Errors that can be returned by an Imm process+data ImmError = +    OtherError         String+  | HTTPError          String+  | UnicodeError       UnicodeException+  | ParseUriError      String+  | ParseTimeError     String+  | ParseItemDateError Item+  | ParseFeedError     String+  | CE                 ConnError+  | IOE                IOError  instance Show ImmError where-    show (OtherError e)            = show e+    show (OtherError e)            = e+    show (HTTPError e)             = e+    show (UnicodeError (DecodeError e _)) = e+    show (UnicodeError (EncodeError e _)) = e     show (ParseUriError raw)       = "Cannot parse URI: " ++ raw-    show (ParseItemDateError item) = "Cannot parse date from item: " ++ show item+    show (ParseItemDateError item) = unlines [+        "Cannot parse date from item: ",+        "    title: "       ++ (show $ getItemTitle item),+        "    link:"         ++ (show $ getItemLink item),+        "    publish date:" ++ (show $ getItemPublishDate item),+        "    date:"         ++ (show $ getItemDate item)]     show (ParseTimeError raw)      = "Cannot parse time: " ++ raw     show (ParseFeedError raw)      = "Cannot parse feed: " ++ raw     show (CE e)                    = show e+    show (IOE e)                   = ioeGetLocation e ++ " " ++ maybe "" id (ioeGetFileName e) ++ " " ++ ioeGetErrorString e++instance Error ImmError where+    strMsg x = OtherError x -- }}} +-- {{{ Settings type data CliOptions = CliOptions {-    mParameter    :: Maybe String,+    mCheck        :: Bool,+    mList         :: Bool,+    mDenyReconf   :: Bool,         -- ^ Do not recompile configuration even if it has changed     mMasterBinary :: Maybe String}    deriving (Data, Typeable, Show, Eq) --- | +-- | Set of settings for imm data Settings = Settings {     mStateDirectory :: PortableFilePath,-    mError          :: Maybe String}+    mFeedGroups     :: [FeedGroup],+    mFromBuilder    :: (Item, Feed) -> String,+    mSubjectBuilder :: (Item, Feed) -> T.Text,+    mBodyBuilder    :: (Item, Feed) -> T.Text   -- ^ sic!+}+-- }}} +-- {{{ Feed types type FeedGroup = (FeedSettings, [String])   data FeedSettings = FeedSettings {-    mMailDirectory  :: PortableFilePath}+    mMaildir  :: PortableFilePath}  type ImmFeed = (URI, Feed)+-- }}}  data Mail = Mail {     mReturnPath         :: String,@@ -50,7 +88,7 @@     mMIME               :: String,     mCharset            :: String,     mContentDisposition :: String,-    mContent            :: T.Text}+    mBody               :: T.Text}  -- | Set of reference directories, typically used to build FilePath-s data RefDirs = RefDirs {
Imm/Util.hs view
@@ -1,36 +1,54 @@ {-# LANGUAGE NoMonomorphismRestriction #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE FlexibleContexts #-} module Imm.Util where  -- {{{ Imports import Imm.Types -import Codec.Binary.UTF8.String----import Control.Exception+import qualified Control.Exception as E import Control.Monad.Error --import Control.Monad.IO.Class +import qualified Data.ByteString.Lazy as B import Data.Maybe-import Data.Time+import Data.Text.Lazy.Encoding hiding(decodeUtf8)+import qualified Data.Text.Lazy as T+import qualified Data.Time as T import Data.Time.RFC2822 import Data.Time.RFC3339 ---import Network.URI+import Network.Browser as N+import Network.HTTP+import Network.URI as N  import System.Console.CmdArgs import System.Directory import System.Environment.XDG.BaseDir+import System.Locale++import qualified Text.Feed.Import as F+import Text.Feed.Types -- }}} +-- {{{ Monadic utilities+-- | Shortcut to liftIO io :: MonadIO m => IO a -> m a io = liftIO -logNormal, logVerbose :: String -> IO ()-logNormal  = whenNormal . putStrLn-logVerbose = whenLoud . putStrLn+-- | Monad-agnostic version of Control.Exception.try+try :: (MonadIO m, MonadError ImmError m) => IO a -> m a+try = (io . E.try) >=> either (throwError . IOE) return +-- }}} -resolve :: (RefDirs -> a) -> IO a-resolve f = do+-- | Print logs with arbitrary importance.+logNormal, logVerbose :: MonadIO m => String -> m ()+logNormal  = io . whenNormal . putStrLn+logVerbose = io . whenLoud . putStrLn++-- | Evaluate given function while replacing directory variables appropriately for the current system+resolve :: MonadIO m => (RefDirs -> a) -> m a+resolve f = io $ do     homeDir   <- getHomeDirectory     tmpDir    <- getTemporaryDirectory     configDir <- getUserConfigDir "imm"@@ -38,14 +56,25 @@          return . f $ RefDirs homeDir tmpDir configDir dataDir -escapeFileName :: Char -> String-escapeFileName '/' = "|"-escapeFileName x   = x:[]-    -parseDate :: String -> Maybe UTCTime-parseDate date = listToMaybe . map zonedTimeToUTC . catMaybes . map ((flip ($)) date) $ [readRFC2822, readRFC3339]+-- {{{ Monad-agnostic version of various error-prone functions    +decodeUtf8 :: MonadError ImmError m => B.ByteString -> m T.Text+decodeUtf8 = either (throwError . UnicodeError) return . decodeUtf8' -decodeIfNeeded :: String -> String-decodeIfNeeded text = case isUTF8Encoded text of-    False -> text-    _     -> decodeString text++parseDate :: String -> Maybe T.UTCTime+parseDate date = listToMaybe . map T.zonedTimeToUTC . catMaybes . flip map [readRFC2822, readRFC3339, T.parseTime defaultTimeLocale "%a, %d %b %G %T"] $ \f -> f . T.unpack . T.strip . T.pack $ date++parseFeedString :: MonadError ImmError m => String -> m Feed+parseFeedString x = maybe (throwError . ParseFeedError $ show x) return $ F.parseFeedString x++parseURI :: (MonadError ImmError m) => String -> m URI+parseURI uri = maybe (throwError $ ParseUriError uri) return $ N.parseURI uri++parseTime :: (MonadError ImmError m) => String -> m T.UTCTime+parseTime string = maybe (throwError $ ParseTimeError string) return $ T.parseTime defaultTimeLocale "%c" string++request :: Request B.ByteString -> BrowserAction (HandleStream B.ByteString) (URI, Response B.ByteString)+request r = io $ (E.catch :: IO a -> (IOError -> IO a) -> IO a)+    (browse $ N.request r)+    (\e -> return (rqURI r, Response (4,0,4) ("Unable to connect to " ++ show (rqURI r)) [] B.empty))    +-- }}}
imm.cabal view
@@ -1,5 +1,5 @@ Name:                imm-Version:             0.2.0.0+Version:             0.3.0.0 Synopsis:            RSS-to-maildir tool --Description:          --Homepage:@@ -23,7 +23,7 @@     Exposed-modules:         Imm.Boot,         Imm.Config,-        Imm.Core,+        Imm.Main,         Imm.Feed,         Imm.Types,         Imm.Util,@@ -33,9 +33,9 @@         base == 4.*,         bytestring,         cmdargs,+        cond,         directory,         dyre,-        errors,         feed,         filepath,         HTTP,@@ -47,7 +47,6 @@         text,         time,         timerep,-        transformers,         utf8-string,         xdg-basedir,         xml@@ -58,6 +57,6 @@  Executable imm     Build-depends: imm, base == 4.*-    Main-is: Main.hs+    Main-is: Executable.hs     Hs-Source-Dirs: Imm     Ghc-options: -Wall -threaded