packages feed

imm-0.5.0.0: Imm/Core.hs

{-# LANGUAGE TypeFamilies #-}
module Imm.Core where

-- {{{ Imports
import Imm.Config
import Imm.Database
import Imm.Error
import Imm.Feed (ImmFeed, FeedID)
import qualified Imm.Feed as Feed
import qualified Imm.Maildir as Maildir
import qualified Imm.Mail as Mail
import Imm.OPML as OPML
import Imm.Options (CliOptions(..), OptionsReader(..), log, logV)
import qualified Imm.Options as Options
import Imm.Util

import Control.Applicative
import Control.Conditional
import Control.Lens hiding((??))
import Control.Monad hiding(forM_, mapM_)
import Control.Monad.Base
import Control.Monad.Error hiding(forM_, mapM_)
import Control.Monad.Reader hiding(forM_, mapM_)
import Control.Monad.Trans.Control

import Data.Default
import Data.Foldable
-- import Data.Functor
import Data.Maybe
import Data.Time as T
import Data.Time.RFC2822
import Data.Time.RFC3339

import Prelude hiding(log, mapM_, sum)

import System.Directory
import System.Locale

import Text.Feed.Query as F
import Text.Feed.Types as F
-- }}}

-- {{{ Types
type FeedList = [(Config -> Config, FeedID)]

newtype I a = I { unIT :: ErrorT ImmError (ReaderT CliOptions (ReaderT Config IO)) a}
    deriving (Applicative, Functor, Monad, MonadBase IO, MonadError ImmError)

instance MonadBaseControl IO I where
    newtype StM I a  = StI { unStI :: StM (ErrorT ImmError (ReaderT CliOptions (ReaderT Config IO))) a }
    liftBaseWith f   = I . liftBaseWith $ \runInBase -> f $ liftM StI . runInBase . unIT
    restoreM         = I . restoreM . unStI


instance ConfigReader I where
    readConfig l = I $ (lift . lift) ask >>= return . view l

instance OptionsReader I where
    readOptions l = I $ lift ask >>= return . view l

runI :: CliOptions -> Config -> I a -> IO (Either ImmError a)
runI options config i = do
    (`runReaderT` config). (`runReaderT` options) . runErrorT $ unIT i

runI' :: CliOptions -> Config -> I () -> IO ()
runI' options config i = do
    result <- runI options config i
    either print return result
-- }}}


checkStateDirectory :: (OptionsReader m, MonadBase IO m, MonadError ImmError m) => m ()
checkStateDirectory = try . io . (createDirectoryIfMissing True) =<< Options.getStateDirectory


check :: (MonadBase IO m) => CliOptions -> FeedList -> m ()
check options feeds = io . forM_ feeds $ \(f, feedID) -> runI' options (f def) $ do
    log $ "Checking: " ++ show feedID
    Feed.check =<< Feed.download feedID


importOPML :: (MonadBase IO m) => m ()
importOPML = io $ mapM_ addFeeds =<< OPML.read <$> getContents


list :: CliOptions -> FeedList -> IO ()
list options = mapM_ (\(f, feedID) -> runI' options (f def) $ (io . putStrLn =<< Feed.showStatus feedID))


markAsRead :: CliOptions -> FeedList -> IO ()
markAsRead options = mapM_ (\(f, feedID) -> runI options (f def) $ checkStateDirectory >> Feed.markAsRead feedID)


markAsUnread :: CliOptions -> FeedList -> IO ()
markAsUnread options = mapM_ (\(f, feedID) -> runI options (f def) $ Feed.markAsUnread feedID)


update :: (MonadBase IO m) => CliOptions -> FeedList -> m ()
update options feeds = io . forM_ feeds $ \(f, feedID) -> do
    runI' options (f def) $ do
        log $ "Updating: " ++ show feedID
        checkStateDirectory
        updateFeed =<< Feed.download feedID

-- | Write mails for each new item, and update the last check time in state file.
updateFeed :: (Applicative m, ConfigReader m, MonadBase IO m, OptionsReader m, MonadError ImmError m) => ImmFeed -> m ()
updateFeed (uri, feed) = do
--    checkStateDirectory
    Maildir.create =<< readConfig maildir

    logV $ Feed.describe feed

    lastCheck <- getLastCheck uri
    results <- forM (feedItems feed) $ \item -> do
        (Right date) <- Feed.getDate item
        (date > lastCheck) ? (updateItem (item, feed) >> return 1) ?? return 0
    log $ "==> " ++ show (sum results) ++ " new item(s)"
    Feed.markAsRead uri


updateItem :: (Applicative m, ConfigReader m, MonadBase IO m, OptionsReader m, MonadError ImmError m) => (Item, Feed) -> m ()
updateItem (item, feed) = do
    logV $ Feed.describeItem item

    timeZone <- io getCurrentTimeZone
    dir <- readConfig maildir
    Maildir.add dir =<< Mail.build timeZone (item, feed)


instance Default Config where
    def = Config {
        _maildir       = "feeds",
        _dateParsers   = [
            return . zonedTimeToUTC <=< readRFC2822,
            return . zonedTimeToUTC <=< readRFC3339,
            T.parseTime defaultTimeLocale "%a, %d %b %G %T",
            T.parseTime defaultTimeLocale "%Y-%m-%d",
            T.parseTime defaultTimeLocale "%e %b %Y",
            T.parseTime defaultTimeLocale "%a, %e %b %Y %k:%M:%S %z",
            T.parseTime defaultTimeLocale "%a, %e %b %Y %T %Z"],
        _formatFrom    = \(item, feed) -> fromMaybe (getFeedTitle feed) $ getItemAuthor item,
        _formatSubject = \(item, _feed) -> fromMaybe "Untitled" $ getItemTitle item,
        _formatBody    = defaultBody,
        _decoder       = "UTF-8"
    }

defaultBody :: (Item, Feed) -> String
defaultBody (item, _feed) = "<p>" ++ link ++ "</p><p>" ++ (null content ? description ?? content) ++ "</p>"
  where
    link        = fromMaybe "No link found." $ getItemLink item
    content     = Feed.getItemContent item
    description = fromMaybe "No description." $ getItemDescription item