packages feed

scrapbook-0.3.2: src/ScrapBook/Feed/Atom.hs

{-# LANGUAGE DataKinds         #-}
{-# LANGUAGE FlexibleContexts  #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE OverloadedLabels  #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PolyKinds         #-}
{-# LANGUAGE TypeFamilies      #-}
{-# LANGUAGE TypeOperators     #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}

module ScrapBook.Feed.Atom
  ( fromAtomFeed
  , toAtomFeed
  , toEntry
  , fromEntry
  , toDocument
  ) where

import qualified Data.List                    as L
import           RIO
import qualified RIO.Map                      as M
import qualified RIO.Text                     as T

import           Data.Extensible
import qualified Data.Ord                     as Ord
import qualified Data.XML.Types               as XML (Content (..))
import           ScrapBook.Data.Config
import           ScrapBook.Data.Site
import qualified ScrapBook.Feed.Atom.Internal as My
import           ScrapBook.Fetch.Internal     (Fetch (..), fetchHtml,
                                               throwFetchError)
import qualified Text.Atom.Feed               as Atom
import           Text.Feed.Import             (parseFeedString)
import           Text.Feed.Types              (Feed (..))
import qualified Text.XML                     as XML

instance Fetch ("atom" >: AtomConfig) where
  fetchFrom _ site conf = do
    resp <- T.unpack <$> fetchHtml (conf ^. #url)
    case parseFeedString resp of
      Just (AtomFeed feed) -> pure $ fromAtomFeed site conf feed
      _                    -> throwFetchError (Right "can't parse atom feed.")

fromAtomFeed ::
  IsSiteFields xs => Record xs -> AtomConfig -> Atom.Feed -> [Post (Record xs)]
fromAtomFeed site conf feed = fromEntry site conf <$> Atom.feedEntries feed

toAtomFeed :: IsSiteFields xs => FeedConfig -> [Post (Record xs)] -> Atom.Feed
toAtomFeed conf posts =
  (Atom.nullFeed
    (mconcat [conf ^. #baseUrl, "/", T.pack $ feedName conf])
    (Atom.TextString $ conf ^. #title)
    (maybe "" (view #date) $ listToMaybe posts'))
    { Atom.feedEntries = map toEntry posts'
    , Atom.feedLinks = [Atom.nullLink $ conf ^. #baseUrl]
    }
  where
    posts' = L.sortOn (Ord.Down . view #date) posts

toEntry :: IsSiteFields xs => Post (Record xs) -> Atom.Entry
toEntry post =
  (Atom.nullEntry
    (post ^. #url) (Atom.TextString $ post ^. #title) (post ^. #date))
    { Atom.entryAuthors =
        [ Atom.nullPerson { Atom.personName = post ^. #site ^. #author } ]
    , Atom.entryLinks = [Atom.nullLink $ post ^. #url]
    , Atom.entrySummary = fmap fromSummary (post ^. #summary)
    }

fromEntry ::
  IsSiteFields xs => Record xs -> AtomConfig -> Atom.Entry -> Post (Record xs)
fromEntry site conf entry
    = #title   @= txtToText (Atom.entryTitle entry)
   <: #url     @= toUrl site conf entry
   <: #date    @= Atom.entryUpdated entry
   <: #summary @= (toSummary =<< Atom.entrySummary entry)
   <: #site    @= site
   <: nil

txtToText :: Atom.TextContent -> Text
txtToText = T.pack . Atom.txtToString

toDocument :: Atom.Feed -> Either (Set Text) XML.Document
toDocument feed = XML.fromXMLElement (My.xmlFeed feed)
  <&> \elm -> XML.Document (XML.Prologue [] Nothing []) elm []

toUrl :: IsSiteFields xs => Record xs -> AtomConfig -> Atom.Entry -> Text
toUrl site conf entry =
  maybe ""
    (toAbsoluteUrl site . Atom.linkHref )
    (listToMaybe . filter (p . Atom.linkAttrs) $ Atom.entryLinks entry)
  where
    p attrs = all (`elem` attrs) $
      toAttr <$> maybe [] M.toList (conf ^. #linkAttrs)

toAttr :: (Text, Text) -> Atom.Attr
toAttr (k, v) = (fromString $ T.unpack k, [XML.ContentText v])

toSummary :: Atom.TextContent -> Maybe Summary
toSummary (Atom.TextString txt) = Just $ TextSummary txt
toSummary (Atom.HTMLString txt) = Just $ HtmlSummary txt
toSummary _                     = Nothing

fromSummary :: Summary -> Atom.TextContent
fromSummary (TextSummary txt) = Atom.TextString txt
fromSummary (HtmlSummary txt) = Atom.HTMLString txt