{-# LANGUAGE BangPatterns #-} {-# LANGUAGE LinearTypes #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ViewPatterns #-} module Main (main) where import Control.Applicative ((<|>)) import Control.Monad (forM, guard, forM_) import Data.ByteString (ByteString) import Data.ByteString qualified as BS import Data.ByteString.Char8 qualified as BS8 import Data.ByteString.Lazy qualified as BSL import Data.Char (isDigit, ord) import Data.Function (on) import Data.HashMap.Strict qualified as HM import Data.List.NonEmpty (NonEmpty((:|)), groupBy) import Data.Map.Strict (Map) import Data.Map.Strict qualified as Map import Data.Text (Text) import Data.Text qualified as T import Data.Text.Builder.Linear.Buffer import Data.Text.Encoding qualified as TE import Data.Text.IO.Utf8 qualified as T import Data.Time (Day, toGregorian, fromGregorianValid) import System.Environment import System.Exit (die) import System.FilePath (()) import Text.Mustache (Template, compileTemplate, substituteValue, (~>)) import Text.Mustache qualified as M import Text.Mustache.Types (Value(..)) import Text.Mustache.Types qualified as M import Text.Read (readMaybe) import Network.HTTP.Server.Mini import Network.HTTP.Server.Mini.Internal.Instrument (atomicPrintS) -- import Debug.Trace import Calendar import Config import EscapeXML import Index import Util import ZNC import System.Directory (listDirectory) newtype Pages = Pages (Map String Template) getPage :: Pages -> String -> Template getPage (Pages mp) name = case Map.lookup name mp of Just tpl -> tpl Nothing -> error $ "Not a page: " ++ name sendPage200 :: Pages -> String -> Value -> Response sendPage200 pages name sub = responseBuilder status200 [("Content-Type", "text/html")] (TE.encodeUtf8Builder (substituteValue (getPage pages name) sub)) pageIndex :: Config -> Pages -> IO Response pageIndex conf pages = return $ sendPage200 pages "index" $ M.object ["networks" ~> [M.object ["name" ~> nw ,"channels" ~> [M.object ["name" ~> ch, "alias" ~> econfChan2Alias conf Map.! Channel nw ch] | ch <- chs]] | (nw, chs) <- [(nw, ch : map chanChannel chs) | Channel nw ch :| chs <- groupBy ((==) `on` chanNetwork) (econfChannels conf)]]] pageLog :: Config -> Pages -> Index -> Request -> Text -> IO Response pageLog conf pages index req alias = case econfAlias2Chan conf Map.!? alias of Nothing -> page404 "Channel not found" Just chan -> do numEvents <- indexNumEvents index chan let npages = (numEvents + numPerPage - 1) `div` numPerPage (mcurpage, mpagehighlight) <- if | Just (readMaybe . BS8.unpack -> Just pg) <- query req "page" -> return (Just (min npages (max 1 pg)), Nothing) | Just (TE.decodeASCII' -> Just eventID) <- query req "eid" -> do mevidx <- findEventIDLinear index chan eventID case mevidx of Just (_, evidx, _) -> return (Just (evidx `div` numPerPage + 1), Just (evidx `mod` numPerPage)) Nothing -> return (Nothing, Nothing) | otherwise -> return (Just npages, Nothing) case mcurpage of Nothing -> page404 "Event ID not found" Just curpage -> do let ntoleft = min 5 (curpage - 1) ntoright = min 5 (npages - curpage) -- traceShowM (indexNumEvents index chan, npages, curpage, ntoleft, ntoright) events <- indexGetEventsLinear index chan ((curpage - 1) * numPerPage) numPerPage return $ sendPage200 pages "log" $ M.object ["network" ~> chanNetwork chan ,"channel" ~> chanChannel chan ,"alias" ~> alias ,"totalevents" ~> renderLargeNumber numEvents ,"picker" ~> M.object ["prevpage" ~> if curpage > 1 then Just (curpage - 1) else Nothing ,"nextpage" ~> if curpage < npages then Just (curpage + 1) else Nothing ,"firstpage" ~> (curpage > 6) ,"leftdots" ~> (curpage > 7) ,"rightdots" ~> (curpage < npages - 6) ,"lastpage" ~> (curpage < npages - 5) ,"leftnums" ~> [curpage - ntoleft .. curpage - 1] ,"curnum" ~> curpage ,"rightnums" ~> [curpage + 1 .. curpage + ntoright] ,"npages" ~> npages] ,"events" ~> [M.object ["classlist" ~> if mpagehighlight == Just dayidx then Just (classlist `classListAdd` "highlight") else classlist ,"datetime" ~> let YMDHMS (YMD y mo d) (HMS h mi s) = time in pad '0' 4 y ++ '-' : pad '0' 2 mo ++ '-' : pad '0' 2 d ++ ' ' : pad '0' 2 h ++ ':' : pad '0' 2 mi ++ ':' : pad '0' 2 s ,"linkid" ~> eid ,"nickwrap1" ~> nickw1 ,"nickE" ~> escapeXML nick ,"nickwrap2" ~> nickw2 ,"messageE" ~> escapeXML msg] | ((time, eid, ev), dayidx) <- zip events [0..] , let (classlist, (nickw1, nick, nickw2), msg) = renderEvent ev]] where numPerPage = 100 renderLargeNumber :: Int -> Text renderLargeNumber n | n < 0 = "-" <> renderLargeNumber n | n < 1000 = T.show n | otherwise = renderLargeNumber (n `div` 1000) <> "," <> T.pack (pad '0' 3 (n `mod` 1000)) -- rnfValue :: Value -> () -- rnfValue (Object o) = foldMap rnfValue o -- rnfValue (Array a) = foldMap rnfValue a -- rnfValue (Number !_) = () -- rnfValue (String !_) = () -- rnfValue Lambda{} = error "Lambda" -- rnfValue (Bool !_) = () -- rnfValue Null = () -- {-# NOINLINE forceValue #-} -- forceValue :: Value -> Value -- forceValue v = rnfValue v `seq` v data SimpleNode = STextBlock !Text | SSection !Text ![SimpleNode] | SVariable {-# UNPACK #-} !Bool !Text deriving (Show) toSimple :: Template -> [SimpleNode] toSimple (M.Template _ nodes _) = map (go Nothing) nodes where go :: Maybe Text -> M.Node Text -> SimpleNode go _ (M.TextBlock t) = STextBlock t go _ (M.Variable esc (M.NamedData [name])) = SVariable esc name go (Just ctx) (M.Variable esc M.Implicit) = SVariable esc ctx go Nothing (M.Variable _ M.Implicit) = error "toSimple: {{.}} outside section" go _ (M.Section (M.NamedData [name]) ns) = SSection name (map (go (Just name)) ns) go _ node = error $ "toSimple: unsupported node: " ++ show node renderSimple :: [SimpleNode] -> Value -> ByteString renderSimple topnodes topvalue = runBufferBS (\b -> foldlIntoBuffer (go [topvalue]) b topnodes) where go :: [Value] -> Buffer %1 -> SimpleNode -> Buffer go values buf node = case node of STextBlock t -> buf |> t SSection name nodes -> case searchName name values of Just (M.Bool False) -> buf Just (M.Array vec) -> foldlIntoBuffer (\b v -> foldlIntoBuffer (go (v : values)) b nodes) buf (toList vec) Just value -> foldlIntoBuffer (go (value : values)) buf nodes Nothing -> case consumeBuffer buf of () -> error $ "renderSimple: name " ++ show name ++ " not found" SVariable esc name -> case searchName name values of Just (M.String t) | esc -> buf |> escapeXML t | otherwise -> buf |> t Just M.Null -> buf Just value -> case consumeBuffer buf of () -> error $ "renderSimple: unsupported value type " ++ show value Nothing -> case consumeBuffer buf of () -> error $ "renderSimple: name " ++ show name ++ " not found" searchName :: Text -> [Value] -> Maybe Value searchName n (M.Object obj : vs) = HM.lookup n obj <|> searchName n vs searchName _ [] = Nothing searchName n (_ : vs) = searchName n vs -- renderCalendarDayPage :: Template -> Channel -> Day -> Text -> Maybe Int -> [(HMS, EventID, Event)] -> ByteString -- renderCalendarDayPage (M.Template _ nodes _) chan day alias mpagehighlight events = -- runBufferBS (\b -> foldlIntoBuffer goTop b nodes) -- where -- date = T.pack (ymdToString (dayToYMD day)) -- goTop :: Buffer %1-> M.Node Text -> Buffer -- goTop buf node = case node of -- M.TextBlock t -> buf |> t -- M.Variable _ (M.NamedData ["channel"]) -> buf |> chanChannel chan -- M.Variable _ (M.NamedData ["network"]) -> buf |> chanNetwork chan -- M.Variable _ (M.NamedData ["date"]) -> buf |> date -- M.Variable _ (M.NamedData ["alias"]) -> buf |> alias -- M.Section (M.NamedData ["events"]) ns -> -- foldlIntoBuffer -- (\b ((time, eid, event), dayidx) -> -- let tup = renderEvent event -- in foldlIntoBuffer (\b' n -> goEv b' time eid dayidx tup n) b ns) -- buf (zip events [0..]) -- _ -> case consumeBuffer buf of -- () -> error $ "renderCalendarDayPage: unexpected top node " ++ show node -- goEv :: Buffer %1-> HMS -> EventID -> Int -> (Maybe Text, (Maybe Text, Text, Maybe Text), Text) -> M.Node Text -> Buffer -- goEv buf time eid dayidx (classlist, (nickw1, nick, nickw2), msg) node = case node of -- M.TextBlock t -> buf |> t -- M.Variable _ (M.NamedData ["alias"]) -> buf |> alias -- M.Variable _ (M.NamedData ["date"]) -> buf |> date -- M.Variable _ (M.NamedData ["linkid"]) -> buf |> eid -- M.Variable _ (M.NamedData ["time"]) -> -- buf |> T.pack (let HMS h mi s = time -- in pad '0' 2 h ++ ':' : pad '0' 2 mi ++ ':' : pad '0' 2 s) -- M.Variable _ (M.NamedData ["nickE"]) -> buf |> escapeXML nick -- M.Variable _ (M.NamedData ["messageE"]) -> buf |> escapeXML msg -- M.Section (M.NamedData ["classlist"]) ns -> -- let classlist' | mpagehighlight == Just dayidx = Just (classlist `classListAdd` "highlight") -- | otherwise = classlist -- in goMaybe buf goImplicit classlist' ns -- M.Section (M.NamedData ["nickwrap1"]) ns -> goMaybe buf goImplicit nickw1 ns -- M.Section (M.NamedData ["nickwrap2"]) ns -> goMaybe buf goImplicit nickw2 ns -- _ -> case consumeBuffer buf of -- () -> error $ "renderCalendarDayPage: unexpected node in event section: " ++ show node -- goMaybe :: Buffer %1-> (Buffer %1-> a -> M.Node Text -> Buffer) -> Maybe a -> [M.Node Text] -> Buffer -- goMaybe buf _ Nothing _ = buf -- goMaybe buf f (Just x) ns = foldlIntoBuffer (\b n -> f b x n) buf ns -- goImplicit :: Buffer %1-> Text -> M.Node Text -> Buffer -- goImplicit buf cl node = case node of -- M.TextBlock t -> buf |> t -- M.Variable False M.Implicit -> buf |> cl -- _ -> case consumeBuffer buf of -- () -> error $ "renderCalendarDayPage: unexpected node in implicit section: " ++ show node pageCalendarDay :: Config -> Pages -> Index -> Request -> Text -> Text -> IO Response pageCalendarDay conf pages index req alias datestr = case (econfAlias2Chan conf Map.!? alias, parseDatestr datestr) of (Nothing, _) -> page404 "Channel not found" (_, Nothing) -> page404 "Invalid date" (Just chan, Just day) -> do events <- indexGetEventsDay index chan day mpagehighlight <- if | Just (TE.decodeASCII' -> Just eventID) <- query req "eid" -> do mevidx <- findEventIDLinear index chan eventID case mevidx of Just (YMDHMS ymd _, _, evdayidx) | ymd == ymdFromGregorian (toGregorian day) -> return (Just evdayidx) _ -> return Nothing | otherwise -> return Nothing -- return $ responseBS -- status200 -- [("Content-Type", "text/html")] -- (renderCalendarDayPage (getPage pages "calendar-day") chan day alias mpagehighlight events) -- -- return $ sendPage200 pages "calendar-day" $! forceValue $ M.object -- return $ sendPage200 pages "calendar-day" $ M.object return $ responseBS status200 [("Content-Type", "text/html")] $ renderSimple (toSimple (getPage pages "calendar-day")) $ M.object ["network" ~> chanNetwork chan ,"channel" ~> chanChannel chan ,"alias" ~> alias ,"date" ~> ymdToString (dayToYMD day) ,"events" ~> [M.object ["classlist" ~> if mpagehighlight == Just dayidx then Just (classlist `classListAdd` "highlight") else classlist ,"time" ~> let HMS h mi s = time in pad '0' 2 h ++ ':' : pad '0' 2 mi ++ ':' : pad '0' 2 s ,"linkid" ~> eid ,"nickwrap1" ~> nickw1 ,"nickE" ~> escapeXML nick ,"nickwrap2" ~> nickw2 ,"messageE" ~> escapeXML msg] | ((time, eid, ev), dayidx) <- zip events [0..] , let (classlist, (nickw1, nick, nickw2), msg) = renderEvent ev]] where parseDatestr :: Text -> Maybe Day parseDatestr t = do -- YYYY-mm-dd guard (T.length t == 4 + 1 + 2 + 1 + 2) y <- parseInt (T.take 4 t) guard (T.index t 4 == '-') m <- parseInt (T.take 2 (T.drop 5 t)) guard (T.index t 7 == '-') d <- parseInt (T.take 2 (T.drop 8 t)) fromGregorianValid (fromIntegral y) m d parseInt :: Text -> Maybe Int parseInt t | T.all isDigit t = Just (T.foldl' (\n c -> 10 * n + (ord c - ord '0')) 0 t) | otherwise = Nothing classListAdd :: Maybe Text -> Text -> Text classListAdd Nothing t = t classListAdd (Just l) t = l <> " " <> t query :: Request -> ByteString -> Maybe ByteString query req key = lookup key (reqQuery req) -- Returns: (classlist, (nickwrap1, nick, nickwrap2), message) renderEvent :: Event -> (Maybe Text, (Maybe Text, Text, Maybe Text), Text) renderEvent = \case Join n addr -> (j "ev-join", (no, "", j "→"), n <> " joins (" <> addr <> ")") Part n addr reas -> (j "ev-leave", (no, "", j "←"), n <> " parts (" <> addr <> ") (" <> reas <> ")") Quit n addr reas -> (j "ev-leave", (no, "", j "×"), n <> " quits (" <> addr <> ") (" <> reas <> ")") ReNick n n' -> (j "ev-meta", (no, n, no), "is now known as " <> n') Talk n m -> (no, (j "<", n, j ">"), m) Notice n m -> (j "ev-notice", (j "-", n, j "-"), m) Act n m -> (j "ev-act", (no, n, no), m) Kick n by reas -> (j "ev-meta", (no, n, no), "is kicked by " <> by <> " (" <> reas <> ")") Mode n m -> (j "ev-meta", (no, n, no), "sets mode " <> m) Topic n m -> (j "ev-meta", (no, n, no), "sets topic to \"" <> m <> "\"") ParseError -> (j "ev-parseerror", (no, "", no), "") where no = Nothing; j = Just pageCalendar :: Config -> Pages -> Index -> Text -> IO Response pageCalendar conf pages index alias = case econfAlias2Chan conf Map.!? alias of Nothing -> page404 "Channel not found" Just chan -> do ((startDay, endDay), counts) <- indexCalendar index chan return $ sendPage200 pages "calendar" $ M.object ["network" ~> chanNetwork chan ,"channel" ~> chanChannel chan ,"alias" ~> alias ,"years" ~> flip map (reverse (calendarLayout startDay endDay counts)) (\(year, monrows) -> M.object ["year" ~> year ,"monrows" ~> flip map monrows (\monrow -> M.object ["months" ~> flip map monrow (\mmonth -> case mmonth of Nothing -> M.object ["monthname" ~> Null] Just (month, weeks) -> M.object ["month" ~> month ,"month00" ~> pad '0' 2 month ,"monthname" ~> monthNames !! (month - 1) ,"weeks" ~> flip map weeks (\week -> M.object ["days" ~> flip map week (\mday -> fmap (\(d, _count) -> M.object ["date" ~> d, "date00" ~> pad '0' 2 d]) mday)]) ,"phantomweek" ~> (length weeks <= 3)])])])] where monthNames :: [Text] monthNames = ["January", "February", "March", "April" ,"May", "June", "July", "August" ,"September", "October", "November", "December"] page404 :: BSL.ByteString -> IO Response page404 message = return $ responseLBS status404 [("Content-Type", "text/plain")] message mainServe :: FilePath -> IO () mainServe confpath = do config <- enrichConfig <$> readConfig confpath index <- initIndex (confLogsDir config) (econfChannels config) let templateFiles = ["index", "log", "calendar", "calendar-day"] staticFiles = ["style.css", "robots.txt", "favicon.png"] pages <- fmap (Pages . Map.fromList) . forM templateFiles $ \name -> do src <- T.readFile ("pages/" ++ name ++ ".mustache") case compileTemplate name src of Right tpl -> return (name, tpl) Left err -> die (show err) let settings = defaultSettings { setPort = confPort config } atomicPrintS $ "Listening on port " ++ show (confPort config) run settings $ \req -> case reqPath req of [] -> pageIndex config pages ["log", TE.decodeUtf8' -> Right alias] -> pageLog config pages index req alias ["cal", TE.decodeUtf8' -> Right alias] -> pageCalendar config pages index alias ["cal", TE.decodeUtf8' -> Right alias, TE.decodeUtf8' -> Right date] -> pageCalendarDay config pages index req alias date [fname] | fname `elem` staticFiles -> return $ responseFile status200 [] ("pages/" ++ BS8.unpack fname) _ -> page404 "URL not found" testParseLog :: FilePath -> IO () testParseLog fname = print =<< parseLog <$> BS.readFile fname testCount :: FilePath -> IO () testCount confpath = do config <- enrichConfig <$> readConfig confpath forM_ (econfChannels config) $ \(Channel nw ch) -> do -- forM_ [Channel "liberachat" "#haskell"] $ \(Channel nw ch) -> do let dir = confLogsDir config T.unpack nw T.unpack ch files <- listDirectory dir forM_ files $ \file -> do evs <- parseLog <$> BS.readFile (dir file) forM_ evs $ \case (_, Talk _ _msg) -> return () (_, Notice n msg) -> error (show (n, msg)) (_, Act _ _msg) -> return () _ -> return () main :: IO () main = getArgs >>= \case ["test", "parselog", fname] -> testParseLog fname ["test", "count", fname] -> testCount fname ["serve", fname] -> mainServe fname _ -> die "Expected command-line argument (see Main.hs)"