diff options
| author | Tom Smeding <tom@tomsmeding.com> | 2026-03-29 23:25:10 +0200 |
|---|---|---|
| committer | Tom Smeding <tom@tomsmeding.com> | 2026-03-29 23:25:10 +0200 |
| commit | f21dcde54b09913550036e6501cca935278597d9 (patch) | |
| tree | 505f373b1bce690f0bafc2038636721126d9bcad /src/Main.hs | |
Initial
Diffstat (limited to 'src/Main.hs')
| -rw-r--r-- | src/Main.hs | 188 |
1 files changed, 188 insertions, 0 deletions
diff --git a/src/Main.hs b/src/Main.hs new file mode 100644 index 0000000..75b9a96 --- /dev/null +++ b/src/Main.hs @@ -0,0 +1,188 @@ +{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ViewPatterns #-} +module Main (main) where + +import Control.Exception (mask_) +import Control.Monad (when) +import Data.ByteString (ByteString) +import Data.ByteString qualified as BS +import Data.ByteString.Char8 qualified as BS8 +import Data.ByteString.Lazy.Char8 qualified as BSL8 +import Data.Function (on, (&)) +import Data.IORef +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.Encoding qualified as TE +import Data.Text.IO qualified as T +import Network.Wai +import Network.HTTP.Types +import Network.Wai.Handler.Warp (runSettings, defaultSettings, setFork, setPort) +import Network.Wai.Handler.Warp.Internal (Settings(..)) +import System.Environment +import System.Exit (die) +import System.Posix.Resource +import Text.Mustache (Template, compileTemplate, substituteValue, (~>)) +import Text.Mustache qualified as M +import Text.Mustache.Types (Value(..)) +import Text.Read (readMaybe) + +import Debug.Trace + +import Config +import Index +import Util +import ZNC + + +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" + Just chan -> do + let npages = (indexNumEvents index chan + numPerPage - 1) `div` numPerPage + curpage | Just (readMaybe . BS8.unpack -> Just pg) <- query "page" = min npages (max 1 pg) + | otherwise = npages + 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" ~> indexNumEvents index chan + ,"picker" ~> M.object + ["prevpage" ~> if curpage > 1 then Just (curpage - 1) else Nothing + ,"leftnums" ~> [curpage - ntoleft .. curpage - 1] + ,"curnum" ~> curpage + ,"rightnums" ~> [curpage + 1 .. curpage + ntoright] + ,"nextpage" ~> if curpage < npages then Just (curpage + 1) else Nothing + ,"npages" ~> npages] + ,"events" ~> [M.object + ["classlist" ~> 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 + ,"nickwrap1" ~> nickw1 + ,"nick" ~> nick + ,"nickwrap2" ~> nickw2 + ,"message" ~> msg] + | (time, ev) <- events + , let (classlist, (nickw1, nick, nickw2), msg) = renderEvent ev]] + where + numPerPage = 100 + + query :: ByteString -> Maybe ByteString + query key = case lookup key (queryString req) of + Nothing -> Nothing + Just Nothing -> Nothing -- given but empty; treat as not given + Just (Just value) -> Just value + + renderEvent :: Event -> (Maybe Text, (Maybe Text, Text, Maybe Text), Text) + renderEvent = \case + Join n addr -> (j "ev-meta", (no, n, j " →"), "Joins (" <> addr <> ")") + Part n addr reas -> (j "ev-meta", (no, n, j " ←"), "Parts (" <> addr <> ") (" <> reas <> ")") + Quit n addr reas -> (j "ev-meta", (no, n, j " ×"), "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), "<parse error>") + where no = Nothing; j = Just + +page404 :: String -> IO Response +page404 thing = return $ responseLBS + status404 + [("Content-Type", "text/plain")] + (BSL8.pack thing <> " not found") + +getUlimitFiles :: IO Int +getUlimitFiles = do + limits <- getResourceLimit ResourceOpenFiles + let infer ResourceLimitInfinity = maxBound + infer ResourceLimitUnknown = maxBound + infer (ResourceLimit n) = fromIntegral n + return (infer (softLimit limits) `min` infer (hardLimit limits)) + +mainServe :: FilePath -> IO () +mainServe confpath = do + config <- enrichConfig <$> readConfig confpath + + index <- initIndex (confLogsDir config) (econfChannels config) + + pages <- Pages . Map.fromList <$> sequence + [do src <- T.readFile ("pages/" ++ name ++ ".mustache") + case compileTemplate name src of + Right tpl -> return (name, tpl) + Left err -> die (show err) + | name <- ["index", "log"]] + + let staticFiles = ["style.css"] + + ulimitFiles <- getUlimitFiles + counter <- newIORef 0 + let connectionLimit = max 2 (ulimitFiles - 100) + checkedFork :: ((forall a. IO a -> IO a) -> IO ()) -> IO () + checkedFork k = mask_ $ do + count <- atomicModifyIORef' counter (\i -> (i + 1, i + 1)) + print (connectionLimit, count) + when (count <= connectionLimit) $ + settingsFork defaultSettings k + atomicModifyIORef' counter (\i -> (i - 1, ())) + settings = defaultSettings & setFork checkedFork + & setPort (confPort config) + + putStrLn $ "Listening on port " ++ show (confPort config) + runSettings settings $ \req respond -> case pathInfo req of + [] -> + respond =<< pageIndex config pages + ["log", alias] -> + respond =<< pageLog config pages index req alias + [fname] | fname `elem` staticFiles -> + respond $ responseFile status200 [] ("pages/" ++ T.unpack fname) Nothing + _ -> + respond =<< page404 "URL" + +testParseLog :: FilePath -> IO () +testParseLog fname = print =<< parseLog <$> BS.readFile fname + +main :: IO () +main = getArgs >>= \case + ["test", "parselog", fname] -> testParseLog fname + ["serve", fname] -> mainServe fname + _ -> die "Expected command-line argument (see Main.hs)" |
