extemp-0.0.1: Extemp.hs
{-# LANGUAGE OverloadedStrings, TemplateHaskell, DeriveDataTypeable, ScopedTypeVariables,
TypeFamilies, MultiParamTypeClasses, FlexibleContexts, StandaloneDeriving,
TypeSynonymInstances, TypeOperators #-}
module Main where
import SmartGroup
import qualified Data.Set as Set
import Data.Set (Set)
import qualified Data.Map as Map
import Data.Map (Map)
import qualified Data.ByteString.Lazy.Char8 as L
import qualified Data.ByteString.Lazy.Search as S
import Data.Maybe
import Data.Monoid
import Data.Typeable
import Data.Time
import Control.Monad
import Control.Monad.Trans
import Control.Monad.State
import Control.Monad.Maybe
import Control.Concurrent
import Happstack.Server.SimpleHTTP as HS
import Happstack.State
import Happstack.Util.Cron
import Happstack.Auth
import System.Environment
import System.Locale
import Text.XHtml hiding (dir)
import Text.Feed.Query
import Text.Feed.Import
import Text.Feed.Types
import Text.XML.Light
import Text.Regex.TDFA
import Text.Regex.TDFA.Common hiding (look)
import Network.URI (URI(..),URIAuth(..), relativeTo, parseURI, parseURIReference, uriQuery)
import qualified Network.HTTP.Base as HTTP
import Network.HTTP.Headers
import Network.Browser hiding (Proxy)
import qualified Control.Monad.Parallel as P
deriving instance Read URIAuth
deriving instance Ord URIAuth
deriving instance Read URI
deriving instance Ord URI
instance Version URIAuth
instance Version URI
$(deriveSerialize ''URIAuth)
$(deriveSerialize ''URI)
data FeedError = InvalidFeed URI | NoConnect URI deriving (Show,Read,Ord,Eq,Typeable)
$(deriveSerialize ''FeedError)
instance Version FeedError
data Config = Config (Map String URI) (Set String) [L.ByteString] Day (Set FeedError) deriving (Read,Show,Ord,Eq,Typeable)
instance Version Config
$(deriveSerialize ''Config)
instance Component Config where
type Dependencies Config = AuthState :+: End
initialValue = Config Map.empty Set.empty [] (ModifiedJulianDay 0) Set.empty
addSource :: String -> Update Config String
addSource i = case parseURI i of
(Just u) -> (modify $ \(Config us ss o d errs)-> Config (Map.insert i u us) ss o d errs) >> return "/"
Nothing -> return "invalid"
delSource :: String -> Update Config ()
delSource i = modify $ \(Config us ss o d errs)-> Config (Map.delete i us) ss o d errs
addErr :: FeedError -> Update Config ()
addErr i = modify $ \(Config us ss o d errs)-> Config us ss o d (Set.insert i errs)
delErr :: FeedError -> Update Config ()
delErr i = modify $ \(Config us ss o d errs)-> Config us ss o d (Set.delete i errs)
getConfig :: Query Config Config
getConfig = askState
putConfig :: Config -> Update Config ()
putConfig = putState
$(mkMethods ''Config ['addSource, 'delSource, 'addErr, 'delErr, 'getConfig, 'putConfig])
showErr :: FeedError -> String
showErr (InvalidFeed u) = (show u) ++ " is not a valid feed."
showErr (NoConnect u) = "Could not connect to " ++ (show u)
liftMaybe :: Maybe a -> MaybeT IO a
liftMaybe = MaybeT . return
printIt :: ServerPartT IO Response
printIt = do
(Config us ss o _ errs) <- query GetConfig
d <- liftIO $ liftM utctDay $ getCurrentTime
update $ PutConfig $ Config us ss [] d errs
let a = L.concat o
return $ if L.null a
then toResponse $ centerHtml 500 << paragraph << ("No new articles" :: String)
else toResponseBS "text/html; charset=UTF-8" a
refreshCache :: IO ()
refreshCache = do
putStrLn "Refreshing cache"
(Config us ss _ _ _) <- query GetConfig
as <- newItems ss (Map.elems us)
d <- liftIO $ liftM utctDay $ getCurrentTime
o' <- getArticles as
(Config _ _ _ _ errs) <- query GetConfig
update $ PutConfig $ Config us (Set.union (Set.fromList $ mapMaybe getItemTitle as) ss) o' d errs
putStrLn "Done refreshing"
getArticles :: [Item] -> IO [L.ByteString]
getArticles = liftM (concat . groupLog 2 stripHtml . catMaybes) . P.mapM (\x->
runMaybeT $ (liftMaybe (getItemLink x >>= parseURI) >>= download >>= (return . prepPage)))
getURI :: Maybe URI -> URI -> MaybeT IO (URI, L.ByteString)
getURI f u = handleErr (NoConnect u) $ MaybeT $ browse $ do
setOutHandler (const $ return ()) >> setErrHandler (const $ return ())
setAllowRedirects True
let fn = case f of
(Just fu) -> insertHeader HdrReferer (show fu)
Nothing -> id
(e,r) <- (request (fn (HTTP.mkRequest HTTP.GET u)))
case HTTP.rspCode r of
(2,_,_) -> return (Just (e,(HTTP.rspBody r)))
otherwise -> return Nothing
handleErr :: FeedError -> MaybeT IO a -> MaybeT IO a
handleErr e a = MaybeT $ do
i <- runMaybeT a
case i of
(Just x) -> update (DelErr e) >> return i
Nothing -> update (AddErr e) >> return Nothing
mkRel :: URI -> L.ByteString -> Maybe URI
mkRel u b = do
x <- parseURIReference (L.unpack b)
r <- x `relativeTo` u
return (r{uriQuery = uriQuery x})
download :: URI -> MaybeT IO (URI, L.ByteString)
download s = do
(s',a) <- getURI Nothing s
let (_::L.ByteString,_::L.ByteString,_::L.ByteString,x) = match (iPat "<A[^>]+HREF=\"([^\"]*)\"[^>]*> *view full text[^<]*</a>") a
(s'',a') <- case x of
(h:_) -> maybe (return (s',a)) (getURI (Just s)) (mkRel s' h)
[] -> return (s',a)
let (_::L.ByteString,_::L.ByteString,_::L.ByteString,y) = match (iPat "<A[^>]+HREF=\"([^\"]*)\"[^>]*> *print *</a>") a'
case y of
(h:_) -> maybe (return (s'',a')) (getURI (Just s')) (mkRel s'' h)
[] -> return (s'',a')
iPat :: L.ByteString -> Regex
iPat = makeRegexOpts (defaultCompOpt {caseSensitive = False}) blankExecOpt
readFeed :: Element -> MaybeT IO Feed
readFeed x = liftMaybe $ readAtom x `mplus` readRSS2 x `mplus` readRSS1 x `mplus` Just (XMLFeed x)
newItems :: Set String -> [URI] -> IO [Item]
newItems is us = liftM concat $ flip P.mapM us $ \u-> do
f <- runMaybeT $ liftM snd (getURI Nothing u) >>= (\x-> handleErr (InvalidFeed u) $ (liftMaybe $ parseXMLDoc x) >>= readFeed)
return $ case f of
(Just a) -> filter (\x-> maybe False (not. flip Set.member is) (getItemTitle x)) (getFeedItems a)
Nothing -> []
mainPage :: ServerPartT IO Response
mainPage = do
(Config us _ _ d errs) <- query GetConfig
let options = map (option <<) (Map.keys us)
return . toResponse . toHtml $ [
header << thetitle << ("Extemp Printing" :: String),
body << centerHtml 210 << [
smallErr (map ((+++ br) . showErr) (Set.toList errs)),
thediv << [paragraph << ("Last Printed: " ++ formatTime defaultTimeLocale "%D" d),
gui "print" << submit "" "Print"],
thediv ! [thestyle "padding-top: 25px;"] <<
table ! [thestyle "background-color:#b0c4de;"] <<
((td << gui "del" << (select ! [name "delItem", thestyle "width: 150px;"]
<< options +++ submit "" "Delete"))
</> (td << gui "add" << [textfield "addItem", submit "" "Add"]))]]
centerHtml :: Int -> Html -> Html
centerHtml n = thediv ! [thestyle $ "margin: 0px auto; width: " ++ (show n) ++ "px; padding-top: 100px;"]
errHtml :: Html -> Html
errHtml = thediv ! [thestyle "color: red;"]
smallErr :: HTML a => a -> Html
smallErr a = errHtml << paragraph ! [thestyle "font-size: small;"] << a
prepPage :: (URI, L.ByteString) -> L.ByteString
prepPage (h,a) = flip L.append "<DIV style=\"page-break-after:always;\"/>" $
L.append "<DIV style=\"color: white; font: normal;\">" $ flip L.append "</DIV>" $ noPrint $ nonRelative $ noScreen $ noScript a where
noPrint = S.replace "media=\"print\"" L.empty
nonRelative s = if L.null s then s else case match (iPat "href=\"([^\" ]*) *\"") s of
(b,m,e,l:_) -> case mkRel h l of
(Just rel) -> b `L.append` "href=\"" `L.append` L.pack (show rel) `L.append` "\"" `L.append` (nonRelative e)
Nothing -> b `L.append` m `L.append` (nonRelative e)
(b,_::L.ByteString,_::L.ByteString,_::[L.ByteString]) -> b
noScreen x = case match (iPat "<link[^>]*media=\"screen\"[^>]*>") x of
(b,_:: L.ByteString,e) -> L.append b e
noScript x = if L.null x then x else case match (iPat "<script[^>]*javascript.*(</script>|/>)") x of
(b,_:: L.ByteString,e) -> L.append b (noScript e)
stripHtml :: L.ByteString -> L.ByteString
stripHtml s = if L.null s then s else case s =~ ("<.*>":: L.ByteString) of
(b,_::L.ByteString,e) -> L.append b (stripHtml e)
controller :: ServerPartT IO Response
controller = updateTimeout 5 >> msum [
dir "login" $ mplus
(methodSP HS.GET loginPage)
(methodSP HS.POST (loginHandler 5 Nothing Nothing (redir "/") (const (const badLogin)))),
dir "register" $ mplus
(methodSP HS.GET registerPage)
(methodSP HS.POST $ withDataFn getRegisterInfo (\x->uncurry (register 5) x (redir "/") (redir "/")) `mplus` noMatch),
loginGate (msum [
dir "print" printIt,
dir "add" (withDataFn (look "addItem") (update . AddSource) >>= redir),
dir "del" (withDataFn (look "delItem") (update . DelSource) >> redir "/"),
dir "invalid" (return $ toResponse $ errHtml << centerHtml 500 << paragraph << ("Invalid Feed URI" :: String)),
nullDir >> mainPage]) (redir "login")]
redir :: String -> ServerPartT IO Response
redir s = seeOther s (toResponse ("" :: String))
getRegisterInfo :: RqData (Username, Password)
getRegisterInfo = do
username <- look "username"
password <- look "password"
passcheck <- look "passcheck"
guard (password == passcheck) >> return (username, password)
loginWith :: Html -> ServerPartT IO Response
loginWith h = return $ toResponse $ centerHtml 201 << table << gui "login" << (h </>
(td << [bold << ("Username:"::String), textfield "username"])
</> (td << [bold << ("Password:"::String), password "password"])
</> (td << submit "" "Login")
</> (td << hotlink "register" (paragraph ! [align "right"] << ("register"::String))))
badLogin :: ServerPartT IO Response
badLogin = loginWith (smallErr ("Invalid username or password" :: String))
loginPage :: ServerPartT IO Response
loginPage = loginWith mempty
noMatch :: ServerPartT IO Response
noMatch = registerWith (smallErr ("Your passwords don't match. Try again."::String))
registerPage :: ServerPartT IO Response
registerPage = registerWith mempty
registerWith :: Html -> ServerPartT IO Response
registerWith h = return $ toResponse $ centerHtml 201 << gui "register" << table << (h </>
(td << [bold << ("Username:"::String), textfield "username"])
</> (td << [bold << ("Password:"::String), password "password"])
</> (td << [bold << ("Confirm Password:"::String), password "passcheck"])
</> (td << submit "" "Login"))
main :: IO ()
main = do
putStrLn "Extemp Printing, version 0.0.1"
putStrLn "Copyright (C) 2010 Sam Anklesaria under BSD"
args <- getArgs
control <- startSystemState (Proxy :: Proxy Config)
tid <- forkIO $ simpleHTTP nullConf{port= case args of {x:[] -> read x; _ -> 8000}} controller
c <- forkIO $ cron 86400 (createCheckpoint control)
d <- forkIO $ cron 300 refreshCache
interpreter
putStrLn "Shutting down..."
killThread tid
killThread c
killThread d
createCheckpoint control
shutdownSystem control
putStrLn "Shutdown complete"
interpreter :: IO ()
interpreter = do
l <- getLine
case l of
"quit" -> return ()
"refresh" -> refreshCache >> interpreter
"state" -> query GetConfig >>= print >> interpreter
otherwise -> interpreter