packages feed

espial-0.0.16: src/Handler/Archive.hs

module Handler.Archive where

import Import
import Data.Function ((&))
import Data.Char (ord)
import qualified Data.Attoparsec.ByteString.Char8 as AP8
import qualified Data.Attoparsec.ByteString as AP
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as LBS
import qualified Data.ByteString.Char8 as BS8
import qualified Network.HTTP.Client as NH
import qualified Network.HTTP.Client.TLS as NH
import qualified Network.HTTP.Types.Status as NH
import qualified Web.FormUrlEncoded as WH
import HTMLEntities.Decoder (htmlEncodedText)
import Data.Text.Lazy.Builder (toLazyText)
import Network.Wai (requestHeaderHost)
import qualified Network.Connection as NC

shouldArchiveBookmark :: User -> Key Bookmark -> Handler Bool
shouldArchiveBookmark user kbid =
  runDB (get kbid) >>= \case
  Nothing -> pure False

  Just bm -> do
    pure $
      isNothing (bookmarkArchiveHref bm) &&
      bookmarkShared bm
      && not (_isArchiveBlacklisted bm)
      && userArchiveDefault user

getArchiveManager :: Handler Manager
getArchiveManager = do
  appSettings <- appSettings <$> getYesod
  let mSocks =
        NC.SockSettingsSimple <$>
        fmap unpack (appArchiveSocksProxyHost appSettings) <*>
        fmap toEnum (appArchiveSocksProxyPort appSettings)
  NH.newTlsManagerWith (NH.mkManagerSettings def mSocks)


archiveBookmarkUrl :: Key Bookmark -> String -> Handler ()
archiveBookmarkUrl kbid url =
  (_fetchArchiveSubmitInfo >>= \case
    Left e -> do
      $(logError) (pack e)
    Right submitInfo ->  do
        userId <- requireAuthId
        req <- _buildArchiveSubmitRequest submitInfo url
        manager <- getArchiveManager
        res <- liftIO $ NH.httpLbs req manager
        let status = NH.responseStatus res
        let updateArchiveUrl url' = runDB $ updateBookmarkArchiveUrl userId kbid $ Just url'
            headers = NH.responseHeaders res
        case status of
          s | s == NH.status200 ->
            for_ (lookup "Refresh" headers >>= _parseRefreshHeaderUrl) updateArchiveUrl
          s | s == NH.status302 || s == NH.status307 ->
            for_ (lookup "Location" headers) (updateArchiveUrl . decodeUtf8)
          _ -> $(logError) (pack (show res)))
  `catch` (\(e::SomeException) -> ($(logError) $ (pack.show) e) >> throwIO e)

_isArchiveBlacklisted :: Bookmark -> Bool
_isArchiveBlacklisted Bookmark {..} =
  [ "hulu"
  , "livestream"
  , "netflix"
  , "skillsmatter"
  , "twitch.tv"
  , "vimeo"
  , "youtu.be"
  , "youtube"
  , "archive."
  ] &
  any (`isInfixOf` bookmarkHref)

_parseRefreshHeaderUrl :: ByteString -> Maybe Text
_parseRefreshHeaderUrl h = do
  let u = BS8.drop 1 $ BS8.dropWhile (/= '=') h
  if not (null u)
    then Just $ decodeUtf8 u
    else Nothing

_fetchArchiveSubmitInfo :: Handler (Either String (String , String))
_fetchArchiveSubmitInfo = do
  req <- buildRequest "https://archive.li/"
  manager <- getArchiveManager
  res <- liftIO $ NH.httpLbs req manager
  let body = LBS.toStrict (responseBody res)
      action = _parseSubstring (AP8.string "action=\"") (AP8.notChar '"') body
      submitId = _parseSubstring (AP8.string "submitid\" value=\"") (AP8.notChar '"') body
  if statusCode (responseStatus res) == 200
    then pure $ (,) <$> action <*> submitId
    else pure $ Left $ "Invalid statusCode: " <> show (responseStatus res)


_parseSubstring :: AP8.Parser ByteString -> AP8.Parser Char -> BS.ByteString -> Either String String
_parseSubstring start inner = AP8.parseOnly (skipAnyTill start >> AP8.many1 inner)
  where
    skipAnyTill end = go where go = end $> () <|> AP8.anyChar *> go


fetchPageTitle :: String -> Handler (Either String Text)
fetchPageTitle url =
  do
     req <- buildRequest url
     res <- liftIO $ NH.httpLbs req =<< NH.getGlobalManager
     let body = LBS.toStrict (responseBody res)
     pure (decodeHtmlBs <$> parseTitle body)
     `catch` (\(e :: SomeException) -> do
                $(logError) $ (pack . show) e
                pure (Left (show e)))
  where
    parseTitle bs =
      flip AP.parseOnly bs do
        _ <- skipAnyTill (AP.string "<title")
        _ <- skipAnyTill (AP.string ">")
        let lt = toEnum (ord '<')
        AP.takeTill (== lt)
    decodeHtmlBs = toStrict . toLazyText . htmlEncodedText . decodeUtf8
    skipAnyTill end = go where go = end $> () <|> AP.anyWord8 *> go

_buildArchiveSubmitRequest :: (String, String) -> String -> Handler NH.Request
_buildArchiveSubmitRequest (action, submitId) href = do
  req <- buildRequest ("POST " <> action)
  pure $ req
    { NH.requestHeaders = ("Content-Type", "application/x-www-form-urlencoded") : NH.requestHeaders req
    , NH.requestBody =
        NH.RequestBodyLBS $
        WH.urlEncodeAsForm
            ([("submitid", submitId), ("url", href)] :: [(String, String)])
    , NH.redirectCount = 0
    }

buildRequest :: String -> Handler Request
buildRequest url = do
  ua <- _archiveUserAgent
  pure $ NH.parseRequest_ url & \r ->
    r { NH.requestHeaders =
          [ ("Cache-Control", "max-age=0")
          , ("User-Agent", ua)
          ]
      }

_archiveUserAgent :: Handler ByteString
_archiveUserAgent = do
  mHost <- requestHeaderHost . reqWaiRequest <$> getRequest
  pure $ "espial-" <> maybe "" (BS8.takeWhile (/= ':')) mHost