packages feed

imm-2.1.0.0: src/main/Alternate.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes        #-}
{-# LANGUAGE TypeApplications  #-}
{-# LANGUAGE TypeFamilies      #-}
module Alternate (AlternateException(..), extractAlternateLinks) where

-- {{{ Imports
import           Data.Aeson
import qualified Data.ByteString.Char8   as Char8
import           Imm.Link
import           Imm.Logger              as Logger
import           Imm.Pretty
import           Pipes
import           Pipes.ByteString        as Pipes (toHandle)
import           System.IO               (hClose)
import           System.Process.Typed
import           URI.ByteString.Extended
-- }}}


newtype AlternateException = FeedNotFound URI
  deriving(Eq, Show)

instance Exception AlternateException where
  displayException = show . pretty

instance Pretty AlternateException where
  pretty (FeedNotFound uri) = "No feeds found at" <+> prettyURI uri


data AlternateLink = AlternateLink Text Text Text
  deriving(Eq, Ord, Show)

instance Pretty AlternateLink where
  pretty (AlternateLink title linkType uri) = "Alternate link" <+> braces (hsep parameters) where
    parameters = [ "title" <> equals <> dquotes (pretty title)
                 , "type" <> equals <> dquotes (pretty linkType)
                 , "href" <> equals <> dquotes (pretty uri)
                 ]

instance FromJSON AlternateLink where
  parseJSON = withObject "AlternateLink" $ \v -> AlternateLink
    <$> (v .: "title" <|> pure mempty)
    <*> v .: "type"
    <*> v .: "href"


asFeedURI :: MonadFail m => URI -> Text -> m URI
asFeedURI baseURI href = let bytes = encodeUtf8 @Text href in
  case parseURI laxURIParserOptions bytes of
    Right uri -> pure uri
    _ -> case parseRelativeRef laxURIParserOptions bytes of
      Right relativeRef -> pure $ mergeURIs baseURI relativeRef
      Left e            -> fail $ show e

mergeURIs :: URI -> URIRef Relative -> URI
mergeURIs uri relativeRef = URI scheme authority path query fragment where
  scheme = uriScheme uri
  authority = rrAuthority relativeRef <|> uriAuthority uri
  path = mergePaths (uriPath uri) (rrPath relativeRef)
  query = rrQuery relativeRef
  fragment = rrFragment relativeRef

mergePaths :: ByteString -> ByteString -> ByteString
mergePaths basePath path = case Char8.head path of
  '/' -> path
  _   -> basePath <> "/" <> path


asLink :: MonadFail m => URI -> AlternateLink -> m Link
asLink baseURI (AlternateLink title type' uri) = Link (Just Alternate) title (parseMediaType type') . AnyURI <$> asFeedURI baseURI uri


extractAlternateLinks :: MonadIO m => MonadFail m => Logger.Handle IO -> URI -> Producer ByteString IO () -> m [Link]
extractAlternateLinks logger baseUri html = io $ withProcessWait pup $ \pupProcess -> do
  log logger Debug $ pretty $ show @String pupProcess

  runEffect $ html >-> toHandle (getStdin pupProcess)
  hClose (getStdin pupProcess)

  links <- getStdout pupProcess & atomically <&> decode <&> fromMaybe mempty
  log logger Info $ "Found alternate links:" <+> pretty links

  mapM (asLink baseUri) links
  where pup = proc "pup" ["html head link[rel=\"alternate\"] json{}"]
          & setStdin createPipe
          & setStdout byteStringOutput
          & setStderr nullStream