{-# OPTIONS_GHC -Wno-partial-type-signatures #-}
{-# LANGUAGE PartialTypeSignatures #-}
{-# LANGUAGE NoFieldSelectors #-}
module Main where
import Control.Exception.Base (handle, SomeException (..))
import Control.Monad ((>=>), forever)
import Data.Functor (void, ($>))
import Data.Result
import Data.String (IsString (..))
import Effectful
import Effectful.Concurrent.Async (runConcurrent, async, waitAnyCancel)
import Effectful.Fail (runFailIO, runFail, Fail)
import Network.HTTP.Conduit (tlsManagerSettings, newManager)
import Network.Wai.Handler.Warp (run)
import Puppy.Config
import Puppy.Context
import Puppy.Crypto.RNG (runRNG)
import Puppy.Database (runDB)
import Puppy.Files (runFiles, Files)
import Puppy.Logging
import Puppy.Protocol.ActivityPub (runActivityPub)
import Puppy.Protocol.ActivityStreams (Id (..))
import Puppy.Protocol.HTTP (runHTTP, HTTP, runSign)
import Puppy.Protocol.HTTP.Signature (Server (..))
import Puppy.TaskQueue
import qualified Data.Text as T
import qualified Network.Wai as WAI
import qualified Puppy.Database as DB
import qualified Puppy.Protocol.ActivityPub as AP
import qualified Router
import System.Directory (createDirectoryIfMissing)
main :: IO ()
main = runEff . runRNG . runFiles ".state" "res" $ do
config <- runFailIO $ do
liftIO $ createDirectoryIfMissing False ".state"
loadConfig
manager <- liftIO (newManager tlsManagerSettings)
let handler = runConfig config
. runServerInfo
. runLog (LogGeneral { scopeLabel = "main" }) config.logLevel
. runConcurrent
. runTaskQueue @DeliverTask
. runTaskQueue @PerformTask
. runHTTP manager
handler $ do
info "Configuration loaded"
info "Starting core tasks"
respondTask <- async respond
performTask <- async perform
deliverTask <- async deliver
void $ waitAnyCancel [
respondTask,
performTask,
deliverTask
]
info "Server terminated. See ya!"
where
loop :: (TaskQueue task :> es, Log :> es) => (task -> Eff (Fail : es) ()) -> Eff es ()
loop f = void $ forever $ logFail (dequeue >>= f)
where
logFail :: Log :> es => Eff (Fail : es) () -> Eff es ()
logFail = runFail >=> \case
Left msg -> warn ("Failed: " <> T.pack msg)
Right _ -> debug "Finished"
named = setContext . LogGeneral
deliver = named "task/deliver" $ do
info "Hello!"
loop $ \(DeliverTask activity channel) -> do
let handler = scope activity.id.url
. runSign channel
handler $ do
AP.doDeliverTask activity
info "Stopped"
perform = named "task/perform" $ do
info "Hello!"
conn <- DB.connect
loop $ \(PerformTask activity channel) -> do
let handler = scope activity.id.url
. runSign channel
. runActivityPub
. runDB conn
handler $ do
AP.doPerformTask activity
info "Stopped"
respond = named "task/respond" $ do
info "Hello!"
cfg <- getConfig
conn <- DB.connect
info (T.unwords ["Starting node", cfg.name, "on port", T.pack (show cfg.port)])
withConcEffToIO Ephemeral Unlimited $ \unlift -> do
run cfg.port (app unlift conn)
info "Stopped"
app
:: (
Config :> es,
Files :> es,
HTTP :> es,
IOE :> es,
Log :> es,
ServerInfo :> es,
TaskQueue DeliverTask :> es,
TaskQueue PerformTask :> es
) => (forall r. Eff es r -> IO r)
-> DB.Connection
-> WAI.Application
app unlift conn request respond = do
result <- unlift $ do
config <- getConfig
tracer <- genTracer
let context = LogRequest { scopeLabel = "handle", tracer }
handler = setContext context
. handleFail
. runDB conn
. runSign (Server config)
. runActivityPub
handler $ withEffToIO $ \effToIO -> do
effToIO $ handleExceptions effToIO $ do
body <- liftIO (WAI.strictRequestBody request)
Router.dispatch request body
respond (toResponse result)
where
handleFail :: (Log :> es) => Eff (Fail : es) Result -> Eff es Result
handleFail = runFail >=> \case
Left msg
-> warn ("Uncaught failure: " <> T.pack msg)
$> Error 500 (fromString msg)
Right res
-> debug ("Finished, status: " <> status res)
$> res
handleExceptions :: (forall r. Eff _ r -> IO r) -> Eff _ Result -> Eff _ Result
handleExceptions localUnlift
= liftIO
. handle (\ex -> localUnlift (warn ("Exception: " <> T.pack (show ex)) $> genResponse ex))
. localUnlift
genResponse :: SomeException -> Result
genResponse _ = Error 500 "Internal error"
status = T.pack . show . getStatusCode