packages feed

ureader-0.1.0.0: src/UReader/RSS.hs

module UReader.RSS
       ( filterItems
       , reverseItems
       , emptyFeed

       , getRSS
       , fetchFeeds

       , resolveComments
       ) where

import Control.Applicative
import Control.Arrow
import Control.Concurrent.ParallelIO
import Control.Exception
import Data.Char
import Data.Either
import Data.List as L
import Data.Maybe
import Data.Text.Encoding as T
import Data.Time
import Network.URI
import Network.Curl
import Network.Curl.Download
import Text.RSS.Syntax
import Text.RSS.Import
import Text.XML.Light.Input
import System.Locale


-- TODO filter by time = drop . find
filterItems :: (RSSItem -> Bool) -> RSS -> RSS
filterItems p rss = rss
    { rssChannel = let ch = rssChannel rss
                   in ch { rssItems = L.filter p (rssItems ch) }
    } -- TODO use lens

reverseItems :: RSS -> RSS
reverseItems rss = rss
  { rssChannel = let ch = rssChannel rss
                 in ch { rssItems = L.reverse (rssItems ch) }
  }

emptyFeed :: RSS -> Bool
emptyFeed = L.null . rssItems . rssChannel

-- TODO openAsFeed
getRSS :: URI -> IO RSS
getRSS uri = do
  body <- either (throwIO . userError) return =<< openURI (show uri)
  xml  <- maybe (throwIO $ userError "invalid XML") return $
            parseXMLDoc (T.decodeUtf8 body)
  rss  <- maybe  (throwIO $ userError "invalid RSS") return $
            elementToRSS xml
  return rss

hdrLastModified :: String
hdrLastModified = "last-modified"

lastModifiedFormat :: String
lastModifiedFormat = "%a, %e %b %Y %H:%M:%S %Z"

parseLastModified :: String -> Maybe UTCTime
parseLastModified = parseTime defaultTimeLocale lastModifiedFormat

resourceExpired :: UTCTime -> URI -> IO Bool
resourceExpired threshold uri = do
    (_, headers) <- curlHead (show uri) []
    let cheaders = L.map (first canonicalize) headers
    return $ case L.lookup hdrLastModified cheaders of
      Just (parseLastModified -> Just lm) -> threshold < lm
      _ -> True
  where
    canonicalize = L.map toLower


getRSSFrom :: Maybe UTCTime -> URI -> IO (Maybe RSS)
getRSSFrom  Nothing uri = Just <$> getRSS uri
getRSSFrom (Just t) uri = do
  updated <- resourceExpired t uri
  if updated
    then Just <$> getRSS uri
    else return Nothing

fetchFeeds :: Maybe UTCTime -> [URI]
           -> IO ([(URI, SomeException)], [RSS])
fetchFeeds mtime urls = do
    res <- parallelE (L.map (getRSSFrom mtime) urls)
    return $ second catMaybes $ partitionEithers $ urlfy res
  where
    urlfy = L.zipWith (\url -> either (Left .  (,) url) Right) urls


resolveComments :: RSS -> IO RSS
resolveComments = error "resolveComments"