scrappy-requests-0.1.0.0: src/Scrappy/Requests/Requests.hs
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE OverloadedStrings #-}
module Scrappy.Requests.Requests where
-- TODO: should i rename as Scrappy.Requests.Class?
-- Idea: a language extension that allows module organization like:
-- import Control
-- .Monad
-- .IO.Class (liftIO)
-- .Trans
-- .StateT (f)
-- .ExceptT (g)
import Scrappy.Requests.Proxies (mkProxdManager)
import Scrappy.Requests.BuildActions (FilledForm(..), showQString, Namespace, QueryString, FormError(..))
import Scrappy.Scrape (ScraperT, runScraperOnHtml, hoistMaybe)
import Scrappy.Find (findNaive)
import Scrappy.Elem.ChainHTML (contains)
import Scrappy.Elem.SimpleElemParser (el)
import Scrappy.Elem.Types (innerText', ElemHead, Clickable(..))
import Scrappy.Links (BaseUrl, Link(..), renderLink)
import Scrappy.Requests.Types (CookieManager(..))
import Scrappy.Types (Html)
-- import Test.WebDriver (WD, getSource, runWD, openPage, getCurrentURL, executeJS)
-- import Test.WebDriver.Commands.Wait (waitUntil, expect, )
-- import Test.WebDriver.Commands ( Selector(ById, ByXPath), findElem )
-- import qualified Test.WebDriver.Commands as WD (click)
-- import Test.WebDriver.Exceptions (InvalidURL(..))
-- import Test.WebDriver.JSON (ignoreReturn)
-- import Test.WebDriver.Session (getSession, WDSession)
import Control.Concurrent (threadDelay)
import Network.HTTP.Types.Header
import Network.HTTP.Client.TLS (tlsManagerSettings)
import Network.HTTP.Client
import Network.HTTP.Types.Method (methodGet, Method,)
import System.Directory (removeFile, copyFile, getAccessTime, listDirectory)
import Text.Parsec (ParsecT, Parsec, ParseError, parse, Stream, many)
import Control.Monad.IO.Class (MonadIO, liftIO )
import Control.Monad.Trans.State (StateT, gets, put, get)
import Control.Monad.Trans.Class (lift)
import Control.Monad (when)
import Control.Monad.Trans.Except (ExceptT)
import Control.Monad.Trans.Maybe (MaybeT, runMaybeT)
import Data.Functor.Identity (Identity)
import Control.Exception (Exception)
import Control.Monad.Except (throwError)
import Control.Monad.Catch (MonadCatch, MonadThrow, catch)
import Data.Maybe (catMaybes, fromMaybe)
import Data.Map (Map, toList)
import Data.List (isInfixOf, isSuffixOf, maximumBy)
import Data.Text (Text, unpack, pack)
import Data.Text.Encoding (encodeUtf8, decodeUtf8, decodeUtf8With)
import qualified Data.Text.Lazy as LazyTX (toStrict, Text)
import qualified Data.Text.Lazy.Encoding as Lazy (decodeUtf8With)
import qualified Data.ByteString.Lazy as LBS
import qualified Data.ByteString as BS
import Data.Time.Clock (UTCTime)
import Data.Time.Clock.System (SystemTime(MkSystemTime), getSystemTime, systemSeconds)
import Data.Int (Int64)
import Data.Aeson (encodeFile)
data ExistT m a = ExistT { runExistT :: MaybeT m a }
-- runSomeFunc :: MonadIO m => FilePath -> Url -> (Html -> MaybeT m a) -> MaybeT m a
-- runSomeFunc fp url someFunc = do
-- (html, _) <- liftIO $ getHtmlST url
-- case someFunc html of
-- Just a -> pure a
-- Nothing -> do
-- htmlV <- fetchVDOM url
-- case someFunc htmlV of
-- Just a -> pure . Just $ a
-- Nothing -> do
-- -- encodeFile (mkFileUrl url)
-- pure . Just $ Nothing
-- Regarding the need to model similar to a browser here's what we know
-- -> The browser gets the index.html (like we do with getHtml, getHtml' ((which should be renamed to getHtmlRaw))
-- -> When the browser gets this it parses the entire HTML structure and finds what it needs to request
-- --> links for css (currently negligible) and scripts which do not exist in the code but have a src attribute
-- -> Also if an action attribute is 0 then it's the current URL
-- Need a section for Headers logic
type ParsecError = ParseError
-- runScraperM "" $ \html -> do
-- ...
---- Control flow
(>>|=) :: Monad m => [a] -> (a -> MaybeT m a) -> MaybeT m [a]
(>>|=) x f = successesM f x
successesM :: Monad m => (a -> MaybeT m b) -> [a] -> MaybeT m [b]
successesM fm inputs =
lift . (fmap catMaybes) . sequenceA $ fmap (runMaybeT . fm) inputs
successesM_ :: Monad m => (a -> MaybeT m b) -> [a] -> MaybeT m ()
successesM_ a b = successesM a b >> return ()
successes :: [a] -> (a -> Maybe b) -> Maybe [b]
successes inputs f =
case catMaybes $ fmap f inputs of
[] -> Nothing
xs -> Just xs
------------------
runScraperM :: Url -> (Html -> MaybeT IO a) -> MaybeT IO a
runScraperM url scraper = (liftIO $ getHtml' url) >>= scraper
trySiteLink :: MonadIO m => Link -> (Html -> MaybeT m ()) -> MaybeT m Link
trySiteLink url scraperBasedEffects = do
html <- liftIO $ getHtml' (renderLink url)
scraperBasedEffects html
return url
-- | propogate to requests module
runScraperOnUrl' :: (MonadIO m, MonadThrow m) => Link -> ScraperT a -> m (Maybe [a])
runScraperOnUrl' url p = fmap (runScraperOnHtml p) (getHtml'' url)
-- | Get html with no Proxy
getHtml'' :: (MonadThrow m, MonadIO m) => Link -> m Html
getHtml'' url = do
mgrHttps <- liftIO $ newManager tlsManagerSettings
requ <- parseRequest (renderLink url)
response <- liftIO $ httpLbs requ mgrHttps
return $ extractDadBod response
-- aside
-- Also need to generalize to MonadIO
-- | Should change these to name_ and then make these names do same thing except read in a
-- | session variable
type Url = String
runScraperOnUrl :: Link -> Parsec Html () a -> IO (Maybe [a])
runScraperOnUrl (Link url) p = fmap (runScraperOnHtml p) (getHtml' url)
runScraperOnUrls :: [Link] -> Parsec Html () a -> IO (Maybe [a])
runScraperOnUrls urls p = fmap (foldr (<>) Nothing) $ mapM (flip runScraperOnUrl p) urls
-- foldr :: (a -> b -> c)
foldFunc :: Maybe [a] -> Maybe [a] -> Maybe [a]
foldFunc = undefined
runScrapersOnUrls = undefined
--- this is meant to be pseudo code at the moment
type STM = IO
-- | Merge Maybe [a] when multiple urls
concurrentlyRunScrapersOnUrls :: [Link] -> [ParsecT s u m a] -> STM (Maybe [a])
concurrentlyRunScrapersOnUrls = undefined
-- inner will call concurrent stream functions on the given urls
-- doSignin :: ElemHead -> ElemHead -> Url
-- | Get html with no Proxy
-- | Raw af
getHtml' :: Url -> IO Html
getHtml' url = do
mgrHttps <- newManager tlsManagerSettings
requ <- parseRequest url
response <- httpLbs requ mgrHttps
return $ extractDadBod response
-- | Gurantees retrieval of Html by replacing the proxy if we are blocked or the proxy fails
getHtml :: Manager -> Link -> IO (Manager, Html)
getHtml mgr url = do
requ <- parseRequest (renderLink url)
let
headers = [ (hUserAgent, "Mozilla/5.0 (X11; Linux x86_64; rv:84.0) Gecko/20100101 Firefox/84.0")
, (hAcceptLanguage, "en-US,en;q=0.5")
, (hAcceptEncoding, "gzip, deflate, br")
, (hConnection, "keep-alive")
]
req = requ { requestHeaders = (fmap . fmap) (encodeUtf8 . pack) headers
, secure = True
}
(mgr', r) <- catch (fmap ((mgr,) . extractDadBod) $ httpLbs requ mgr) (recoverMgr' url)
return (mgr', r)
recoverMgr' :: Link -> HttpException -> IO (Manager, String)
recoverMgr' url _ = mkProxdManager >>= flip getHtml url
-- -- type SiteNew sv = ReaderT (MVar [FreeSite], sv, Url) (ExceptT ScrapeException' IO) Url
-- -- maybe but really just want this:
-- scrape patttern -- implicit State
-- data ImplicitState = IsUrl String | IsHtml String
-- -- If we want to scrape we check the state
-- -- when IsUrl $ do getHtml (=<< gets seshV) >>= putImplicit
-- f :: SiteScraperT ()
-- f = do
-- scrape x
-- scrape y
-- fetch newUrl -- lazily puts (IsUrl String)
-- -- A site could also keep hold of a Map of all urls on site
-- -- We could also use this informatsion for patterns
-- -- ie a Contact us section would probably be shallower a tree
-- class MultiSite where
-- -- really just would be a construct for this is not constrained to a single site via getUsefulLinks
-- -- could also do where if we do fetch another site, we have a mechanism to hold
-- -- MVars of site data from previously viewed sites performed upon fetch
-- -- | Where the sv is effectively constrained to SessionState sv => sv
type SiteM hasSv e a = StateT hasSv (ExceptT e IO) a
-- | TODO: implement default
-- | Where the sv is effectively constrained to SessionState sv => sv
-- type SiteT sv e a = StateT sv (ExceptT e IO) a
-- | Where the sv is effectively constrained to SessionState sv => sv
newtype SiteT sv e a = SiteT { runSite :: StateT sv (ExceptT e IO) a }
type Host = String
type Port = String
class SessionState a where
getHtmlST :: (MonadThrow m, MonadIO m) => a -> Link -> m (Html, a)
getHtmlAndUrl :: (MonadThrow m, MonadIO m) => a -> Link -> m (Html, Link, a)
submitForm :: (MonadThrow m, MonadIO m) => a -> FilledForm -> m ((Html, Link, a), FilledForm)
click :: (MonadThrow m, MonadIO m) => FilePath -> a -> Clickable -> m (String, a)
-- Download a pdf link
clickWritePdf :: (MonadThrow m, MonadIO m) => a -> FilePath -> Clickable -> m (Either ScrapeException a)
clickWriteFile :: (MonadThrow m, MonadIO m) => a -> FileExtension -> Clickable -> m (Either ScrapeException (), a)
clickWriteFile' :: a
-> FileExtension -- desired file extension to match
-> FilePath -- where to save
-> Clickable
-> IO (Either ScrapeException (), a)
-- askCookies :: m CookieJar
instance SessionState Manager where
getHtmlST manager link = do
(m, s) <- liftIO $ getHtml manager link
return (s, m)
getHtmlAndUrl manager (Link url) = do
req <- parseRequest url
liftIO $ catch (baseGetHtml manager req) (saveReq' (Link url) getHtmlAndUrl)
-- Note: qStrVari has data on basic params factored in
submitForm manager (FilledForm actionUrl reqM term tInput qStrVari) = do
req <- parseRequest actionUrl
let
req2 = req { method = reqM, queryString = (encodeUtf8 . showQString) $ head tInput <> head qStrVari }
f2 = FilledForm actionUrl reqM term tInput (tail qStrVari)
liftIO $ fmap (, f2) $ catch (baseGetHtml manager req2) (saveReq req2 baseGetHtml)
clickWritePdf manager filepath x@(Clickable _ url) = do
(pdf, mgr) <- getHtmlST manager url
-- path <- liftIO $ resultPath searchTerm (getHost baseU) (Paper x) >>= flip writeFile pdf
liftIO $ writeFile filepath pdf
return $ Right mgr
-- Invalidate normal HTML responses here------
-- AND if file did not download then this was not a PdfLink like expected
click = undefined
clickWriteFile = undefined
clickWriteFile' = undefined
-- we only actually wanna do this in the case of
-- getHtmlST' :: (HasSessionState hasSv, MonadIO m) => Url -> StateT hasSv m Html
-- getHtmlST' = do
-- (c, m) <- gets cookies <*> gets manager
-- url <- getHtmlST (c,m) url
-- return ""
-- getHtmlReq :: CookieManager -> Request -> IO (CookieManager, Html)
-- getHtmlReq mgr req = do
-- fmap (mgr,) $ httpLbs req mgr -- ?
-- data CookieManager = CookieManager CookieJar Manager
-- getHtmlST :: Url || Form ||
--- - MonadIO m, HasSessionState s) => SiteT s m a
-- we dont fucking need this, just take idea with seshVar (get, put set up)
-- class HasSessionState a sv | a -> sv where
-- takeSession :: SessionState sv => a -> sv
-- writeSession :: SessionState sv => sv -> a -> a
setCJ :: CookieJar -> Request -> Request
setCJ cj req = req { cookieJar = Just cj }
setBasicHeaders :: Request -> Request
setBasicHeaders req =
let
headers = [ (hUserAgent, "Mozilla/5.0 (X11; Linux x86_64; rv:98.0) Gecko/20100101 Firefox/98.0")
, (hAcceptLanguage, "en-US,en;q=0.5")
--, (hAcceptEncoding, "gzip, deflate, br")
, (hConnection, "keep-alive")
]
in req { requestHeaders = (fmap . fmap) (encodeUtf8 . pack) headers
, secure = True
}
buildReq :: MonadThrow m => CookieJar -> Link -> m Request
buildReq cj (Link url) = do
req <- parseRequest url
pure $ (setCJ cj) . setBasicHeaders $ req
-- ((setCJ cj) . setBasicHeaders) <$> parseRequest url
-- | Gurantees retrieval of Html by replacing the proxy if we are blocked or the proxy fails
getHtmlMgr :: Manager -> Link -> IO (Manager, Html)
getHtmlMgr mgr url = do
requ <- parseRequest (renderLink url)
let
headers = [ (hUserAgent, "Mozilla/5.0 (X11; Linux x86_64; rv:84.0) Gecko/20100101 Firefox/84.0")
, (hAcceptLanguage, "en-US,en;q=0.5")
, (hAcceptEncoding, "gzip, deflate, br")
, (hConnection, "keep-alive")
]
req = requ { requestHeaders = (fmap . fmap) (encodeUtf8 . pack) headers
, secure = True
}
(mgr', r) <- catch (fmap ((mgr,) . extractDadBod) $ httpLbs requ mgr) (recoverMgr' url)
return (mgr', r)
{-# DEPRECATED baseGetHtml "needs extractDadBod" #-}
baseGetHtml :: Manager -> Request -> IO (Html, Link, Manager)
-- baseGetHtml Request -> ReaderT Manager IO (Html, Url)
baseGetHtml manager req = do
hResponse <- responseOpenHistory req manager
let
finReq = hrFinalRequest hResponse
dadBodNew response = (unpack . decodeUtf8) response
finResBody <- fmap mconcat $ brConsume $ responseBody $ hrFinalResponse hResponse
return (dadBodNew finResBody, Link $ (unpack . decodeUtf8) $ (host finReq) <> (path finReq) <> (queryString finReq), manager)
-- | Gurantees retrieval of Html by replacing the proxy if we are blocked or the proxy fails
getHtmlMgr' :: Manager -> Url -> IO (Manager, Html)
getHtmlMgr' mgr url = do
let
headers = [ (hUserAgent, "Mozilla/5.0 (X11; Linux x86_64; rv:84.0) Gecko/20100101 Firefox/84.0")
, (hAcceptLanguage, "en-US,en;q=0.5")
, (hAcceptEncoding, "gzip, deflate, br")
, (hConnection, "keep-alive")
]
getHtmlHeaderMgr headers mgr url
-- return (mgr', r)
getHtmlHeaderMgr :: [Header] -> Manager -> Url -> IO (Manager, Html)
getHtmlHeaderMgr headers mgr url = do
-- (fmap.fmap) extractDadBod . $
(mgr, res) <- persistGet mgr =<< mkReq headers url
res' <- readMyBody $ hrFinalResponse res
pure (mgr, res')
mkReq :: MonadThrow m => [Header] -> Url -> m Request
mkReq headers url = fmap (setHeaders headers) $ parseRequest url
where setHeaders headers req = req { requestHeaders = headers }
-- return (mgr', r)
recoverMgr :: Request
-> HttpException
-> IO (Manager, HistoriedResponse BodyReader)
recoverMgr req _ = print "recover manager" >> (flip persistGet req =<< mkProxdManager)
persistGet :: MonadIO m => Manager
-> Request
-> m (Manager, HistoriedResponse BodyReader)
persistGet sv req = liftIO $ catch (getHtmlHistoried sv req) (recoverMgr req)
-- | Only applies sv into http functions
getHtmlHistoried :: MonadIO m => Manager
-> Request
-> m (Manager, HistoriedResponse BodyReader)
getHtmlHistoried m r = liftIO $ fmap (m,) $ responseOpenHistory r m
--getHtmlFlex manager req = catch (baseGetHtml manager req) (saveReq getHtmlFlex req)
-- getHtm
-- getHtmlST url = do
-- CookieManager cj mgr <- gets sv
-- req <- parseRequest url
-- (m, s) <- getHtmlReq mgr $ setCJ cj req
-- return (s, m)
-- | this fails if its a post request
requestToUrl' :: Request -> Maybe Url
requestToUrl' req = if methodGet == method req
then Just . unpack . decodeUtf8 $ (host req) <> (path req) <> (queryString req)
else Nothing
-- works for any request
getURL :: Request -> Url
getURL req = unpack . decodeUtf8 $ (host req) <> (path req) <> (queryString req)
drop1qStrVar :: FilledForm -> FilledForm
drop1qStrVar (FilledForm a b c d qStr) = FilledForm a b c d (drop 1 qStr)
mkFormRequest :: MonadThrow m => Url -> Method -> QueryString -> m Request
mkFormRequest url reqMethod qString = do
req <- parseRequest url
pure $ req { method = reqMethod
, queryString = encodeUtf8 . showQString $ qString
}
-- extractDadBod :: Response ByteString -> String
-- extractDadBod response = (unpack . LazyTX.toStrict . mySafeDecoder . responseBody) response
-- mySafeDecoder :: ByteString -> LazyTX.Text
-- mySafeDecoder = Lazy.decodeUtf8With (\_ _ -> Just '?')
-- | Get html with no Proxy
getHtmlText :: Url -> IO Text
getHtmlText url = do
mgrHttps <- newManager tlsManagerSettings
requ <- parseRequest url
let
headers = [ (hUserAgent, "Mozilla/5.0 (X11; Linux x86_64; rv:84.0) Gecko/20100101 Firefox/84.0")
, (hAcceptLanguage, "en-US,en;q=0.5")
, (hAcceptEncoding, "gzip, deflate, br")
, (hConnection, "keep-alive")
]
req = requ { requestHeaders = (fmap . fmap) (encodeUtf8 . pack) headers
, secure = True
}
response <- httpLbs requ mgrHttps
return $ extractDadBodText response
extractDadBodText :: Response LBS.ByteString -> Text
extractDadBodText = LazyTX.toStrict . mySafeDecoder . responseBody
extractDadBod :: Response LBS.ByteString -> String
extractDadBod = unpack . LazyTX.toStrict . mySafeDecoder . responseBody
extractDadBod' :: LBS.ByteString -> String
extractDadBod' = unpack . LazyTX.toStrict . mySafeDecoder
mySafeDecoder :: LBS.ByteString -> LazyTX.Text
mySafeDecoder = Lazy.decodeUtf8With (\_ _ -> Nothing) -- Just '?')
getHistoriedBody res = fmap (readHtml . LBS.fromStrict) $ brRead . responseBody $ res
--ewqweqq
-- -- Since 0.4.1
-- withResponseHistory :: Request
-- -> Manager
-- -> (HistoriedResponse BodyReader -> IO a)
-- -> IO a
-- withResponseHistory req man = bracket
-- (responseOpenHistory req man)
-- (responseClose . hrFinalResponse)
httpHistLbs :: Request -> Manager -> IO (HistoriedResponse LBS.ByteString)
httpHistLbs req man = withResponseHistory req man $ \hRes -> do
bss <- brConsume $ responseBody . hrFinalResponse $ hRes
-- return $ hRes { responseBody = LBS.fromChunks bss }
pure $ f' hRes (f (hrFinalResponse hRes) (LBS.fromChunks bss))
where
f' hr r = hr { hrFinalResponse = r }
f res b = res { responseBody = b }
-- httpHistLbs = withResponseHistory req man $ \hrbr ->
-- pure ()
test2 = do
req <- parseRequest "https://www.google.com/"
mgr <- newManager tlsManagerSettings
hRes <- httpHistLbs req mgr
-- print $ extractDadBod . hrFinalResponse $ hRes
print . fst =<< getHtmlST (CookieManager mempty mgr) (Link "https://www.google.com/")
-- print $ responseBody . hrFinalResponse $ hRes
pure ()
readMyBody :: Response (IO BS.ByteString) -> IO Html
readMyBody res = do
chunks <- brConsume $ ((responseBody $ res) :: BodyReader)
chunk <- brRead . responseBody $ res
print chunk
let
-- body :: ByteString
body = LBS.fromChunks chunks
print "hey"
--print body
pure $ unpack . LazyTX.toStrict . mySafeDecoder {-(readHtml . fromStrict)-} $ body
where
mySafeDeco :: LBS.ByteString -> LazyTX.Text
mySafeDeco = Lazy.decodeUtf8With (\_ _ -> Just '?')
readHtml :: LBS.ByteString -> Html
readHtml = unpack . LazyTX.toStrict . mySafeDecoder
mkRGateUrl pgNum term = rGateBaseUrl <> "/search/publication?q=" <> term <> "&page=" <> (show pgNum)
rGateBaseUrl = "https://www.researchgate.net"
-- TODO(galen): rewrite these funcs to use httpHistLbs where applicable
instance SessionState CookieManager where
getHtmlST cm@(CookieManager cj mgr) link = do
req <- buildReq cj link
hRes <- liftIO $ httpHistLbs req mgr
let newCookies = responseCookieJar . hrFinalResponse $ hRes
return (extractDadBod . hrFinalResponse $ hRes, CookieManager (cj <> newCookies) mgr)
getHtmlAndUrl (CookieManager cj mgr) url = do
req <- buildReq cj url
(manager, response) <- persistGet mgr req
let
finalResponse = hrFinalResponse response
newCookies = responseCookieJar finalResponse
lastUrl = getURL $ hrFinalRequest response
html <- liftIO $ readMyBody finalResponse
return (html, Link lastUrl, CookieManager (cj <> newCookies) manager)
-- (html, ) getHtmlST cm url
-- req <- parseRequest url
-- catch (baseGetHtml manager req) (saveReq' url getHtmlAndUrl)
submitForm (CookieManager cj mgr) form@(FilledForm actionUrl reqM term tInput qStrVari) = do
req <- mkFormRequest actionUrl reqM (head tInput <> head qStrVari)
(manager, response) <- persistGet mgr req
let
finalResponse = hrFinalResponse response
newCookies = responseCookieJar finalResponse
-- undefined cuz it needs to be removed --> this is never used here
html <- liftIO $ readMyBody finalResponse
pure ((html, undefined, CookieManager (cj <> newCookies) manager), drop1qStrVar form)
-- in future we could use applyJS or something to make perfect
-- in future this could return a WebDocument
click _ cmanager (Clickable _ url) = getHtmlST cmanager url
clickWritePdf cmanager filepath x@(Clickable _ url) = do
(pdf, mgr) <- getHtmlST cmanager url
liftIO $ writeFile filepath pdf
return $ Right mgr
clickWriteFile = undefined
clickWriteFile' = undefined
-- pure ((html, cm), drop1qStrVar form)
-- -- Note: qStrVari has data on basic params factored in
-- submitForm manager (FilledForm actionUrl reqM term tInput qStrVari) = do
-- req <- parseRequest actionUrl
-- let
-- req2 = req { method = reqM
-- , queryString = (encodeUtf8 . showQString)
-- $ head tInput <> head qStrVari }
-- formToDo = FilledForm actionUrl reqM term tInput (tail qStrVari)
-- fmap (, formToDo) $ catch (baseGetHtml manager req2) (saveReq req2 baseGetHtml)
-- clickWritePdf cmanager filepath x@(Clickable baseU _ url) = do
-- (pdf, mgr) <- getHtmlST cmanager url
-- -- path <- liftIO $ resultPath searchTerm (getHost baseU) (Paper x) >>= flip writeFile pdf
-- writeFile filepath pdf
-- return $ Right mgr
-- Invalidate HTML responses here------
-- AND if file did not download then this was not a PdfLink like expected
-- data PersistentAction a = PersistentAction { action :: IO a
-- , retryInterval :: SystemTime
-- , test :: a -> Bool
-- , cutoffTries :: Maybe Int
-- , cutoffTime :: Maybe SystemTime
-- -- ^ absolute time
-- }
-- clickWritePdf wdSesh (Clickable baseU (e, attrs) url) =
-- also need to check state of previous download folder
-- since we have to do this, might as well test old `cropFrom` new
-- and result should be a single file OR notReadyYet
-- if isSuffixOf ".pdf" url
-- then
-- do
-- (pdf, wdSesh') <- getHtmlST wdSesh url
-- liftIO $ writeFile (resultFolder baseU Pdf) pdf
-- return (Right wdSesh')
-- else
-- runWD wdSesh $ do
-- e <- findElem (ByXPath . pack $ xpath (e, attrs))
-- WD.click e
-- -- src <- waitUntil 10 (do
-- Should CHANGE TO checking if .pdf format
-- thats a lot of work tho sooooo....
-- src <- getSource
-- expect (if ((length (unpack src)) < 10000) then False else True)
-- return src
-- )
-- pdf <- lift $ takeNewestFile baseU
-- let
-- host baseU = undefined
-- liftIO $ resultPath searchTerm baseU->host (Paper x) >>= flip writeFile pdf
-- fmap Right getSession
-- (unpack src,) <$> getSession
addTenSeconds :: SystemTime -> SystemTime
addTenSeconds (MkSystemTime s nanoS) =
MkSystemTime (s + 10) nanoS
-- | This is only recommended if we can be absolutely confident that
-- | the condition will be satisfied in a reasonable timescale
-- | Do note that such a function could be very useful for an FRP system
-- | that relies on external events
persistUntilUnsafe :: IO a -> (a -> Bool) -> IO a
persistUntilUnsafe actn test = do
x <- actn
if test x
then pure x
else persistUntilUnsafe actn test
toMilli :: Int -> Int
toMilli = (*) 1000000
heatDeathOfTheUniverse :: Int64
heatDeathOfTheUniverse = 100000000000000000
persistUntilSafe :: PersistentAction a -> IO a
persistUntilSafe (PersistentAction actn ri test cutR cutT) = do
x <- actn
currentT <- getSystemTime
if test x
then pure x
else if (fromMaybe 1 cutR == 0)
|| (systemSeconds currentT > heatDeathOfTheUniverse)
then error "failed"
else
do
threadDelay $ toMilli ri
persistUntilSafe $ PersistentAction actn ri test cutR cutT
data PersistentAction a = PersistentAction { action :: IO a
, retryInterval :: Int
, test :: a -> Bool
, cutoffTries :: Maybe Int
, cutoffTime :: Maybe SystemTime
-- ^ absolute time
}
-- | There's actually no reason either of these couldnt be a Monad
data PersistentActionM m a = PersistentActionM { actionM :: m a
, retryIntervalM :: SystemTime
, testM :: a -> Bool
, cutoffTriesM :: Maybe Int
, cutoffTimeM :: Maybe SystemTime
-- ^ absolute timex
}
coerceE2M :: Either a b -> Maybe b
coerceE2M (Left _) = Nothing
coerceE2M (Right a) = Just a
coerceM2E :: e -> Maybe a -> Either e a
coerceM2E e Nothing = Left e
coerceM2E _ (Just a) = Right a
-- readPage >>= writeSuccesses
-- >>/= tryNextPage (=<< ifSuccessfulFindLink) .. loop
-- further complicated by the fact that we want Abstracts as well
-- means for now that we need to preprocess the html
-- should change to : click :: sv -> Clickable -> IO WebDocument
-- results <- successesM (someRecursiveFunc sv) $ fromMaybe [] $ scrape pdfLink html
-- return (local <> results)
--grabResearchResults --> Nothing then this link in the SearchItem is Invalid
-- couldn't I use TemplateHaskell to "teach" a domain in webscraping to a scraper?
-- performResearchItem = do
-- scrape abstract html
-- pdf
-- someRecursiveFunc :: sv -> Url -> MaybeT m [ResearchResult]
firstSucc :: (a -> MaybeT IO b) -> [a] -> MaybeT IO b
firstSucc _ [] = hoistMaybe Nothing
firstSucc fm (a:as) = f $ fmap (runMaybeT . fm) as
where
f (actn:actns) = do
x <- liftIO actn
case x of
Just a -> pure a
Nothing -> f actns
-- let
-- save ::
-- catch (fm a) (\_ -> firstSucc fm as)
-- Could eventually open this up to further extensions
-- If something is a tree like string structure then we could extend to MessyTreeMatch which is
-- configurable to Open and Close
type FileExtension = FilePath
data ScrapeException = --NoAdvSearchJustBasic
NoSearchOnSite
| CantDerivePagination
| InvalidPaginate
| ItemResultNF
| PDFRequestFailed
| NoSearchItems
| AuthError
| FormErr FormError
| OtherError String
| PromisedPdfDownloadNF String
deriving Show
instance Exception ScrapeException
-- instance Trajectory s => Trajectory SiteT s m a where
-- | A trajectory is a self contained, possibly recursive scraping plan
-- | Each state is simply a message to the runner, where we left off
-- |
-- | Note too that we could use this for ResearchGate
-- |
-- | data ResearchGate = PdfPage _x_ | RelatedToPage _y_ | GetPdf? | other
-- | and the site has a recursive layout / papers do so this can just infinitely recurse between the opts
-- | we could also institute some runner Function that runs the trajectory some given N times
class Eq s => Trajectory s where
end :: s
-- could also call this stepTrajectory
performSiteState :: MonadIO m => s -> m s
-- -- | As long as we have
-- -- ...
-- end :: MySiteState
-- end = SiteEndState
-- could also be called manageTrajectories
performSiteStateMultiple :: (MonadIO m, Trajectory s) => [s] -> m ()
performSiteStateMultiple trajs = do
let
tr' = dropWhile (==end) trajs
newState <- performSiteState (head tr')
when (not . null $ tr') $ performSiteStateMultiple $ (tail tr') <> (newState:[])
--when (s == end) $ performSiteStateMultiple ss
performSiteStateSingle :: (Trajectory s, MonadIO m) => s -> m ()
performSiteStateSingle s = do
if (s == end)
then return ()
else performSiteState s >>= performSiteStateSingle
saveReq :: Request
-> (Manager -> Request -> IO (Html, Link, Manager))
-> HttpException
-> IO (Html, Link, Manager)
saveReq req func _ = do
newManager <- mkProxdManager
func newManager req
saveReq' :: Link
-> (Manager -> Link -> IO (Html, Link, Manager))
-> HttpException
-> IO (Html, Link, Manager)
saveReq' link func _ = do
newManager <- mkProxdManager
func newManager link
-- hrFinalRequest res
-- mgr <- newManager tlsManagerSettings
-- res <- fmap (responseBody . hrFinalResponse) (responseOpenHistory req mgr) >>= brRead
-- let
-- finalDraftResponse = dadBod res
-- let
-- f :: Manager -> Request -> IO (Html, Url, Manager)
-- f mgr req = do
-- hResponse <- responseOpenHistory req mgr
-- -- res <- hrFinalResponse hRes
-- -- res <- (brConsume $ hrFinalResponse hRes)
-- let
-- finReq = hrFinalRequest hResponse
-- dadBodNew response = (unpack . decodeUtf8) response
-- finResBody <- (brRead (responseBody $ hrFinalResponse hResponse))
-- return (dadBodNew finResBody, (unpack . decodeUtf8) $ (host finReq) <> (path finReq) <> (queryString finReq), mgr)
------------------------------------------------------------------------------------------------------------------
-- fmap (responseBody . hrFinalResponse) (responseOpenHistory req mgr) >>= brRead
------------------------------------------------------------------------------------------------------------------
type DownloadsFolder = FilePath
-- | Pulls newest file in downloads folder into program/IO scope
takeNewestFile :: DownloadsFolder -> Clickable -> BaseUrl -> ExceptT ScrapeException IO String
takeNewestFile dwnlds clickable baseU = do
dirNames <- liftIO $ listDirectory dwnlds
when (length dirNames == 0) $ throwError (PromisedPdfDownloadNF
"either link was not a pdf or did not download properly or not yet")
times <- liftIO $ mapM getAccessTime dirNames
let
f :: [(FilePath, UTCTime)]
f = zip dirNames times
newFile = maximumBy (\x y -> if snd x > snd y then GT else LT) f
---------------------------------------------
--Invalidate normal HTML responses here------
-- AND if file did not download then this was not a PdfLink like expected
---------------------------------------------
-- liftIO $ copyFile (fst newFile) filepath
liftIO $ readFile (fst newFile) <* (removeFile $ fst newFile)
-- return pdf
xpath :: ElemHead -> String
xpath (e, attrs) = "//" <> e <> (attrsXpath attrs)
attrsXpath :: Map String String -> String
attrsXpath m =
let
lis = toList m
f :: (String, String) -> String
f (k,v) = "[@" <> k <> "='" <> v <> "']"
in
mconcat $ fmap f lis
-- type ActionUrl = Url
-- data Form' = Form' SearchTerm ActionUrl Method QueryString [QueryString]
-- submitForm wdSesh (FilledForm{..}) = do
-- liftIO $ print "inside submit form"
-- let
-- f2 = FilledForm baseUrl reqMethod searchTerm actnAttr textInputOpts (tail qStringVariants)
-- if (reqMethod == methodGet)
-- then fmap (,f2) $ runWD wdSesh (wdFuncF baseUrl actnAttr textInputOpts qStringVariants)
-- else fmap (,f2) $ postFormWD wdSesh (FilledForm baseUrl reqMethod searchTerm actnAttr textInputOpts qStringVariants)
-- where
-- wdFuncF baseU aAttr tio qStrVari = do
-- openPage $ baseU <> "/" <> (unpack $ aAttr <> (showQString $ (head tio) <> (head qStrVari)))
-- (,,) <$> (fmap unpack getSource) <*> getCurrentURL <*> getSession
-- wdSubmitFormGET :: Url -> QueryString -> WD (String, Link, WDSession)
-- wdSubmitFormGET actionUrl tioqStrVari = do
-- openPage (actionUrl <> "?" <> (unpack (showQString $ tioqStrVari)))
-- src <- waitUntil 10 (do
-- src <- getSource
-- expect (if ((length (unpack src)) < 50000) then False else True)
-- return $ unpack src
-- )
-- (,,) <$> (fmap unpack getSource) <*> (Link <$> getCurrentURL) <*> getSession
-- postFormWD :: WDSession -> FilledForm -> IO (Html, Url, WDSession)
-- postFormWD sesh form =
-- (liftIO $ putStrLn "writing and submitting POST form")
-- >> runWD sesh (submitPostFormWD $ writeForm form)
-- FilledForm baseUrl' reqMethod' searchTerm' actnAttr' textInputOpts' (qStr:qStrs)
-- | actnAttr is part of Url
writeForm :: Text -> QueryString -> Text
writeForm url qString =
"<form id=\"myBullshitForm\""
<> " method=\"post\""
<> " action=\"" <> url <> "\""
<> ">"
-- <> ""
<> "hey"
<> writeFormParams qString
<> "</form>"
-- | actnAttr is part of Url
-- writeForm' :: Url -> Method -> QueryString -> Text
-- writeForm' (FilledForm {..}) =
-- "<form id=\"myBullshitForm\""
-- <> " method=\"" <> (if reqMethod == methodGet then "get" else "post") <> "\""
-- <> " action=\"" <> (pack baseUrl <> "/" <> actnAttr) <> "\""
-- <> ">"
-- <> writeFormParams (head textInputOpts <> head qStringVariants)
-- <> "</form>"
writeFormParams :: QueryString -> Text
writeFormParams [] = ""
writeFormParams (param:params) = writeParam param <> writeFormParams params
writeParam :: (Namespace, Text) -> Text
writeParam (n, v) =
"<input type=\"hidden\""
<> " name=\"" <> n <> "\""
<> " value=\"" <> v <> "\""
<> ">"